Repository: goplus/gop
Pull request #1083 (2022-01-04 22:49:41)

V1.1


Patch:
diff --git a/ast/ast.go b/ast/ast.go
index 95029af1..08526d91 100644
--- a/ast/ast.go
+++ b/ast/ast.go
@@ -1114,16 +1114,10 @@ type File struct {
 	Code         []byte
 	NoEntrypoint bool // no entrypoint func to indicate the module entry point.
 	NoPkgDecl    bool // no `package xxx` declaration
-	FileType     FileType
+	IsClass      bool // is a classfile
+	IsProj       bool // is a project classfile
 }
 
-const (
-	FileTypeGo  = -1 // .go (should be negative)
-	FileTypeGop = 0  // .gop
-	FileTypeSpx = 1  // .spx
-	FileTypeGmx = 2  // .gmx
-)
-
 // Pos returns position of first character belonging to the node.
 func (f *File) Pos() token.Pos { return f.Package }
 
diff --git a/ast/filter.go b/ast/filter.go
index 3f5b3cdd..2a48f4b2 100644
--- a/ast/filter.go
+++ b/ast/filter.go
@@ -509,6 +509,6 @@ func MergePackageFiles(pkg *Package, mode MergeMode) *File {
 	// TODO(gri) need to compute unresolved identifiers!
 	return &File{
 		doc, pos, NewIdent(pkg.Name), decls, pkg.Scope,
-		imports, nil, comments, nil, false, false, FileTypeGop,
+		imports, nil, comments, nil, false, false, false, false,
 	}
 }
diff --git a/ast/gopq/gop_query.go b/ast/gopq/gop_query.go
index b0b28858..65ee3e88 100644
--- a/ast/gopq/gop_query.go
+++ b/ast/gopq/gop_query.go
@@ -94,7 +94,7 @@ func NewSourceFrom(
 	fset *token.FileSet, fs parser.FileSystem, path string,
 	filter func(os.FileInfo) bool, mode parser.Mode) (doc NodeSet, err error) {
 
-	pkgs, err := parser.ParseFSDir(fset, fs, path, filter, mode)
+	pkgs, err := parser.ParseFSDir(fset, fs, path, parser.Config{Filter: filter, Mode: mode})
 	if err != nil {
 		return
 	}
diff --git a/cl/classfile.go b/cl/classfile.go
index 6d4cd590..86945a8f 100644
--- a/cl/classfile.go
+++ b/cl/classfile.go
@@ -22,45 +22,14 @@ import (
 	"go/types"
 	"path/filepath"
 	"strings"
-	"syscall"
 
 	"github.com/goplus/gop/ast"
-	"github.com/goplus/gop/parser"
 	"github.com/goplus/gop/token"
 	"github.com/goplus/gox"
 )
 
 // -----------------------------------------------------------------------------
 
-type gmxInfo struct {
-	extSpx   string
-	pkgPaths []string
-}
-
-var (
-	gmxTypes = map[string]gmxInfo{
-		".gmx": {".spx", []string{"github.com/goplus/spx", "math"}},
-	}
-)
-
-// RegisterClassFileType registers Go+ class file types.
-func RegisterClassFileType(extGmx, extSpx string, pkgPaths ...string) error {
-	if pkgPaths == nil {
-		panic("RegisterClassFileType: no pkgPath specified")
-	}
-	parser.RegisterFileType(extGmx, ast.FileTypeGmx)
-	if extSpx != "" {
-		parser.RegisterFileType(extSpx, ast.FileTypeSpx)
-	}
-	if _, ok := gmxTypes[extGmx]; ok {
-		return syscall.EEXIST
-	}
-	gmxTypes[extGmx] = gmxInfo{extSpx, pkgPaths}
-	return nil
-}
-
-// -----------------------------------------------------------------------------
-
 type gmxSettings struct {
 	gameClass  string
 	extSpx     string
@@ -91,7 +60,7 @@ func (p *gmxSettings) getScheds(cb *gox.CodeBuilder) []goast.Stmt {
 	return p.schedStmts
 }
 
-func newGmx(pkg *gox.Package, file string) *gmxSettings {
+func newGmx(pkg *gox.Package, file string, conf *Config) *gmxSettings {
 	_, name := filepath.Split(file)
 	ext := filepath.Ext(name)
 	if idx := strings.Index(name, "."); idx > 0 {
@@ -100,16 +69,19 @@ func newGmx(pkg *gox.Package, file string) *gmxSettings {
 			name = "_main"
 		}
 	}
-	gt := gmxTypes[ext]
-	pkgPaths := gt.pkgPaths
-	p := &gmxSettings{extSpx: gt.extSpx, gameClass: name, pkgPaths: pkgPaths}
+	gt, ok := conf.LookupClass(ext)
+	if !ok {
+		panic("TODO: class not found")
+	}
+	pkgPaths := gt.PkgPaths
+	p := &gmxSettings{extSpx: gt.WorkExt, gameClass: name, pkgPaths: pkgPaths}
 	p.pkgImps = make([]*gox.PkgRef, len(pkgPaths))
 	for i, pkgPath := range pkgPaths {
 		p.pkgImps[i] = pkg.Import(pkgPath)
 	}
 	spx := p.pkgImps[0]
 	p.game, p.gameIsPtr = spxRef(spx, "Gop_game", "Game")
-	if gt.extSpx != "" {
+	if gt.WorkExt != "" {
 		p.sprite, _ = spxRef(spx, "Gop_sprite", "Sprite")
 	}
 	if x := getStringConst(spx, "Gop_sched"); x != "" {
@@ -175,7 +147,7 @@ func getFields(ctx *blockCtx, f *ast.File) (specs []ast.Spec) {
 }
 
 func setBodyHandler(ctx *blockCtx) {
-	if ctx.fileType > 0 { // in a Go+ class file
+	if ctx.isClass { // in a Go+ class file
 		if scheds := ctx.getScheds(ctx.cb); scheds != nil {
 			ctx.cb.SetBodyHandler(func(body *goast.BlockStmt, kind int) {
 				idx := 0
diff --git a/cl/compile.go b/cl/compile.go
index 51f3cf98..9cddd01c 100644
--- a/cl/compile.go
+++ b/cl/compile.go
@@ -29,6 +29,7 @@ import (
 
 	"github.com/goplus/gop/ast"
 	"github.com/goplus/gop/token"
+	"github.com/goplus/gop/x/mod/modfile"
 	"github.com/goplus/gox"
 )
 
@@ -70,6 +71,8 @@ func (p *Errors) Error() string {
 
 // -----------------------------------------------------------------------------
 
+type Class = modfile.Classfile
+
 // Config of loading Go+ packages.
 type Config struct {
 	// WorkingDir is the directory in which to run gop compiler.
@@ -82,6 +85,9 @@ type Config struct {
 	// If Fset is nil, Load will use a new fileset, but preserve Fset's value.
 	Fset *token.FileSet
 
+	// LookupClass lookups a class by specified file extension.
+	LookupClass func(ext string) (c *Class, ok bool)
+
 	// An Importer resolves import paths to Packages.
 	Importer types.Importer
 
@@ -245,7 +251,7 @@ type blockCtx struct {
 	classRecv    *ast.FieldList // avaliable when gmxSettings != nil
 	fileLine     bool
 	relativePath bool
-	fileType     int16
+	isClass      bool
 }
 
 func (bc *blockCtx) findImport(name string) (pr *gox.PkgRef, ok bool) {
@@ -363,8 +369,8 @@ func NewPackage(pkgPath string, pkg *ast.Package, conf *Config) (p *gox.Package,
 	}
 	p = gox.NewPackage(pkgPath, pkg.Name, confGox)
 	for file, gmx := range pkg.Files {
-		if gmx.FileType == ast.FileTypeGmx {
-			ctx.gmxSettings = newGmx(p, file)
+		if gmx.IsProj {
+			ctx.gmxSettings = newGmx(p, file, conf)
 			break
 		}
 	}
@@ -372,14 +378,14 @@ func NewPackage(pkgPath string, pkg *ast.Package, conf *Config) (p *gox.Package,
 		preloadFile(p, ctx, fpath, f, targetDir, conf)
 	}
 	for _, f := range pkg.Files {
-		if f.FileType == ast.FileTypeGmx {
+		if f.IsProj {
 			loadFile(ctx, f)
 			gmxMainFunc(p, ctx)
 			break
 		}
 	}
 	for _, f := range pkg.Files {
-		if f.FileType != ast.FileTypeGmx { // only one .gmx file
+		if !f.IsProj { // only one .gmx file
 			loadFile(ctx, f)
 		}
 	}
@@ -406,16 +412,15 @@ func hasMethod(o types.Object, name string) bool {
 	return false
 }
 
-func getEntrypoint(ft ast.FileType, isMod bool) string {
-	switch ft {
-	case ast.FileTypeSpx:
-		return "Main"
-	case ast.FileTypeGmx:
+func getEntrypoint(f *ast.File, isMod bool) string {
+	switch {
+	case f.IsProj:
 		return "MainEntry"
+	case f.IsClass:
+		return "Main"
+	case isMod:
+		return "init"
 	default:
-		if isMod {
-			return "init"
-		}
 		return "main"
 	}
 }
@@ -456,27 +461,29 @@ func preloadFile(p *gox.Package, parent *pkgCtx, file string, f *ast.File, targe
 	fileLine := !conf.NoFileLine
 	testingFile := strings.HasSuffix(file, "_test.gop")
 	ctx := &blockCtx{
-		pkg: p, pkgCtx: parent, cb: p.CB(), fset: p.Fset, targetDir: targetDir, fileType: f.FileType,
-		fileLine: fileLine, relativePath: conf.RelativePath, imports: make(map[string]*gox.PkgRef),
+		pkg: p, pkgCtx: parent, cb: p.CB(), fset: p.Fset, targetDir: targetDir,
+		fileLine: fileLine, relativePath: conf.RelativePath, isClass: f.IsClass,
+		imports: make(map[string]*gox.PkgRef),
 	}
 	var classType string
 	var baseTypeName string
 	var baseType types.Type
-	switch f.FileType {
-	case ast.FileTypeSpx:
-		if parent.gmxSettings != nil {
-			classType = getDefaultClass(file)
-			o := parent.sprite
-			baseTypeName, baseType = o.Name(), o.Type()
-		}
-		// TODO: panic
-	case ast.FileTypeGmx:
+	var spxClass bool
+	switch {
+	case f.IsProj:
 		classType = parent.gameClass
 		o := parent.game
 		baseTypeName, baseType = o.Name(), o.Type()
 		if parent.gameIsPtr {
 			baseType = types.NewPointer(baseType)
 		}
+	case f.IsClass:
+		if parent.gmxSettings != nil {
+			classType = getDefaultClass(file)
+			o := parent.sprite
+			baseTypeName, baseType, spxClass = o.Name(), o.Type(), true
+		}
+		// TODO: panic
 	}
 	if classType != "" {
 		if debugLoad {
@@ -501,7 +508,7 @@ func preloadFile(p *gox.Package, parent *pkgCtx, file string, f *ast.File, targe
 				pkg := p.Types
 				flds := make([]*types.Var, 1, 2)
 				flds[0] = types.NewField(pos, pkg, baseTypeName, baseType, true)
-				if f.FileType == ast.FileTypeSpx {
+				if spxClass {
 					typ := toType(ctx, &ast.StarExpr{X: &ast.Ident{Name: parent.gameClass}})
 					fld := types.NewField(pos, pkg, getTypeName(typ), typ, true)
 					flds = append(flds, fld)
@@ -539,7 +546,7 @@ func preloadFile(p *gox.Package, parent *pkgCtx, file string, f *ast.File, targe
 		switch d := decl.(type) {
 		case *ast.FuncDecl:
 			if f.NoEntrypoint && d.Name.Name == "main" {
-				d.Name.Name = getEntrypoint(f.FileType, f.Name.Name != "main")
+				d.Name.Name = getEntrypoint(f, f.Name.Name != "main")
 			}
 			if ctx.classRecv != nil { // in class file (.spx/.gmx)
 				if d.Recv == nil {
diff --git a/cl/expr.go b/cl/expr.go
index 81f43ddd..07e58477 100644
--- a/cl/expr.go
+++ b/cl/expr.go
@@ -80,7 +80,7 @@ func compileIdent(ctx *blockCtx, ident *ast.Ident, flags int) *gox.PkgRef {
 		}
 	}
 
-	if ctx.fileType > 0 { // in a Go+ class file
+	if ctx.isClass { // in a Go+ class file
 		if fn := ctx.cb.Func(); fn != nil {
 			sig := fn.Ancestor().Type().(*types.Signature)
 			if recv := sig.Recv(); recv != nil {
diff --git a/cl/internal/gop.mod b/cl/internal/gop.mod
deleted file mode 100644
index bd102f50..00000000
--- a/cl/internal/gop.mod
+++ /dev/null
@@ -1,4 +0,0 @@
-module github.com/goplus/gop/cl/internal
-
-go 1.16
-gop 1.0
diff --git a/cmd/gengo/gen.go b/cmd/gengo/gen.go
deleted file mode 100644
index f1c6ae99..00000000
--- a/cmd/gengo/gen.go
+++ /dev/null
@@ -1,235 +0,0 @@
-/*
- * Copyright (c) 2021 The GoPlus Authors (goplus.org). All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package gengo
-
-import (
-	"errors"
-	"fmt"
-	"io/ioutil"
-	"os"
-	"path"
-	"path/filepath"
-	"strings"
-	"syscall"
-	"time"
-
-	"github.com/goplus/gop/ast"
-	"github.com/goplus/gop/cl"
-	"github.com/goplus/gop/parser"
-	"github.com/goplus/gop/token"
-	"github.com/goplus/gox"
-)
-
-const (
-	PkgFlagGo = 1 << iota
-	PkgFlagGoPlus
-	PkgFlagGoGen
-	PkgFlagGopModified
-	PkgFlagSpx
-	PkgFlagGmx
-)
-
-const (
-	autoGenFile      = "gop_autogen.go"
-	autoGenTestFile  = "gop_autogen_test.go"
-	autoGen2TestFile = "gop_autogen2_test.go"
-)
-
-type Error struct {
-	PkgDir string
-	Stage  string
-	Err    error
-}
-
-func (p *Error) Error() string { // TODO: more friendly
-	return p.Err.Error()
-}
-
-// -----------------------------------------------------------------------------
-
-type Runner struct {
-	errs  []*Error
-	after func(p *Runner, dir string, pkgFlags int) error
-}
-
-func (p *Runner) SetAfter(after func(p *Runner, dir string, flags int) error) {
-	p.after = after
-}
-
-func (p *Runner) Errors() []*Error {
-	return p.errs
-}
-
-func (p *Runner) ResetErrors() []*Error {
-	errs := p.errs
-	p.errs = nil
-	return errs
-}
-
-func (p *Runner) GenGo(dir string, recursive bool, base *cl.Config) {
-	fis, err := ioutil.ReadDir(dir)
-	if err != nil {
-		p.addError(dir, "readDir", err)
-		return
-	}
-	var gopTime time.Time
-	var gogenTime time.Time
-	var pkgFlags int
-	for _, fi := range fis {
-		fname := fi.Name()
-		if strings.HasPrefix(fname, "_") {
-			continue
-		}
-		if fi.IsDir() {
-			if recursive {
-				pkgDir := path.Join(dir, fname)
-				p.GenGo(pkgDir, true, base)
-			}
-			continue
-		}
-		ext := filepath.Ext(fname)
-		if flag, ok := extPkgFlags[ext]; ok {
-			modTime := fi.ModTime()
-			switch flag {
-			case PkgFlagGo:
-				switch fname {
-				case autoGenFile, autoGenTestFile, autoGen2TestFile:
-					flag = PkgFlagGoGen
-					if (pkgFlags&PkgFlagGoGen) == 0 || gogenTime.After(modTime) {
-						gogenTime = modTime
-					}
-				}
-			default:
-				if modTime.After(gopTime) {
-					gopTime = modTime
-				}
-			}
-			pkgFlags |= flag
-		}
-	}
-	if pkgFlags != 0 {
-		if (pkgFlags & PkgFlagGo) != 0 { // a Go package
-			// TODO: depency check
-		} else if gopTime.After(gogenTime) { // update a Go+ package
-			fmt.Printf("GenGoPkg %s\n", dir)
-			pkgFlags |= PkgFlagGopModified
-			p.GenGoPkg(dir, base)
-		}
-		if p.after != nil {
-			if err = p.after(p, dir, pkgFlags); err != nil {
-				p.addError(dir, "after", err)
-			}
-		}
-	}
-}
-
-var (
-	extPkgFlags = map[string]int{
-		".gop": PkgFlagGoPlus,
-		".go":  PkgFlagGo,
-	}
-)
-
-func RegisterPkgFlags(ext string, flag int) {
-	if flag != PkgFlagGmx && flag != PkgFlagSpx {
-		panic("RegisterPkgFlags: flag should be PkgFlagGmx or PkgFlagSpx")
-	}
-	extPkgFlags[ext] = flag
-}
-
-func (p *Runner) GenGoPkg(pkgDir string, base *cl.Config) (err error) {
-	defer func() {
-		if e := recover(); e != nil {
-			switch v := e.(type) {
-			case error:
-				err = p.addError(pkgDir, "recover", v)
-			case string:
-				err = p.addError(pkgDir, "recover", errors.New(v))
-			default:
-				panic(e)
-			}
-		}
-	}()
-
-	pkgDir, _ = filepath.Abs(pkgDir)
-
-	conf := *base
-	conf.WorkingDir = pkgDir
-	if conf.Fset == nil {
-		conf.Fset = token.NewFileSet()
-	}
-	pkgs, err := parser.ParseDir(conf.Fset, pkgDir, nil, parser.ParseComments)
-	if err != nil {
-		return p.addError(pkgDir, "parse", err)
-	}
-	if len(pkgs) == 0 {
-		return syscall.ENOENT
-	}
-
-	var pkgTest *ast.Package
-	for name, pkg := range pkgs {
-		if strings.HasSuffix(name, "_test") {
-			if pkgTest != nil {
-				panic("TODO: has multi test package?")
-			}
-			pkgTest = pkg
-			continue
-		}
-		out, err := cl.NewPackage("", pkg, &conf)
-		if err != nil {
-			return p.addError(pkgDir, "compile", err)
-		}
-		err = saveGoFile(pkgDir, out)
-		if err != nil {
-			return p.addError(pkgDir, "save", err)
-		}
-	}
-	if pkgTest != nil {
-		out, err := cl.NewPackage("", pkgTest, &conf)
-		if err != nil {
-			return p.addError(pkgDir, "compile", err)
-		}
-		err = gox.WriteFile(filepath.Join(pkgDir, autoGen2TestFile), out, true)
-		if err != nil {
-			return p.addError(pkgDir, "save", err)
-		}
-	}
-	return nil
-}
-
-func (p *Runner) addError(pkgDir string, stage string, err error) error {
-	e := &Error{PkgDir: pkgDir, Stage: stage, Err: err}
-	p.errs = append(p.errs, e)
-	return e
-}
-
-func saveGoFile(dir string, pkg *gox.Package) error {
-	err := os.MkdirAll(dir, 0755)
-	if err != nil {
-		return err
-	}
-	err = gox.WriteFile(filepath.Join(dir, autoGenFile), pkg, false)
-	if err != nil {
-		return err
-	}
-	if pkg.HasTestingFile() {
-		return gox.WriteFile(filepath.Join(dir, autoGenTestFile), pkg, true)
-	}
-	return nil
-}
-
-// -----------------------------------------------------------------------------
diff --git a/cmd/gop/main.go b/cmd/gop/main.go
index 0f8eb1be..d8b8fb49 100644
--- a/cmd/gop/main.go
+++ b/cmd/gop/main.go
@@ -25,6 +25,7 @@ import (
 	"github.com/goplus/gop/cmd/internal/bug"
 	"github.com/goplus/gop/cmd/internal/deps"
 	"github.com/goplus/gop/cmd/internal/gopget"
+	"github.com/goplus/gop/cmd/internal/list"
 	"github.com/qiniu/x/log"
 
 	"github.com/goplus/gop/cmd/internal/base"
@@ -56,6 +57,7 @@ func init() {
 		mod.Cmd,
 		gopfmt.Cmd,
 		gopget.Cmd,
+		list.Cmd,
 		bug.Cmd,
 		clean.Cmd,
 		env.Cmd,
diff --git a/cmd/internal/base/gocmd.go b/cmd/internal/base/gocmd.go
index 1ff7a8d6..1390acd1 100644
--- a/cmd/internal/base/gocmd.go
+++ b/cmd/internal/base/gocmd.go
@@ -17,74 +17,11 @@
 package base
 
 import (
-	"flag"
-	"fmt"
 	"log"
 	"os"
 	"os/exec"
-	"path/filepath"
-	"strings"
-
-	"github.com/goplus/gop/cl"
-	"github.com/goplus/gop/cmd/gengo"
 )
 
-// SkipSwitches skips all switches and returns non-switch arguments.
-func SkipSwitches(args []string, f *flag.FlagSet) []string {
-	out := make([]string, 0, len(args))
-	for _, arg := range args {
-		if strings.HasPrefix(arg, "-") {
-			if f.Lookup(arg[1:]) == nil { // flag not found
-				continue
-			}
-		}
-		out = append(out, arg)
-	}
-	return out
-}
-
-// GetBuildDir Get build directory from arguments
-func GetBuildDir(args []string) (dir string, recursive bool) {
-	if len(args) == 0 {
-		args = []string{"."}
-	}
-	dir = args[0]
-	if strings.HasSuffix(dir, "/...") {
-		dir = dir[:len(dir)-4]
-		recursive = true
-	}
-	if fi, err := os.Stat(dir); err == nil {
-		if fi.IsDir() {
-			return
-		}
-		return filepath.Dir(dir), recursive
-	}
-	return
-}
-
-// GenGoForBuild Generate go code before building or installing, and cache pkgs if success
-func GenGoForBuild(dir string, recursive bool, errorHandle func()) {
-	hasError := false
-	runner := new(gengo.Runner)
-	runner.SetAfter(func(p *gengo.Runner, dir string, flags int) error {
-		errs := p.ResetErrors()
-		if errs != nil {
-			hasError = true
-			for _, err := range errs {
-				fmt.Fprintln(os.Stderr, err)
-			}
-			fmt.Fprintln(os.Stderr)
-		}
-		return nil
-	})
-	baseConf := &cl.Config{}
-	runner.GenGo(dir, recursive, baseConf)
-	if hasError {
-		errorHandle()
-		os.Exit(1)
-	}
-}
-
 // RunGoCmd executes `go` command tools.
 func RunGoCmd(dir string, op string, args ...string) {
 	cmd := exec.Command("go", append([]string{op}, args...)...)
diff --git a/cmd/internal/build/build.go b/cmd/internal/build/build.go
index ebbcc886..a230e7bc 100644
--- a/cmd/internal/build/build.go
+++ b/cmd/internal/build/build.go
@@ -18,52 +18,54 @@
 package build
 
 import (
-	"fmt"
 	"os"
 
 	"github.com/qiniu/x/log"
 
 	"github.com/goplus/gop/cl"
 	"github.com/goplus/gop/cmd/internal/base"
-	"github.com/goplus/gop/cmd/internal/modload"
+	"github.com/goplus/gop/x/gengo"
 	"github.com/goplus/gox"
 )
 
-// -----------------------------------------------------------------------------
-
-// Cmd - gop build
+// gop build
 var Cmd = &base.Command{
-	UsageLine: "gop build [-v] [-o output] [packages]",
+	UsageLine: "gop build [-v -o output] [packages]",
 	Short:     "Build Go+ files",
 }
 
 var (
-	flagBuildOutput string
-	flagVerbose     = flag.Bool("v", false, "print verbose information.")
-	flag            = &Cmd.Flag
+	flagVerbose = flag.Bool("v", false, "print verbose information.")
+	flagOutput  = flag.String("o", "", "gop build output file.")
+	flag        = &Cmd.Flag
 )
 
 func init() {
-	flag.StringVar(&flagBuildOutput, "o", "", "gop build output file.")
 	Cmd.Run = runCmd
 }
 
 func runCmd(_ *base.Command, args []string) {
-	err := flag.Parse(base.SkipSwitches(args, flag))
+	err := flag.Parse(args)
 	if err != nil {
 		log.Fatalln("parse input arguments failed:", err)
 	}
-	ssargs := flag.Args()
-	dir, recursive := base.GetBuildDir(ssargs)
+
+	pattern := flag.Args()
+	if len(pattern) == 0 {
+		pattern = []string{"."}
+	}
 
 	if *flagVerbose {
 		gox.SetDebug(gox.DbgFlagAll &^ gox.DbgFlagComments)
 		cl.SetDebug(cl.DbgFlagAll)
 		cl.SetDisableRecover(true)
 	}
-	modload.UpdateGoMod(dir)
-	base.GenGoForBuild(dir, recursive, func() { fmt.Fprintln(os.Stderr, "GenGo failed, stop building") })
-	base.RunGoCmd(dir, "build", args...)
+	_ = flagOutput
+
+	if !gengo.GenGo(gengo.Config{}, false, pattern...) {
+		os.Exit(1)
+	}
+	base.RunGoCmd(".", "build", args...)
 }
 
 // -----------------------------------------------------------------------------
diff --git a/cmd/internal/deps/deps.go b/cmd/internal/deps/deps.go
index 8d1ead7e..1d9249ff 100644
--- a/cmd/internal/deps/deps.go
+++ b/cmd/internal/deps/deps.go
@@ -19,7 +19,6 @@ package deps
 import (
 	"fmt"
 	"log"
-	"sort"
 
 	"github.com/goplus/gop/cmd/internal/base"
 	"github.com/goplus/gop/x/gopmod"
@@ -27,7 +26,7 @@ import (
 
 // -----------------------------------------------------------------------------
 
-// Cmd - gop deps
+// gop deps
 var Cmd = &base.Command{
 	UsageLine: "gop deps [-v] [package]",
 	Short:     "Show dependencies of a package or module",
@@ -47,6 +46,7 @@ func runCmd(cmd *base.Command, args []string) {
 	if err != nil {
 		log.Fatalln("parse input arguments failed:", err)
 	}
+
 	var dir string
 	narg := flag.NArg()
 	if narg < 1 {
@@ -54,13 +54,9 @@ func runCmd(cmd *base.Command, args []string) {
 	} else {
 		dir = flag.Arg(0)
 	}
-	getDeps(dir)
-}
 
-func getDeps(dir string) {
 	imports, err := gopmod.Imports(dir)
 	check(err)
-	sort.Strings(imports)
 	for _, imp := range imports {
 		fmt.Println(imp)
 	}
diff --git a/cmd/internal/gengo/go.go b/cmd/internal/gengo/go.go
index 32cf189f..3cb08b40 100644
--- a/cmd/internal/gengo/go.go
+++ b/cmd/internal/gengo/go.go
@@ -18,66 +18,26 @@
 package gengo
 
 import (
-	"fmt"
 	"os"
-	"strings"
 
 	"github.com/qiniu/x/log"
 
 	"github.com/goplus/gop/cl"
-	"github.com/goplus/gop/cmd/gengo"
 	"github.com/goplus/gop/cmd/internal/base"
-	"github.com/goplus/gop/cmd/internal/modload"
+	"github.com/goplus/gop/x/gengo"
 	"github.com/goplus/gox"
 )
 
-// -----------------------------------------------------------------------------
-/*
-var (
-	errTestFailed = errors.New("test failed")
-)
-
-func testPkg(p *gengo.Runner, dir string, flags int) error {
-	if flags == gengo.PkgFlagGo { // don't test Go packages
-		return nil
-	}
-	cmd1 := exec.Command("go", "run", path.Join(dir, "gop_autogen.go"))
-	gorun, err := cmd1.CombinedOutput()
-	if err != nil {
-		os.Stderr.Write(gorun)
-		fmt.Fprintf(os.Stderr, "[ERROR] `%v` failed: %v\n", cmd1, err)
-		return err
-	}
-	cmd2 := exec.Command("gop", "run", "-quiet", dir) // -quiet: don't generate any log
-	qrun, err := cmd2.CombinedOutput()
-	if err != nil {
-		os.Stderr.Write(qrun)
-		fmt.Fprintf(os.Stderr, "[ERROR] `%v` failed: %v\n", cmd2, err)
-		return err
-	}
-	if !bytes.Equal(gorun, qrun) {
-		fmt.Fprintf(os.Stderr, "[ERROR] Output has differences!\n")
-		fmt.Fprintf(os.Stderr, ">>> Output of `%v`:\n", cmd1)
-		os.Stderr.Write(gorun)
-		fmt.Fprintf(os.Stderr, "\n>>> Output of `%v`:\n", cmd2)
-		os.Stderr.Write(qrun)
-		return errTestFailed
-	}
-	return nil
-}
-*/
-// -----------------------------------------------------------------------------
-
-// Cmd - gop go
+// gop go
 var Cmd = &base.Command{
-	UsageLine: "gop go [-debug -test -slow] <gopSrcDir>",
+	UsageLine: "gop go [-v -n] [packages]",
 	Short:     "Convert Go+ packages into Go packages",
 }
 
 var (
-	flag      = &Cmd.Flag
-	flagDebug = flag.Bool("debug", false, "set log level to debug")
-	flagTest  = flag.Bool("test", false, "test Go+ package")
+	flagDontRun = flag.Bool("n", false, "print the processing flow but do not run them.")
+	flagVerbose = flag.Bool("v", false, "print verbose information.")
+	flag        = &Cmd.Flag
 )
 
 func init() {
@@ -89,37 +49,18 @@ func runCmd(cmd *base.Command, args []string) {
 	if err != nil {
 		log.Fatalln("parse input arguments failed:", err)
 	}
-	if flag.NArg() < 1 {
-		cmd.Usage(os.Stderr)
-		return
+	pattern := flag.Args()
+	if len(pattern) == 0 {
+		pattern = []string{"."}
 	}
-	if *flagDebug {
-		log.SetOutputLevel(log.Ldebug)
-		gox.SetDebug(gox.DbgFlagAll)
+
+	if *flagVerbose {
+		gox.SetDebug(gox.DbgFlagAll &^ gox.DbgFlagComments)
+		cl.SetDebug(cl.DbgFlagAll)
+		cl.SetDisableRecover(true)
 	}
-	dir := flag.Arg(0)
-	dir = strings.TrimSuffix(dir, "/...")
-	modload.UpdateGoMod(dir)
-	runner := new(gengo.Runner)
-	runner.SetAfter(func(p *gengo.Runner, dir string, flags int) error {
-		errs := p.ResetErrors()
-		if errs != nil {
-			for _, err := range errs {
-				fmt.Fprintln(os.Stderr, err)
-			}
-			fmt.Fprintln(os.Stderr)
-		} else if *flagTest {
-			panic("gop go -test: not impl")
-		}
-		return nil
-	})
-	runner.GenGo(dir, true, &cl.Config{})
-	errs := runner.Errors()
-	if errs != nil {
-		for _, err := range errs {
-			fmt.Fprintln(os.Stderr, err)
-		}
-		os.Exit(-1)
+	if !gengo.GenGo(gengo.Config{}, *flagDontRun, pattern...) {
+		os.Exit(1)
 	}
 }
 
diff --git a/cmd/internal/install/install.go b/cmd/internal/install/install.go
index b79d9815..51444286 100644
--- a/cmd/internal/install/install.go
+++ b/cmd/internal/install/install.go
@@ -18,18 +18,19 @@
 package install
 
 import (
-	"fmt"
-	"github.com/qiniu/x/log"
 	"os"
 
+	"github.com/qiniu/x/log"
+
 	"github.com/goplus/gop/cl"
 	"github.com/goplus/gop/cmd/internal/base"
+	"github.com/goplus/gop/x/gengo"
 	"github.com/goplus/gox"
 )
 
-// Cmd - gop install
+// gop install
 var Cmd = &base.Command{
-	UsageLine: "gop install [-v] <GopPackages>",
+	UsageLine: "gop install [-v] [packages]",
 	Short:     "Build Go+ files and install target to GOBIN",
 }
 
@@ -43,20 +44,26 @@ func init() {
 }
 
 func runCmd(_ *base.Command, args []string) {
-	err := flag.Parse(base.SkipSwitches(args, flag))
+	err := flag.Parse(args)
 	if err != nil {
 		log.Fatalln("parse input arguments failed:", err)
 	}
-	ssargs := flag.Args()
-	dir, recursive := base.GetBuildDir(ssargs)
+
+	pattern := flag.Args()
+	if len(pattern) == 0 {
+		pattern = []string{"."}
+	}
 
 	if *flagVerbose {
 		gox.SetDebug(gox.DbgFlagAll &^ gox.DbgFlagComments)
 		cl.SetDebug(cl.DbgFlagAll)
 		cl.SetDisableRecover(true)
 	}
-	base.GenGoForBuild(dir, recursive, func() { fmt.Fprintln(os.Stderr, "GenGo failed, stop installing") })
-	base.RunGoCmd(dir, "install", args...)
+
+	if !gengo.GenGo(gengo.Config{}, false, pattern...) {
+		os.Exit(1)
+	}
+	base.RunGoCmd(".", "install", args...)
 }
 
 // -----------------------------------------------------------------------------
diff --git a/cmd/internal/modload/modload.go b/cmd/internal/list/list.go
similarity index 55%
rename from cmd/internal/modload/modload.go
rename to cmd/internal/list/list.go
index 8f87a5de..11942d1b 100644
--- a/cmd/internal/modload/modload.go
+++ b/cmd/internal/list/list.go
@@ -14,29 +14,55 @@
  * limitations under the License.
  */
 
-package modload
+package list
 
 import (
-	"github.com/goplus/gop/cl"
-	"github.com/goplus/gop/cmd/gengo"
+	"fmt"
+	"log"
+
+	"github.com/goplus/gop/cmd/internal/base"
 	"github.com/goplus/gop/x/gopmod"
 )
 
 // -----------------------------------------------------------------------------
 
-func UpdateGoMod(dir string) {
-	p, err := gopmod.Load(dir)
+// gop list
+var Cmd = &base.Command{
+	UsageLine: "gop list [-json] [packages]",
+	Short:     "List packages or modules",
+}
+
+var (
+	flag = &Cmd.Flag
+	_    = flag.Bool("json", false, "printing in JSON format.")
+)
+
+func init() {
+	Cmd.Run = runCmd
+}
+
+func runCmd(cmd *base.Command, args []string) {
+	err := flag.Parse(args)
+	if err != nil {
+		log.Fatalln("parse input arguments failed:", err)
+	}
+
+	pattern := flag.Args()
+	if len(pattern) == 0 {
+		pattern = []string{"."}
+	}
+
+	pkgPaths, err := gopmod.List(pattern...)
+	check(err)
+	for _, pkgPath := range pkgPaths {
+		fmt.Println(pkgPath)
+	}
+}
+
+func check(err error) {
 	if err != nil {
-		return
+		log.Fatalln(err)
 	}
-	p.UpdateGoMod(true)
-	p.RegisterClasses(func(c *gopmod.Class) {
-		gengo.RegisterPkgFlags(c.ProjExt, gengo.PkgFlagGmx)
-		gengo.RegisterPkgFlags(c.WorkExt, gengo.PkgFlagSpx)
-		if c != gopmod.ClassSpx {
-			cl.RegisterClassFileType(c.ProjExt, c.WorkExt, c.PkgPaths...)
-		}
-	})
 }
 
 // -----------------------------------------------------------------------------
diff --git a/cmd/internal/run/run.go b/cmd/internal/run/run.go
index 3bc8a741..4b42da0a 100644
--- a/cmd/internal/run/run.go
+++ b/cmd/internal/run/run.go
@@ -28,7 +28,6 @@ import (
 	"github.com/goplus/gop/ast"
 	"github.com/goplus/gop/cl"
 	"github.com/goplus/gop/cmd/internal/base"
-	"github.com/goplus/gop/cmd/internal/modload"
 	"github.com/goplus/gop/parser"
 	"github.com/goplus/gop/scanner"
 	"github.com/goplus/gop/token"
@@ -39,7 +38,7 @@ import (
 
 // Cmd - gop run
 var Cmd = &base.Command{
-	UsageLine: "gop run [-asm -quiet -debug -nr -gop -prof] <gopSrcDir|gopSrcFile>",
+	UsageLine: "gop run [-asm -quiet -debug -nr -gop -prof] package [arguments...]",
 	Short:     "Run a Go+ program",
 }
 
@@ -114,7 +113,6 @@ func runCmd(cmd *base.Command, args []string) {
 	if isDir {
 		srcDir = src
 		gofile = src + "/gop_autogen.go"
-		modload.UpdateGoMod(srcDir)
 		isDirty = true // TODO: check if code changed
 		if isDirty {
 			pkgs, err = parser.ParseDir(fset, src, nil, parserMode)
diff --git a/go.mod b/go.mod
index 288983ea..b4d395ab 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,7 @@ module github.com/goplus/gop
 go 1.16
 
 require (
-	github.com/goplus/gox v1.9.1
+	github.com/goplus/gox v1.9.3
 	github.com/qiniu/x v1.11.5
 	golang.org/x/mod v0.5.1
 )
diff --git a/go.sum b/go.sum
index 5d3e59a0..1562aa0f 100644
--- a/go.sum
+++ b/go.sum
@@ -1,5 +1,5 @@
-github.com/goplus/gox v1.9.1 h1:vupKI4LRpGDuKPSZFaN1BjiEsKjzSsUEKiwssQ/3fBU=
-github.com/goplus/gox v1.9.1/go.mod h1:PznHkzl2HARBf7+s2reqcwKm1Z1a6Wae6EntQEh0iJI=
+github.com/goplus/gox v1.9.3 h1:34dCED5aYUardtwfxvP0Xyq1c5VDTonzFKklTo9qTHg=
+github.com/goplus/gox v1.9.3/go.mod h1:PznHkzl2HARBf7+s2reqcwKm1Z1a6Wae6EntQEh0iJI=
 github.com/qiniu/x v1.11.5 h1:TYr5cl4g2yoHAZeDK4MTjKF6CMoG+IHlCDvvM5qym6U=
 github.com/qiniu/x v1.11.5/go.mod h1:03Ni9tj+N2h2aKnAz+6N0Xfl8FwMEDRC2PAlxekASDs=
 github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
diff --git a/parser/interface.go b/parser/interface.go
index 408e621b..be5e4a8c 100644
--- a/parser/interface.go
+++ b/parser/interface.go
@@ -42,8 +42,6 @@ const (
 	DeclarationErrors
 	// AllErrors - report all errors (not just the first 10 on different lines)
 	AllErrors
-	// ParseGoFiles - parse *.go files
-	ParseGoFiles
 )
 
 // ParseFile parses the source code of a single Go source file and returns
diff --git a/parser/parser_gop.go b/parser/parser_gop.go
index ed3f0ec5..a8785391 100644
--- a/parser/parser_gop.go
+++ b/parser/parser_gop.go
@@ -24,7 +24,6 @@ import (
 	"os"
 	"path/filepath"
 	"strings"
-	"syscall"
 
 	"github.com/goplus/gop/ast"
 	"github.com/goplus/gop/token"
@@ -98,7 +97,19 @@ func astFileToPkg(file *ast.File, fileName string) (pkg *ast.Package) {
 // ParseDir calls ParseFSDir by passing a local filesystem.
 //
 func ParseDir(fset *token.FileSet, path string, filter func(os.FileInfo) bool, mode Mode) (pkgs map[string]*ast.Package, first error) {
-	return ParseFSDir(fset, local, path, filter, mode)
+	return ParseFSDir(fset, local, path, Config{Filter: filter, Mode: mode})
+}
+
+type Config struct {
+	IsClass func(ext string) (isProj bool, ok bool)
+	Filter  func(os.FileInfo) bool
+	Mode    Mode
+}
+
+// ParseDirEx calls ParseFSDir by passing a local filesystem.
+//
+func ParseDirEx(fset *token.FileSet, path string, conf Config) (pkgs map[string]*ast.Package, first error) {
+	return ParseFSDir(fset, local, path, conf)
 }
 
 // ParseFSDir calls ParseFile for all files with names ending in ".gop" in the
@@ -114,11 +125,14 @@ func ParseDir(fset *token.FileSet, path string, filter func(os.FileInfo) bool, m
 // returned. If a parse error occurred, a non-nil but incomplete map and the
 // first error encountered are returned.
 //
-func ParseFSDir(fset *token.FileSet, fs FileSystem, path string, filter func(os.FileInfo) bool, mode Mode) (pkgs map[string]*ast.Package, first error) {
+func ParseFSDir(fset *token.FileSet, fs FileSystem, path string, conf Config) (pkgs map[string]*ast.Package, first error) {
 	list, err := fs.ReadDir(path)
 	if err != nil {
 		return nil, err
 	}
+	if conf.IsClass == nil {
+		conf.IsClass = defaultIsClass
+	}
 	pkgs = make(map[string]*ast.Package)
 	for _, d := range list {
 		if d.IsDir() {
@@ -126,14 +140,23 @@ func ParseFSDir(fset *token.FileSet, fs FileSystem, path string, filter func(os.
 		}
 		fname := d.Name()
 		ext := filepath.Ext(fname)
-		ft, isOk := extGopFiles[ext]
-		if ft == ast.FileTypeGo && (mode&ParseGoFiles) == 0 {
-			isOk = false
+		var isProj, isClass bool
+		switch ext {
+		case ".gop":
+		case ".go":
+			if strings.HasPrefix(fname, "gop_autogen") {
+				continue
+			}
+		default:
+			if isProj, isClass = conf.IsClass(ext); !isClass {
+				continue
+			}
 		}
-		if isOk && !strings.HasPrefix(fname, "_") && (filter == nil || filter(d)) {
+		if !strings.HasPrefix(fname, "_") && (conf.Filter == nil || conf.Filter(d)) {
 			filename := fs.Join(path, fname)
 			if filedata, err := fs.ReadFile(filename); err == nil {
-				if src, err := ParseFSFile(fset, fs, filename, filedata, mode); err == nil {
+				if src, err := ParseFSFile(fset, fs, filename, filedata, conf.Mode); err == nil {
+					src.IsProj, src.IsClass = isProj, isClass
 					name := src.Name.Name
 					pkg, found := pkgs[name]
 					if !found {
@@ -155,25 +178,15 @@ func ParseFSDir(fset *token.FileSet, fs FileSystem, path string, filter func(os.
 	return
 }
 
-var (
-	extGopFiles = map[string]ast.FileType{
-		".go":  ast.FileTypeGo,
-		".gop": ast.FileTypeGop,
-		".spx": ast.FileTypeSpx,
-		".gmx": ast.FileTypeGmx,
+func defaultIsClass(ext string) (isProj bool, ok bool) {
+	switch ext {
+	case ".gmx":
+		isProj = true
+		fallthrough
+	case ".spx":
+		ok = true
 	}
-)
-
-// RegisterFileType registers a new Go+ class file type.
-func RegisterFileType(ext string, format ast.FileType) error {
-	if format != ast.FileTypeSpx && format != ast.FileTypeGmx {
-		return syscall.EINVAL
-	}
-	if _, ok := extGopFiles[ext]; ok {
-		return syscall.EEXIST
-	}
-	extGopFiles[ext] = format
-	return nil
+	return
 }
 
 // -----------------------------------------------------------------------------
@@ -209,11 +222,6 @@ func ParseFile(fset *token.FileSet, filename string, src interface{}, mode Mode)
 
 // ParseFSFile parses the source code of a single Go+ source file and returns the corresponding ast.File node.
 func ParseFSFile(fset *token.FileSet, fs FileSystem, filename string, src interface{}, mode Mode) (f *ast.File, err error) {
-	ext := filepath.Ext(filename)
-	ft, isOk := extGopFiles[ext]
-	if !isOk {
-		ft = ast.FileTypeGop
-	}
 	var code []byte
 	if src == nil {
 		code, err = fs.ReadFile(filename)
@@ -223,11 +231,7 @@ func ParseFSFile(fset *token.FileSet, fs FileSystem, filename string, src interf
 	if err != nil {
 		return
 	}
-	f, err = parseFile(fset, filename, code, mode)
-	if f != nil {
-		f.FileType = ft
-	}
-	return
+	return parseFile(fset, filename, code, mode)
 }
 
 var (
diff --git a/x/gengo/gengo.go b/x/gengo/gengo.go
index 8c158ff9..3b6c4916 100644
--- a/x/gengo/gengo.go
+++ b/x/gengo/gengo.go
@@ -20,17 +20,25 @@ import (
 	"bytes"
 	"crypto/md5"
 	"encoding/hex"
+	"errors"
 	"fmt"
 	"go/types"
+	"io"
+	"log"
 	"os"
 	"path/filepath"
 	"sort"
 	"strings"
+	"syscall"
 	"time"
 
+	"github.com/goplus/gop/ast"
+	"github.com/goplus/gop/cl"
+	"github.com/goplus/gop/parser"
 	"github.com/goplus/gop/token"
 	"github.com/goplus/gop/x/gopmod"
 	"github.com/goplus/gop/x/mod/modfetch"
+	"github.com/goplus/gox"
 	"github.com/goplus/gox/packages"
 )
 
@@ -52,6 +60,7 @@ type pkgInfo struct {
 	imports []string
 	deps    []*pkgInfo
 	hash    string
+	nsource int
 	flags   int
 }
 
@@ -65,57 +74,139 @@ type Runner struct {
 	state   int
 }
 
-func (p *Runner) GenGo(fset *token.FileSet, loaded map[string]*types.Package) (err error) {
-	if loaded == nil {
-		loaded = make(map[string]*types.Package)
+type Event interface {
+	OnStart(pkgPath string)
+	OnErr(stage string, err error)
+	OnInfo(format string, args ...interface{})
+	OnEnd()
+}
+
+type defaultEvent struct{}
+
+func (p defaultEvent) OnStart(pkgPath string) {
+	fmt.Fprintln(os.Stderr, pkgPath)
+}
+
+func (p defaultEvent) OnInfo(format string, args ...interface{}) {
+	fmt.Fprintf(os.Stderr, format, args...)
+}
+
+func (p defaultEvent) OnErr(stage string, err error) {
+	fmt.Fprintf(os.Stderr, "%s: %v\n", stage, err)
+}
+
+func (p defaultEvent) OnEnd() {
+}
+
+type Config struct {
+	Event
+	Fset   *token.FileSet
+	Loaded map[string]*types.Package
+}
+
+func (p *Runner) GenGoFlow() (pkgPaths []string, err error) {
+	pkgPaths = make([]string, 0, len(p.pkgs))
+	return p.genFlowPkgs(pkgPaths)
+}
+
+func (p *Runner) genFlowPkgs(pkgPaths []string) ([]string, error) {
+	if p.state != stateNormal {
+		if p.state != stateDone {
+			panic("TODO: cycle or error?")
+		}
+		return pkgPaths, nil
 	}
-	if fset == nil {
-		fset = token.NewFileSet()
+	p.state = stateProcessing
+	for _, pkg := range p.pkgs {
+		if pkg.flags == pkgFlagChanged {
+			for _, dep := range pkg.deps {
+				if r := dep.runner; r != p {
+					pkgPaths, _ = dep.runner.genFlowPkgs(pkgPaths)
+				}
+			}
+		}
 	}
-	var errs ErrorList
-	p.genGoPkgs(fset, loaded, &errs)
-	if len(errs) > 0 {
-		err = errs
+	for _, pkg := range p.pkgs {
+		if pkg.flags == pkgFlagChanged {
+			pkgPaths = p.genFlowPkg(pkgPaths, pkg)
+		}
 	}
-	return
+	p.state = stateDone
+	return pkgPaths, nil
+}
+
+func (p *Runner) genFlowPkg(pkgPaths []string, pkg *pkgInfo) []string {
+	for _, dep := range pkg.deps {
+		if r := dep.runner; r == p && dep.flags == pkgFlagChanged {
+			pkgPaths = r.genFlowPkg(pkgPaths, dep)
+		}
+	}
+	pkgPaths = append(pkgPaths, pkg.path)
+	pkg.flags = 0
+	return pkgPaths
+}
+
+func (p *Runner) GenGo(conf Config) bool {
+	if conf.Loaded == nil {
+		conf.Loaded = make(map[string]*types.Package)
+	}
+	if conf.Fset == nil {
+		conf.Fset = token.NewFileSet()
+	}
+	if conf.Event == nil {
+		conf.Event = defaultEvent{}
+	}
+	p.genGoPkgs(&conf)
+	return p.state == stateDone
 }
 
-func (p *Runner) genGoPkgs(fset *token.FileSet, loaded map[string]*types.Package, errs *ErrorList) {
+func (p *Runner) genGoPkgs(conf *Config) {
 	if p.state != stateNormal {
 		if p.state != stateDone {
-			panic("TODO: cycle?")
+			panic("TODO: cycle or error?")
 		}
 		return
 	}
 	p.state = stateProcessing
-	imports := make(map[string]none)
+	imports, changed := make(map[string]none), false
 	for _, pkg := range p.pkgs {
 		if pkg.flags == pkgFlagChanged {
+			changed = true
+			for _, dep := range pkg.deps {
+				if r := dep.runner; r != p {
+					r.genGoPkgs(conf)
+				}
+			}
 			for _, imp := range pkg.imports {
 				imports[imp] = none{}
 			}
 		}
 	}
+	if !changed {
+		return
+	}
 	imps := getKeys(imports)
-	conf := &packages.Config{
-		ModRoot:       p.modRoot,
-		ModPath:       p.modPath,
-		SupportedExts: p.mod.SupportedExts(),
-		Loaded:        loaded,
-		Fset:          fset,
-	}
-	imp, _, err := packages.NewImporter(conf, imps...)
+	impConf := &packages.Config{
+		ModRoot: p.modRoot,
+		ModPath: p.modPath,
+		Loaded:  conf.Loaded,
+		Fset:    conf.Fset,
+	}
+	imp, _, err := packages.NewImporter(impConf, imps...)
 	if err != nil {
-		*errs, p.state = append(*errs, err), stateOccurErrors
+		conf.OnErr("newImporter", err)
+		p.state = stateOccurErrors
 		return
 	}
-	_ = imp
-	for path, pkg := range p.pkgs {
+	conf.OnInfo("newImporter: %v\n", imps)
+	for _, pkg := range p.pkgs {
 		if pkg.flags == pkgFlagChanged {
-			p.genGoPkg(fset, path)
+			p.genGoPkg(pkg, imp, conf)
 		}
 	}
-	p.state = stateDone
+	if p.state == stateProcessing {
+		p.state = stateDone
+	}
 }
 
 func getKeys(v map[string]none) []string {
@@ -126,12 +217,126 @@ func getKeys(v map[string]none) []string {
 	return keys
 }
 
-func (p *Runner) genGoPkg(fset *token.FileSet, pkgPath string) {
-	/*
-		dir := p.modRoot + pkgPath[len(p.modPath):]
-		parser.ParseDir(fset, dir, nil)
-		cl.NewPackage(pkgPath)
-	*/
+const (
+	autoGenFile      = "gop_autogen.go"
+	autoGenTestFile  = "gop_autogen_test.go"
+	autoGen2TestFile = "gop_autogen2_test.go"
+)
+
+func (p *Runner) genGoPkg(pkg *pkgInfo, imp types.Importer, conf *Config) {
+	for _, dep := range pkg.deps {
+		if r := dep.runner; r == p && dep.flags == pkgFlagChanged {
+			r.genGoPkg(dep, imp, conf)
+		}
+	}
+	if err := p.doGenGoPkg(pkg, imp, conf); err != nil {
+		pkg.flags |= pkgFlagIll
+		p.state = stateOccurErrors
+		return
+	}
+	pkg.flags = 0
+}
+
+func (p *Runner) doGenGoPkg(pkgi *pkgInfo, imp types.Importer, conf *Config) (err error) {
+	pkgPath := pkgi.path
+	conf.OnStart(pkgPath)
+	pkgDir := p.modRoot + pkgPath[len(p.modPath):]
+
+	defer func() {
+		if e := recover(); e != nil {
+			switch v := e.(type) {
+			case error:
+				err = v
+			case string:
+				err = errors.New(v)
+			default:
+				panic(e)
+			}
+			conf.OnErr("genGoPkg", err)
+		}
+		conf.OnEnd()
+	}()
+
+	pkgs, err := parser.ParseDirEx(conf.Fset, pkgDir, parser.Config{
+		IsClass: p.mod.IsClass,
+		Mode:    parser.ParseComments,
+	})
+	if err != nil {
+		conf.OnErr("parse", err)
+		return
+	}
+	if len(pkgs) == 0 {
+		panic("TODO: no source in directory")
+	}
+
+	var pkgTest *ast.Package
+	clConf := &cl.Config{
+		WorkingDir:  pkgDir,
+		Fset:        conf.Fset,
+		LookupClass: p.mod.LookupClass,
+		Importer:    imp,
+	}
+	for name, pkg := range pkgs {
+		if strings.HasSuffix(name, "_test") {
+			if pkgTest != nil {
+				panic("TODO: has multi test package?")
+			}
+			pkgTest = pkg
+			continue
+		}
+		out, e := cl.NewPackage("", pkg, clConf)
+		if e != nil {
+			conf.OnErr("compile", e)
+			return e
+		}
+		err = saveGoFile(pkgDir, out, pkgi)
+		if err != nil {
+			conf.OnErr("compile", err)
+			return
+		}
+	}
+	if pkgTest != nil {
+		out, e := cl.NewPackage("", pkgTest, clConf)
+		if e != nil {
+			conf.OnErr("compile", e)
+			return e
+		}
+		err = gox.WriteFile(filepath.Join(pkgDir, autoGen2TestFile), out, true)
+		if err != nil {
+			conf.OnErr("compile", err)
+		}
+	}
+	return
+}
+
+func saveGoFile(dir string, pkg *gox.Package, pkgi *pkgInfo) error {
+	os.MkdirAll(dir, 0755)
+	err := func() (err error) {
+		file := filepath.Join(dir, autoGenFile)
+		f, err := os.Create(file)
+		if err != nil {
+			return
+		}
+		err = syscall.EFAULT
+		defer func() {
+			f.Close()
+			if err != nil {
+				os.Remove(file)
+			}
+		}()
+		err = pkgi.writeCinfo(f)
+		if err != nil {
+			return
+		}
+		return gox.WriteTo(f, pkg, false)
+	}()
+	if err != nil {
+		return err
+	}
+	if pkg.HasTestingFile() {
+		return gox.WriteFile(filepath.Join(dir, autoGenTestFile), pkg, true)
+	}
+	return nil
 }
 
 // -----------------------------------------------------------------------------
@@ -148,30 +353,24 @@ func modTime(file string, errs *ErrorList) (mt time.Time) {
 }
 
 func New(mod *gopmod.Module, pattern ...string) (p *Runner, err error) {
-	modFile := mod.Modfile()
-	modRoot, _ := filepath.Split(modFile)
-	modPath := mod.Path()
-	conf := &packages.Config{
-		ModRoot:       modRoot,
-		ModPath:       modPath,
-		SupportedExts: mod.SupportedExts(),
-	}
-	pkgPaths, err := packages.List(conf, pattern...)
+	pkgPaths := make(map[string]struct{})
+	err = mod.List(pkgPaths, pattern...)
 	if err != nil {
 		return
 	}
-	modRoot = modRoot[:len(modRoot)-1] // remove the last pathSeparator
+	modFile := mod.Modfile()
+	modRoot := filepath.Dir(modFile)
 
 	var errs ErrorList
 	p = &Runner{
 		mod:     mod,
 		modRoot: modRoot,
-		modPath: modPath,
+		modPath: mod.Path(),
 		modTime: modTime(modFile, &errs),
 		pkgs:    make(map[string]*pkgInfo),
 	}
 	p.runners = map[string]*Runner{modRoot: p}
-	for _, pkgPath := range pkgPaths {
+	for pkgPath := range pkgPaths {
 		p.genDeps(pkgPath, pkgPath, &errs)
 	}
 	if len(errs) > 0 {
@@ -192,6 +391,9 @@ func (p *Runner) genDeps(pkgPath, pkgPathBase string, errs *ErrorList) (pkg *pkg
 	default:
 		panic("TODO: invalid pkgPath")
 	}
+	if v, ok := p.pkgs[pkgPath]; ok {
+		return v
+	}
 	pkg = &pkgInfo{
 		path:   pkgPath,
 		runner: p,
@@ -209,13 +411,13 @@ func (p *Runner) genDeps(pkgPath, pkgPathBase string, errs *ErrorList) (pkg *pkg
 	}
 
 	var buf bytes.Buffer
-	var depsHash string
-	imps, err := p.mod.Imports(dir, false)
+	imports, err := p.getImports(dir)
 	if err != nil {
-		*errs, pkg.flags = append(*errs, err), pkgFlagIll
+		errs.addError("import "+pkgPath, err)
+		pkg.flags = pkgFlagIll
 		return
 	}
-	sort.Strings(imps)
+	imps := getSortedKeys(imports)
 	for _, imp := range imps {
 		if t := p.genDeps(imp, pkgPath, errs); t != nil {
 			buf.WriteString(t.runner.modRoot)
@@ -230,12 +432,42 @@ func (p *Runner) genDeps(pkgPath, pkgPathBase string, errs *ErrorList) (pkg *pkg
 	}
 	pkg.imports = imps
 	pkg.hash = hashOf(buf.Bytes())
-	if p.sourceChanged(dir, ci, &depsHash) || pkg.hash != depsHash {
+	pkg.nsource = ci.SourceNum
+	if p.sourceChanged(dir, ci.ModTime, pkg) {
 		pkg.flags |= pkgFlagChanged
 	}
 	return
 }
 
+func (p *Runner) getImports(dir string) (ret map[string]struct{}, err error) {
+	pkgs, err := p.mod.DirImports(dir)
+	if err != nil {
+		return
+	}
+	exists := false
+	for name, imps := range pkgs {
+		if !strings.HasSuffix(name, "_test") {
+			if exists {
+				return nil, errors.New("multiple packages in a directory")
+			}
+			ret, exists = imps, true
+		}
+	}
+	if exists {
+		return
+	}
+	return nil, syscall.ENOENT
+}
+
+func getSortedKeys(v map[string]none) []string {
+	keys := make([]string, 0, len(v))
+	for key := range v {
+		keys = append(keys, key)
+	}
+	sort.Strings(keys)
+	return keys
+}
+
 func hashOf(b []byte) string {
 	h := md5.Sum(b)
 	return hex.EncodeToString(h[:])
@@ -244,7 +476,7 @@ func hashOf(b []byte) string {
 func (p *Runner) genExternDeps(pkgPath string, errs *ErrorList) *pkgInfo {
 	_, modVer, ok := p.mod.LookupExternPkg(pkgPath)
 	if !ok {
-		panic("TODO: externPkg not found")
+		log.Panicln("externPkg not found:", pkgPath)
 	}
 	modRoot, err := modfetch.ModCachePath(modVer)
 	if err != nil {
@@ -261,7 +493,7 @@ func (p *Runner) genExternDeps(pkgPath string, errs *ErrorList) *pkgInfo {
 		exr = &Runner{
 			mod:     mod,
 			modRoot: modRoot,
-			modPath: modVer.Path,
+			modPath: mod.Path(),
 			modTime: modTime(mod.Modfile(), errs),
 			pkgs:    make(map[string]*pkgInfo),
 			runners: p.runners,
@@ -271,27 +503,37 @@ func (p *Runner) genExternDeps(pkgPath string, errs *ErrorList) *pkgInfo {
 	return exr.genDeps(pkgPath, pkgPath, errs)
 }
 
-func (p *Runner) sourceChanged(dir string, ci gopmod.ChangeInfo, depsHash *string) bool {
+func (p *Runner) sourceChanged(dir string, modt time.Time, pkg *pkgInfo) bool {
 	f, err := os.Open(dir + "/gop_autogen.go")
 	if err != nil {
 		return true
 	}
 	fi, err := f.Stat()
-	if err != nil || ci.ModTime.After(fi.ModTime()) {
+	if err != nil || modt.After(fi.ModTime()) {
 		return true
 	}
 	//cinfo $SourceNum $DepsHash
 	var sourceNum int
-	if _, err = fmt.Fscanf(f, "//cinfo %d %s\n", &sourceNum, depsHash); err != nil {
+	var hash string
+	if _, err = fmt.Fscanf(f, "//cinfo %d %s\n", &sourceNum, &hash); err != nil {
 		return true
 	}
-	return sourceNum != ci.SourceNum
+	return pkg.nsource != sourceNum || pkg.hash != hash
+}
+
+func (p pkgInfo) writeCinfo(w io.Writer) error {
+	_, err := fmt.Fprintf(w, "//cinfo %d %s\n", p.nsource, p.hash)
+	return err
 }
 
 // -----------------------------------------------------------------------------
 
 type ErrorList []error
 
+func (p *ErrorList) addError(stage string, err error) {
+	*p = append(*p, fmt.Errorf("%s: %v", stage, err))
+}
+
 func (e ErrorList) Error() string {
 	errStrs := make([]string, len(e))
 	for i, err := range e {
@@ -301,3 +543,38 @@ func (e ErrorList) Error() string {
 }
 
 // -----------------------------------------------------------------------------
+
+func GenGo(conf Config, dontRun bool, pattern ...string) bool {
+	if conf.Event == nil {
+		conf.Event = defaultEvent{}
+	}
+	mod, err := gopmod.Load(".")
+	if err != nil {
+		conf.OnErr("loadMod", err)
+		return false
+	}
+	err = mod.RegisterClasses()
+	if err != nil {
+		conf.OnErr("registerClass", err)
+		return false
+	}
+	p, err := New(mod, pattern...)
+	if err != nil {
+		conf.OnErr("genDeps", err)
+		return false
+	}
+	if dontRun {
+		pkgPaths, err := p.GenGoFlow()
+		if err != nil {
+			conf.OnErr("genGo", err)
+			return false
+		}
+		for _, pkgPath := range pkgPaths {
+			fmt.Fprintln(os.Stderr, pkgPath)
+		}
+		return true
+	}
+	return p.GenGo(conf)
+}
+
+// -----------------------------------------------------------------------------
diff --git a/x/gopmod/classfile.go b/x/gopmod/classfile.go
index ecbb13fb..461b961f 100644
--- a/x/gopmod/classfile.go
+++ b/x/gopmod/classfile.go
@@ -42,6 +42,14 @@ var (
 
 // -----------------------------------------------------------------------------
 
+func (p *Module) IsClass(ext string) (isProj bool, ok bool) {
+	c, ok := p.classes[ext]
+	if ok {
+		isProj = (ext == c.ProjExt)
+	}
+	return
+}
+
 func (p *Module) LookupClass(ext string) (c *Class, ok bool) {
 	c, ok = p.classes[ext]
 	return
@@ -65,7 +73,7 @@ func (p *Module) RegisterClasses(registerClass ...func(c *Class)) (err error) {
 }
 
 func (p *Module) registerMod(modPath string, regcls func(c *Class)) (err error) {
-	mod, ok := p.LookupMod(modPath)
+	mod, ok := p.LookupDepMod(modPath)
 	if !ok {
 		return syscall.ENOENT
 	}
diff --git a/x/gopmod/godeps.go b/x/gopmod/godeps.go
index 1305fc71..e6569aff 100644
--- a/x/gopmod/godeps.go
+++ b/x/gopmod/godeps.go
@@ -30,12 +30,13 @@ import (
 
 type none = struct{}
 
-func (p *Module) parseGoImport(errs *ErrorList, imports map[string]none, gopfile string) {
+func (p *Module) parseGoImport(errs *ErrorList, getImports getImportsFunc, gopfile string) {
 	f, err := parser.ParseFile(p.fset, gopfile, nil, parser.ImportsOnly)
 	if err != nil {
 		*errs = append(*errs, err)
 		return
 	}
+	imports := getImports(f.Name.Name)
 	for _, imp := range f.Imports {
 		p.importGo(imports, imp)
 	}
diff --git a/x/gopmod/gopdeps.go b/x/gopmod/gopdeps.go
index 21cdb55f..8cc63f50 100644
--- a/x/gopmod/gopdeps.go
+++ b/x/gopmod/gopdeps.go
@@ -28,15 +28,17 @@ import (
 
 // -----------------------------------------------------------------------------
 
-func (p *Module) parseGopImport(errs *ErrorList, imports map[string]none, gopfile string) {
+func (p *Module) parseGopImport(errs *ErrorList, getImports getImportsFunc, gopfile string) PkgImports {
 	f, err := parser.ParseFile(p.fset, gopfile, nil, parser.ImportsOnly)
 	if err != nil {
 		*errs = append(*errs, err)
-		return
+		return nil
 	}
+	imports := getImports(f.Name.Name)
 	for _, imp := range f.Imports {
 		p.importGop(imports, imp)
 	}
+	return imports
 }
 
 func (p *Module) importGop(imports map[string]none, spec *ast.ImportSpec) {
diff --git a/x/gopmod/module.go b/x/gopmod/module.go
index 8f526687..b40c76b0 100644
--- a/x/gopmod/module.go
+++ b/x/gopmod/module.go
@@ -17,7 +17,9 @@
 package gopmod
 
 import (
+	"fmt"
 	"os"
+	"path"
 	"path/filepath"
 	"sort"
 	"strings"
@@ -30,11 +32,8 @@ import (
 	"golang.org/x/mod/module"
 )
 
-func Imports(dir string) (imps []string, err error) {
-	var recursive bool
-	if strings.HasSuffix(dir, "/...") {
-		dir, recursive = dir[:len(dir)-4], true
-	}
+func Imports(dir string) (ret []string, err error) {
+	dir, recursive := parsePattern(dir)
 	mod, err := Load(dir)
 	if err != nil {
 		return
@@ -43,7 +42,38 @@ func Imports(dir string) (imps []string, err error) {
 	if err != nil {
 		return
 	}
-	return mod.Imports(dir, recursive)
+	imports := make(map[string]none)
+	err = mod.Imports(imports, dir, recursive)
+	if err != nil {
+		return
+	}
+	return getSortedKeys(imports), nil
+}
+
+func List(pattern ...string) (pkgPaths []string, err error) {
+	mod, err := Load(".")
+	if err != nil {
+		return
+	}
+	err = mod.RegisterClasses()
+	if err != nil {
+		return
+	}
+	ret := make(map[string]none)
+	err = mod.List(ret, pattern...)
+	if err != nil {
+		return
+	}
+	return getSortedKeys(ret), nil
+}
+
+func getSortedKeys(v map[string]none) []string {
+	keys := make([]string, 0, len(v))
+	for key := range v {
+		keys = append(keys, key)
+	}
+	sort.Strings(keys)
+	return keys
 }
 
 // -----------------------------------------------------------------------------
@@ -60,16 +90,6 @@ type Module struct {
 	fset    *token.FileSet
 }
 
-func (p *Module) SupportedExts() map[string]struct{} {
-	exts := make(map[string]none, len(p.classes)+2)
-	exts[".go"] = none{}
-	exts[".gop"] = none{}
-	for ext := range p.classes {
-		exts[ext] = none{}
-	}
-	return exts
-}
-
 // PkgType specifies a package type.
 type PkgType int
 
@@ -122,9 +142,9 @@ func (p *Module) LookupExternPkg(pkgPath string) (modPath string, modVer module.
 	return
 }
 
-// LookupMod lookups a depended module.
+// LookupDepMod lookups a depended module.
 // If modVer.Path is replace to be a local path, it will be canonical to an absolute path.
-func (p *Module) LookupMod(modPath string) (modVer module.Version, ok bool) {
+func (p *Module) LookupDepMod(modPath string) (modVer module.Version, ok bool) {
 	for _, m := range p.depmods {
 		if m.path == modPath {
 			modVer, ok = m.real, true
@@ -134,6 +154,18 @@ func (p *Module) LookupMod(modPath string) (modVer module.Version, ok bool) {
 	return
 }
 
+func (p *Module) IsGopMod() bool {
+	const gopPkgPath = "github.com/goplus/gop"
+	_, file := filepath.Split(p.Modfile())
+	if file == "gop.mod" {
+		return true
+	}
+	if _, ok := p.LookupDepMod(gopPkgPath); ok {
+		return true
+	}
+	return p.Path() == gopPkgPath
+}
+
 func getDepMods(mod modload.Module) []depmodInfo {
 	depmods := mod.DepMods()
 	ret := make([]depmodInfo, 0, len(depmods))
@@ -181,22 +213,149 @@ func loadModFrom(mod module.Version) (p *Module, err error) {
 	return Load(dir)
 }
 
-func (p *Module) Imports(dir string, recursive bool) (imps []string, err error) {
-	imports := make(map[string]none)
-	err = p.parseImports(imports, dir, recursive)
-	imps = getKeys(imports)
+func (p *Module) List(ret map[string]struct{}, pattern ...string) (err error) {
+	modRoot, modPath := p.Root(), p.Path()
+	for _, pat := range pattern {
+		if err = p.listPkgs(ret, pat, modRoot, modPath); err != nil {
+			return
+		}
+	}
 	return
 }
 
-func getKeys(v map[string]none) []string {
-	keys := make([]string, 0, len(v))
-	for key := range v {
-		keys = append(keys, key)
+func (p *Module) listPkgs(ret map[string]none, pat, modRoot, modPath string) error {
+	pat, recursive := parsePattern(pat)
+	if isLocal(pat) {
+		patAbs, err1 := filepath.Abs(pat)
+		patRel, err2 := filepath.Rel(modRoot, patAbs)
+		if err1 != nil || err2 != nil || strings.HasPrefix(patRel, "..") {
+			return fmt.Errorf("directory `%s` outside available modules", pat)
+		}
+		pkgPathBase := path.Join(modPath, filepath.ToSlash(patRel))
+		return p.doListLocalPkgs(ret, pkgPathBase, pat, recursive)
 	}
-	return keys
+	return p.doListPkgs(ret, pat, recursive)
+}
+
+func parsePattern(pat string) (string, bool) {
+	const multi = "/..."
+	recursive := strings.HasSuffix(pat, multi)
+	if recursive {
+		pat = pat[:len(pat)-len(multi)]
+		if pat == "" {
+			pat = "/"
+		}
+	}
+	return pat, recursive
 }
 
-func (p *Module) parseImports(imports map[string]none, dir string, recursive bool) (err error) {
+func isLocal(ns string) bool {
+	if len(ns) > 0 {
+		switch c := ns[0]; c {
+		case '/', '\\', '.':
+			return true
+		default:
+			return len(ns) >= 2 && ns[1] == ':' && ('A' <= c && c <= 'Z' || 'a' <= c && c <= 'z')
+		}
+	}
+	return false
+}
+
+func (p *Module) doListLocalPkgs(ret map[string]none, pkgPathBase, pat string, recursive bool) (err error) {
+	fis, err := os.ReadDir(pat)
+	if err != nil {
+		return
+	}
+	noSouceFile := true
+	for _, fi := range fis {
+		name := fi.Name()
+		if strings.HasPrefix(name, "_") { // skip this file/directory
+			continue
+		}
+		if fi.IsDir() {
+			if recursive {
+				err = p.doListLocalPkgs(ret, pkgPathBase+"/"+name, pat+"/"+name, true)
+				if err != nil {
+					return
+				}
+			}
+		} else if noSouceFile {
+			ext := path.Ext(name)
+			switch ext {
+			case ".gop", ".go":
+				noSouceFile = false
+			default:
+				if _, ok := p.classes[ext]; ok {
+					noSouceFile = false
+				}
+			}
+		}
+	}
+	if !noSouceFile {
+		ret[pkgPathBase] = none{}
+	}
+	return
+}
+
+func (p *Module) doListPkgs(ret map[string]none, pkgPath string, recursive bool) error {
+	switch p.PkgType(pkgPath) {
+	case PkgtStandard:
+		if !recursive {
+			ret[pkgPath] = none{}
+			return nil
+		}
+	case PkgtModule:
+		relDir := p.Root() + pkgPath[len(p.Path()):]
+		return p.doListLocalPkgs(ret, pkgPath, relDir, recursive)
+	}
+	modPath, modVer, ok := p.LookupExternPkg(pkgPath)
+	if !ok {
+		panic("TODO: external package not found")
+	}
+	mod, err := LoadMod(modVer)
+	if err != nil {
+		return err
+	}
+	relDir := mod.Root() + pkgPath[len(modPath):]
+	return mod.doListLocalPkgs(ret, pkgPath, relDir, recursive)
+}
+
+func (p *Module) Imports(imports map[string]struct{}, dir string, recursive bool) (err error) {
+	getImports := func(name string) PkgImports {
+		return imports
+	}
+	err = p.doImports(getImports, dir, false)
+	return
+}
+
+type PkgImports map[string]struct{}
+
+func (p *Module) DirImports(dir string) (pkgs map[string]PkgImports, err error) {
+	pkgs = make(map[string]PkgImports)
+	getImports := func(name string) PkgImports {
+		imps, ok := pkgs[name]
+		if !ok {
+			imps = make(PkgImports)
+			if p.IsGopMod() {
+				importGopBuiltins(imps)
+			}
+			pkgs[name] = imps
+		}
+		return imps
+	}
+	err = p.doImports(getImports, dir, false)
+	return
+}
+
+func importGopBuiltins(imports PkgImports) {
+	imports["strconv"] = none{}
+	imports["strings"] = none{}
+	imports["github.com/goplus/gop/builtin"] = none{}
+}
+
+type getImportsFunc func(name string) PkgImports
+
+func (p *Module) doImports(getImports getImportsFunc, dir string, recursive bool) (err error) {
 	list, err := os.ReadDir(dir)
 	if err != nil {
 		return
@@ -209,24 +368,28 @@ func (p *Module) parseImports(imports map[string]none, dir string, recursive boo
 		}
 		if d.IsDir() {
 			if recursive {
-				p.parseImports(imports, filepath.Join(dir, fname), true)
+				if err = p.doImports(getImports, filepath.Join(dir, fname), true); err != nil {
+					return
+				}
 			}
 			continue
 		}
 		ext := filepath.Ext(fname)
 		switch ext {
 		case ".gop":
-			p.parseGopImport(&errs, imports, filepath.Join(dir, fname))
+			p.parseGopImport(&errs, getImports, filepath.Join(dir, fname))
 		case ".go":
 			if !strings.HasPrefix(fname, "gop_autogen") {
-				p.parseGoImport(&errs, imports, filepath.Join(dir, fname))
+				p.parseGoImport(&errs, getImports, filepath.Join(dir, fname))
 			}
 		default:
 			if c, ok := p.classes[ext]; ok {
-				for _, pkgPath := range c.PkgPaths {
-					imports[pkgPath] = none{}
+				imports := p.parseGopImport(&errs, getImports, filepath.Join(dir, fname))
+				if imports != nil {
+					for _, pkgPath := range c.PkgPaths {
+						imports[pkgPath] = none{}
+					}
 				}
-				p.parseGopImport(&errs, imports, filepath.Join(dir, fname))
 			}
 		}
 	}
diff --git a/x/mod/modfile/rule.go b/x/mod/modfile/rule.go
index f1f2f092..ff1c2def 100644
--- a/x/mod/modfile/rule.go
+++ b/x/mod/modfile/rule.go
@@ -19,6 +19,7 @@ package modfile
 import (
 	"errors"
 	"fmt"
+	"path/filepath"
 	"strconv"
 	"strings"
 
@@ -128,6 +129,15 @@ func parseToFile(file string, data []byte, fix VersionFixer, strict bool) (parse
 }
 
 func (f *File) parseVerb(errs *ErrorList, verb string, line *Line, args []string, strict bool) {
+	wrapModPathError := func(modPath string, err error) {
+		*errs = append(*errs, Error{
+			Filename: f.Syntax.Name,
+			Pos:      line.Start,
+			ModPath:  modPath,
+			Verb:     verb,
+			Err:      err,
+		})
+	}
 	wrapError := func(err error) {
 		*errs = append(*errs, Error{
 			Filename: f.Syntax.Name,
@@ -139,7 +149,70 @@ func (f *File) parseVerb(errs *ErrorList, verb string, line *Line, args []string
 		wrapError(fmt.Errorf(format, args...))
 	}
 	switch verb {
-	case "require", "replace", "exclude", "module", "go", "retract":
+	case "require", "exclude", "module", "go", "retract":
+	case "replace":
+		arrow := 2
+		if len(args) >= 2 && args[1] == "=>" {
+			arrow = 1
+		}
+		if len(args) < arrow+2 || len(args) > arrow+3 || args[arrow] != "=>" {
+			errorf("usage: %s module/path [v1.2.3] => other/module v1.4\n\t or %s module/path [v1.2.3] => ../local/directory", verb, verb)
+			return
+		}
+		s, err := parseString(&args[0])
+		if err != nil {
+			errorf("invalid quoted string: %v", err)
+			return
+		}
+		pathMajor, err := modulePathMajor(s)
+		if err != nil {
+			wrapModPathError(s, err)
+			return
+		}
+		var v string
+		if arrow == 2 {
+			v, err = parseVersion(verb, s, &args[1])
+			if err != nil {
+				wrapError(err)
+				return
+			}
+			if err := module.CheckPathMajor(v, pathMajor); err != nil {
+				wrapModPathError(s, err)
+				return
+			}
+		}
+		ns, err := parseString(&args[arrow+1])
+		if err != nil {
+			errorf("invalid quoted string: %v", err)
+			return
+		}
+		nv := ""
+		if len(args) == arrow+2 {
+			if !IsDirectoryPath(ns) {
+				errorf("replacement module without version must be directory path (rooted or starting with ./ or ../)")
+				return
+			}
+			if filepath.Separator == '/' && strings.Contains(ns, `\`) {
+				errorf("replacement directory appears to be Windows path (on a non-windows system)")
+				return
+			}
+		}
+		if len(args) == arrow+3 {
+			nv, err = parseVersion(verb, ns, &args[arrow+2])
+			if err != nil {
+				wrapError(err)
+				return
+			}
+			if IsDirectoryPath(ns) {
+				errorf("replacement module directory path %q cannot have version", ns)
+				return
+			}
+		}
+		f.Replace = append(f.Replace, &Replace{
+			Old:    module.Version{Path: s, Version: v},
+			New:    module.Version{Path: ns, Version: nv},
+			Syntax: line,
+		})
 	case "gop":
 		if f.Gop != nil {
 			errorf("repeated go statement")
@@ -207,6 +280,42 @@ func (f *File) parseVerb(errs *ErrorList, verb string, line *Line, args []string
 	}
 }
 
+func parseVersion(verb string, path string, s *string) (string, error) {
+	t, err := parseString(s)
+	if err != nil {
+		return "", &Error{
+			Verb:    verb,
+			ModPath: path,
+			Err: &module.InvalidVersionError{
+				Version: *s,
+				Err:     err,
+			},
+		}
+	}
+	cv := module.CanonicalVersion(t)
+	if cv == "" {
+		return "", &Error{
+			Verb:    verb,
+			ModPath: path,
+			Err: &module.InvalidVersionError{
+				Version: t,
+				Err:     errors.New("must be of the form v1.2.3"),
+			},
+		}
+	}
+	t = cv
+	*s = t
+	return *s, nil
+}
+
+func modulePathMajor(path string) (string, error) {
+	_, major, ok := module.SplitPathVersion(path)
+	if !ok {
+		return "", fmt.Errorf("invalid module path")
+	}
+	return major, nil
+}
+
 // IsDirectoryPath reports whether the given path should be interpreted
 // as a directory path. Just like on the go command line, relative paths
 // and rooted paths are directory paths; the rest are module paths.
@@ -214,18 +323,6 @@ func IsDirectoryPath(ns string) bool {
 	return modfile.IsDirectoryPath(ns)
 }
 
-func IsLocal(ns string) bool {
-	if len(ns) > 0 {
-		switch c := ns[0]; c {
-		case '/', '\\', '.':
-			return true
-		default:
-			return len(ns) >= 2 && ns[1] == ':' && ('A' <= c && c <= 'Z' || 'a' <= c && c <= 'z')
-		}
-	}
-	return false
-}
-
 // MustQuote reports whether s must be quoted in order to appear as
 // a single token in a gop.mod line.
 func MustQuote(s string) bool {
diff --git a/x/mod/modload/module.go b/x/mod/modload/module.go
index eeeac15c..a207497b 100644
--- a/x/mod/modload/module.go
+++ b/x/mod/modload/module.go
@@ -46,6 +46,11 @@ func (p Module) Modfile() string {
 	return p.Syntax.Name
 }
 
+// Root returns absolute root path of this module.
+func (p Module) Root() string {
+	return filepath.Dir(p.Syntax.Name)
+}
+
 // Path returns the module path.
 func (p Module) Path() string {
 	if mod := p.Module; mod != nil {


Test patch:
diff --git a/cl/builtin_test.go b/cl/builtin_test.go
index ede759c1..1fe44902 100644
--- a/cl/builtin_test.go
+++ b/cl/builtin_test.go
@@ -22,6 +22,7 @@ import (
 
 	"github.com/goplus/gop/ast"
 	"github.com/goplus/gop/token"
+	"github.com/goplus/gop/x/gopmod"
 	"github.com/goplus/gox"
 	"github.com/goplus/gox/packages"
 )
@@ -107,7 +108,9 @@ func TestCanAutoCall(t *testing.T) {
 
 func TestGmxSettings(t *testing.T) {
 	pkg := gox.NewPackage("", "foo", goxConf)
-	gmx := newGmx(pkg, "main.t2gmx")
+	gmx := newGmx(pkg, "main.t2gmx", &Config{
+		LookupClass: lookupClass,
+	})
 	scheds := gmx.getScheds(pkg.CB())
 	if len(scheds) != 2 || scheds[0] == nil || scheds[0] != scheds[1] {
 		t.Fatal("TestGmxSettings failed")
@@ -127,17 +130,14 @@ func TestSpxLookup(t *testing.T) {
 	spxLookup(nil, "foo")
 }
 
-func TestRegisterClassFileType(t *testing.T) {
-	defer func() {
-		if e := recover(); e == nil {
-			t.Fatal("TestRegisterClassFileType: no error?")
-		}
-	}()
-	RegisterClassFileType(".t3gmx", ".t3spx")
-}
-
-func init() {
-	RegisterClassFileType(".t2gmx", ".t2spx", "github.com/goplus/gop/cl/internal/spx2")
+func lookupClass(ext string) (c *gopmod.Class, ok bool) {
+	switch ext {
+	case ".t2gmx", ".t2spx":
+		return &gopmod.Class{
+			ProjExt: ".t2gmx", WorkExt: ".t2spx",
+			PkgPaths: []string{"github.com/goplus/gop/cl/internal/spx2"}}, true
+	}
+	return
 }
 
 // -----------------------------------------------------------------------------
diff --git a/cl/compile_spx_test.go b/cl/compile_spx_test.go
index c9c86688..7c5c264f 100644
--- a/cl/compile_spx_test.go
+++ b/cl/compile_spx_test.go
@@ -26,6 +26,7 @@ import (
 	"github.com/goplus/gop/parser"
 	"github.com/goplus/gop/parser/parsertest"
 	"github.com/goplus/gop/scanner"
+	"github.com/goplus/gop/x/gopmod"
 	"github.com/goplus/gox"
 )
 
@@ -38,9 +39,30 @@ func newTwoFileFS(dir string, fname, data string, fname2 string, data2 string) *
 	})
 }
 
-func init() {
-	cl.RegisterClassFileType(".tgmx", ".tspx", "github.com/goplus/gop/cl/internal/spx", "math")
-	cl.RegisterClassFileType(".tgmx", ".tspx", "github.com/goplus/gop/cl/internal/spx", "math")
+func lookupClass(ext string) (c *gopmod.Class, ok bool) {
+	switch ext {
+	case ".tgmx", ".tspx":
+		return &gopmod.Class{
+			ProjExt: ".tgmx", WorkExt: ".tspx",
+			PkgPaths: []string{"github.com/goplus/gop/cl/internal/spx", "math"}}, true
+	case ".t2gmx", ".t2spx":
+		return &gopmod.Class{
+			ProjExt: ".t2gmx", WorkExt: ".t2spx",
+			PkgPaths: []string{"github.com/goplus/gop/cl/internal/spx2"}}, true
+	}
+	return
+}
+
+func spxParserConf() parser.Config {
+	return parser.Config{
+		IsClass: func(ext string) (isProj bool, ok bool) {
+			c, ok := lookupClass(ext)
+			if ok {
+				isProj = (c.ProjExt == ext)
+			}
+			return
+		},
+	}
 }
 
 func gopSpxTest(t *testing.T, gmx, spxcode, expected string) {
@@ -52,7 +74,7 @@ func gopSpxTestEx(t *testing.T, gmx, spxcode, expected, gmxfile, spxfile string)
 	defer cl.SetDisableRecover(false)
 
 	fs := newTwoFileFS("/foo", spxfile, spxcode, gmxfile, gmx)
-	pkgs, err := parser.ParseFSDir(gblFset, fs, "/foo", nil, 0)
+	pkgs, err := parser.ParseFSDir(gblFset, fs, "/foo", spxParserConf())
 	if err != nil {
 		scanner.PrintError(os.Stderr, err)
 		t.Fatal("ParseFSDir:", err)
@@ -75,7 +97,7 @@ func gopSpxTestEx(t *testing.T, gmx, spxcode, expected, gmxfile, spxfile string)
 
 func gopSpxErrorTestEx(t *testing.T, msg, gmx, spxcode, gmxfile, spxfile string) {
 	fs := newTwoFileFS("/foo", spxfile, spxcode, gmxfile, gmx)
-	pkgs, err := parser.ParseFSDir(gblFset, fs, "/foo", nil, 0)
+	pkgs, err := parser.ParseFSDir(gblFset, fs, "/foo", spxParserConf())
 	if err != nil {
 		scanner.PrintError(os.Stderr, err)
 		t.Fatal("ParseFSDir:", err)
diff --git a/cl/compile_test.go b/cl/compile_test.go
index 02777b6c..07ff352f 100644
--- a/cl/compile_test.go
+++ b/cl/compile_test.go
@@ -60,9 +60,10 @@ func init() {
 		panic(err)
 	}
 	gblConf = &cl.Config{
-		Fset:       gblFset,
-		Importer:   imp,
-		NoFileLine: true,
+		Fset:        gblFset,
+		Importer:    imp,
+		LookupClass: lookupClass,
+		NoFileLine:  true,
 	}
 }
 
@@ -75,7 +76,7 @@ func gopClTestEx(t *testing.T, pkgname, gopcode, expected string) {
 	defer cl.SetDisableRecover(false)
 
 	fs := parsertest.NewSingleFileFS("/foo", "bar.gop", gopcode)
-	pkgs, err := parser.ParseFSDir(gblFset, fs, "/foo", nil, parser.ParseComments)
+	pkgs, err := parser.ParseFSDir(gblFset, fs, "/foo", parser.Config{Mode: parser.ParseComments})
 	if err != nil {
 		scanner.PrintError(os.Stderr, err)
 		t.Fatal("ParseFSDir:", err)
diff --git a/cl/error_msg_test.go b/cl/error_msg_test.go
index 59569e71..07563b08 100644
--- a/cl/error_msg_test.go
+++ b/cl/error_msg_test.go
@@ -32,7 +32,7 @@ func codeErrorTest(t *testing.T, msg, src string) {
 
 func codeErrorTestEx(t *testing.T, pkgname, msg, src string) {
 	fs := parsertest.NewSingleFileFS("/foo", "bar.gop", src)
-	pkgs, err := parser.ParseFSDir(gblFset, fs, "/foo", nil, 0)
+	pkgs, err := parser.ParseFSDir(gblFset, fs, "/foo", parser.Config{})
 	if err != nil {
 		scanner.PrintError(os.Stderr, err)
 		t.Fatal("parser.ParseFSDir failed")
diff --git a/cmd/internal/test/test.go b/cmd/internal/test/test.go
index 469cc227..eef77a45 100644
--- a/cmd/internal/test/test.go
+++ b/cmd/internal/test/test.go
@@ -18,21 +18,19 @@
 package test
 
 import (
-	"fmt"
 	"os"
-	"strings"
 
 	"github.com/qiniu/x/log"
 
 	"github.com/goplus/gop/cl"
-	"github.com/goplus/gop/cmd/gengo"
 	"github.com/goplus/gop/cmd/internal/base"
+	"github.com/goplus/gop/x/gengo"
 	"github.com/goplus/gox"
 )
 
-// Cmd - gop install
+// gop test
 var Cmd = &base.Command{
-	UsageLine: "gop test [-v] <GopPackages>",
+	UsageLine: "gop test [-v] [packages]",
 	Short:     "Test Go+ packages",
 }
 
@@ -46,19 +44,14 @@ func init() {
 }
 
 func runCmd(_ *base.Command, args []string) {
-	err := flag.Parse(base.SkipSwitches(args, flag))
+	err := flag.Parse(args)
 	if err != nil {
 		log.Fatalln("parse input arguments failed:", err)
 	}
-	ssargs := flag.Args()
-	if len(ssargs) == 0 {
-		ssargs = []string{"."}
-	}
-	var recursive bool
-	var dir = ssargs[0]
-	if strings.HasSuffix(dir, "/...") {
-		dir = dir[:len(dir)-4]
-		recursive = true
+
+	pattern := flag.Args()
+	if len(pattern) == 0 {
+		pattern = []string{"."}
 	}
 
 	if *flagVerbose {
@@ -66,25 +59,11 @@ func runCmd(_ *base.Command, args []string) {
 		cl.SetDebug(cl.DbgFlagAll)
 		cl.SetDisableRecover(true)
 	}
-	hasError := false
-	runner := new(gengo.Runner)
-	runner.SetAfter(func(p *gengo.Runner, dir string, flags int) error {
-		errs := p.ResetErrors()
-		if errs != nil {
-			hasError = true
-			for _, err := range errs {
-				fmt.Fprintln(os.Stderr, err)
-			}
-			fmt.Fprintln(os.Stderr)
-		}
-		return nil
-	})
-	baseConf := &cl.Config{}
-	runner.GenGo(dir, recursive, baseConf)
-	if hasError {
+
+	if !gengo.GenGo(gengo.Config{}, false, pattern...) {
 		os.Exit(1)
 	}
-	base.RunGoCmd(dir, "test", args...)
+	base.RunGoCmd(".", "test", args...)
 }
 
 // -----------------------------------------------------------------------------
diff --git a/env/mod_test.go b/env/mod_test.go
index 6e39bd81..a6ffbd67 100644
--- a/env/mod_test.go
+++ b/env/mod_test.go
@@ -36,12 +36,12 @@ func TestGopMod(t *testing.T) {
 	if !strings.HasSuffix(file, "gop"+sep+"go.mod") {
 		t.Fatal("GOPMOD failed:", file)
 	}
-	file, err = GOPMOD("../cl/internal/spx")
+	file, err = GOPMOD("./modtest")
 	if err != nil {
-		t.Fatal("GOPMOD spx failed:", err)
+		t.Fatal("GOPMOD modtest failed:", err)
 	}
-	if !strings.HasSuffix(file, "cl"+sep+"internal"+sep+"gop.mod") {
-		t.Fatal("GOPMOD spx failed:", file)
+	if !strings.HasSuffix(filepath.ToSlash(file), "env/modtest/gop.mod") {
+		t.Fatal("GOPMOD modtest failed:", file)
 	}
 }
 
diff --git a/env/modtest/gop.mod b/env/modtest/gop.mod
new file mode 100644
index 00000000..b1656c25
--- /dev/null
+++ b/env/modtest/gop.mod
@@ -0,0 +1,4 @@
+package modtest
+
+go 1.6
+gop 1.0
diff --git a/parser/parserdir_test.go b/parser/parserdir_test.go
index a14b8aa7..4875d4e8 100644
--- a/parser/parserdir_test.go
+++ b/parser/parserdir_test.go
@@ -23,12 +23,10 @@ import (
 	"path"
 	"reflect"
 	"strings"
-	"syscall"
 	"testing"
 
 	"github.com/qiniu/x/log"
 
-	"github.com/goplus/gop/ast"
 	"github.com/goplus/gop/parser/parsertest"
 	"github.com/goplus/gop/scanner"
 	"github.com/goplus/gop/token"
@@ -91,7 +89,7 @@ func testFrom(t *testing.T, pkgDir, sel string, exclude Mode) {
 	}
 	log.Println("Parsing", pkgDir)
 	fset := token.NewFileSet()
-	pkgs, err := ParseDir(fset, pkgDir, nil, (Trace|ParseGoFiles|ParseComments)&^exclude)
+	pkgs, err := ParseDir(fset, pkgDir, nil, (Trace|ParseComments)&^exclude)
 	if err != nil || len(pkgs) != 1 {
 		if errs, ok := err.(scanner.ErrorList); ok {
 			for _, e := range errs {
@@ -112,11 +110,11 @@ func testFrom(t *testing.T, pkgDir, sel string, exclude Mode) {
 
 func TestParseGo(t *testing.T) {
 	fset := token.NewFileSet()
-	pkgs, err := ParseDir(fset, "./_testdata/functype", nil, Trace)
+	pkgs, err := ParseDirEx(fset, "./_testdata/functype", Config{Mode: Trace})
 	if err != nil {
 		t.Fatal("TestParseGo: ParseDir failed -", err)
 	}
-	if len(pkgs) != 0 {
+	if len(pkgs) != 1 {
 		t.Fatal("TestParseGo failed: len(pkgs) =", len(pkgs))
 	}
 }
@@ -154,14 +152,4 @@ func TestFromTestdata(t *testing.T) {
 	}
 }
 
-func TestRegisterFileType(t *testing.T) {
-	RegisterFileType(".gsh", ast.FileTypeSpx)
-	if err := RegisterFileType(".gshx", ast.FileTypeGop); err != syscall.EINVAL {
-		t.Fatal("TestRegisterFileType failed:", err)
-	}
-	if err := RegisterFileType(".gsh", ast.FileTypeGmx); err != syscall.EEXIST {
-		t.Fatal("TestRegisterFileType failed:", err)
-	}
-}
-
 // -----------------------------------------------------------------------------
diff --git a/x/mod/modfile/gop_test.go b/x/mod/modfile/gop_test.go
index 0701c0ea..12dc5269 100644
--- a/x/mod/modfile/gop_test.go
+++ b/x/mod/modfile/gop_test.go
@@ -17,6 +17,7 @@
 package modfile
 
 import (
+	"path/filepath"
 	"testing"
 
 	"golang.org/x/mod/modfile"
@@ -39,21 +40,6 @@ func TestGetWeight(t *testing.T) {
 	}
 }
 
-func TestIsLocal(t *testing.T) {
-	if !IsLocal(".") || !IsLocal("/") {
-		t.Fatal(`IsLocal(".") || IsLocal("/")`)
-	}
-	if !IsLocal("c:/foo") {
-		t.Fatal(`IsLocal("c:/foo")`)
-	}
-	if !IsLocal("C:/foo") {
-		t.Fatal(`IsLocal("C:/foo")`)
-	}
-	if IsLocal("") {
-		t.Fatal(`IsLocal("")`)
-	}
-}
-
 // -----------------------------------------------------------------------------
 
 const gopmodSpx = `
@@ -137,6 +123,11 @@ register github.com/goplus/spx
 require (
     github.com/goplus/spx v1.0
 )
+
+replace (
+	github.com/goplus/spx v1.0 => github.com/xushiwei/spx v1.2
+	github.com/goplus/gop => /Users/xushiwei/work/gop
+)
 `
 
 func TestParse2(t *testing.T) {
@@ -151,6 +142,9 @@ func TestParse2(t *testing.T) {
 	if f.Register[0].ClassfileMod != "github.com/goplus/spx" {
 		t.Fatal("Parse => Register:", f.Register)
 	}
+	if len(f.Replace) != 2 {
+		t.Fatal("Parse => Replace:", f.Replace)
+	}
 	f.DropAllRequire()
 	if f.Require != nil {
 		t.Fatal("DropAllRequire failed")
@@ -171,6 +165,42 @@ func TestParse2(t *testing.T) {
 }
 
 func TestParseErr(t *testing.T) {
+	doTestParseErr(t, `gop.mod:2: replace github.com/goplus/gop/v2: version "v1.2.0" invalid: should be v2, not v1`, `
+replace github.com/goplus/gop/v2 v1.2 => ../
+`)
+	if filepath.Separator == '/' {
+		doTestParseErr(t, `gop.mod:2:3: replacement directory appears to be Windows path (on a non-windows system)`, `
+		replace github.com/goplus/gop v1.2 => ..\
+		`)
+	}
+	doTestParseErr(t, `gop.mod:2: replacement module directory path "../" cannot have version`, `
+replace github.com/goplus/gop v1.2 => ../ v1.3
+`)
+	doTestParseErr(t, `gop.mod:2: replacement module without version must be directory path (rooted or starting with ./ or ../)`, `
+replace github.com/goplus/gop v1.2 => abc.com
+`)
+	doTestParseErr(t, `gop.mod:2: invalid quoted string: unquoted string cannot contain quote`, `
+replace github.com/goplus/gop v1.2 => /"
+`)
+	doTestParseErr(t, `gop.mod:2: replace github.com/goplus/gop: version "v1.2\"" invalid: unquoted string cannot contain quote`, `
+replace github.com/goplus/gop v1.2" => /
+`)
+	doTestParseErr(t, `gop.mod:2: invalid quoted string: unquoted string cannot contain quote`, `
+replace gopkg.in/" v1.2 => /
+`)
+	doTestParseErr(t, `gop.mod:2: replace gopkg.in/?: invalid module path`, `
+replace gopkg.in/? v1.2 => /
+`)
+	doTestParseErr(t, `gop.mod:2: replace /: version "?" invalid: must be of the form v1.2.3`, `
+replace github.com/goplus/gop => / ?
+`)
+	doTestParseErr(t, `gop.mod:2: replace github.com/goplus/gop: version "?" invalid: must be of the form v1.2.3`, `
+replace github.com/goplus/gop ? => /
+`)
+	doTestParseErr(t, `gop.mod:2: usage: replace module/path [v1.2.3] => other/module v1.4
+	 or replace module/path [v1.2.3] => ../local/directory`, `
+replace ?
+`)
 	doTestParseErr(t, `gop.mod:3: repeated go statement`, `
 gop 1.1
 gop 1.2