Repository: goplus/gop
Pull request #1082 (2022-01-04 22:43:07)

Revert "Revert "remove PkgsLoader cache""
Reverts goplus/gop#1077


Patch:
diff --git a/cl/packages.go b/cl/_packages.go
similarity index 51%
rename from cl/packages.go
rename to cl/_packages.go
index 9b45551c..a4dbd82b 100644
--- a/cl/packages.go
+++ b/cl/_packages.go
@@ -17,17 +17,13 @@
 package cl
 
 import (
-	"fmt"
 	"io/ioutil"
-	"log"
-	"os"
 	"path/filepath"
 	"strings"
 	"syscall"
 
 	"github.com/goplus/gop/env"
 	"github.com/goplus/gop/x/mod/modfile"
-	"github.com/goplus/gox"
 	"golang.org/x/tools/go/packages"
 )
 
@@ -48,46 +44,15 @@ func GetModulePath(file string) (pkgPath string, err error) {
 // -----------------------------------------------------------------------------
 
 type PkgsLoader struct {
-	cached     *gox.LoadPkgsCached
 	genGoPkg   func(pkgDir string, base *Config) error
-	LoadPkgs   gox.LoadPkgsFunc
 	BaseConfig *Config
 }
 
 func initPkgsLoader(base *Config) {
-	if base.ModRootDir == "" {
-		modfile, err := env.GOPMOD(base.Dir)
-		if err != nil {
-			log.Panicln("gop.mod not found:", err)
-		}
-		base.ModRootDir, _ = filepath.Split(modfile)
-	}
 	p := &PkgsLoader{genGoPkg: base.GenGoPkg, BaseConfig: base}
-	if base.PersistLoadPkgs {
-		if base.CacheFile == "" && base.ModRootDir != "" {
-			dir := base.ModRootDir + "/.gop"
-			os.MkdirAll(dir, 0755)
-			base.CacheFile = dir + "/gop.cache"
-		}
-	}
-	if base.CacheFile != "" {
-		p.cached = gox.OpenLoadPkgsCached(base.CacheFile, p.Load)
-		p.LoadPkgs = p.cached.Load
-	} else if base.CacheLoadPkgs {
-		p.LoadPkgs = gox.NewLoadPkgsCached(p.Load)
-	} else {
-		p.LoadPkgs = p.loadPkgsNoCache
-	}
 	base.PkgsLoader = p
 }
 
-func (p *PkgsLoader) Save() error {
-	if p.cached == nil {
-		return nil
-	}
-	return p.cached.Save()
-}
-
 func (p *PkgsLoader) GenGoPkgs(cfg *packages.Config, notFounds []string) (err error) {
 	if p.genGoPkg == nil {
 		return syscall.ENOENT
@@ -114,48 +79,4 @@ func (p *PkgsLoader) GenGoPkgs(cfg *packages.Config, notFounds []string) (err er
 	return nil
 }
 
-func (p *PkgsLoader) Load(cfg *packages.Config, patterns ...string) ([]*packages.Package, error) {
-	var nretry int
-retry:
-	loadPkgs, err := packages.Load(cfg, patterns...)
-	if err == nil && p.genGoPkg != nil {
-		var notFounds []string
-		packages.Visit(loadPkgs, nil, func(pkg *packages.Package) {
-			const goGetCmd = "go get "
-			for _, err := range pkg.Errors {
-				if pos := strings.LastIndex(err.Msg, goGetCmd); pos > 0 {
-					notFounds = append(notFounds, err.Msg[pos+len(goGetCmd):])
-				}
-			}
-		})
-		if notFounds != nil {
-			if nretry > 1 {
-				log.Println("Load packages too many times:", notFounds)
-				return loadPkgs, err
-			}
-			if e := p.GenGoPkgs(cfg, notFounds); e == nil {
-				nretry++
-				goto retry
-			}
-		}
-	}
-	return loadPkgs, err
-}
-
-func (p *PkgsLoader) loadPkgsNoCache(at *gox.Package, imports map[string]*gox.PkgRef, pkgPaths ...string) int {
-	conf := at.InternalGetLoadConfig()
-	loadPkgs, err := p.Load(conf, pkgPaths...)
-	if err != nil {
-		fmt.Fprintln(os.Stderr, err)
-		return 1
-	}
-	if n := packages.PrintErrors(loadPkgs); n > 0 {
-		return n
-	}
-	for _, loadPkg := range loadPkgs {
-		gox.LoadGoPkg(at, imports, loadPkg)
-	}
-	return 0
-}
-
 // -----------------------------------------------------------------------------
diff --git a/cl/classfile.go b/cl/classfile.go
index 15e3646f..6d4cd590 100644
--- a/cl/classfile.go
+++ b/cl/classfile.go
@@ -22,6 +22,7 @@ import (
 	"go/types"
 	"path/filepath"
 	"strings"
+	"syscall"
 
 	"github.com/goplus/gop/ast"
 	"github.com/goplus/gop/parser"
@@ -43,7 +44,7 @@ var (
 )
 
 // RegisterClassFileType registers Go+ class file types.
-func RegisterClassFileType(extGmx, extSpx string, pkgPaths ...string) {
+func RegisterClassFileType(extGmx, extSpx string, pkgPaths ...string) error {
 	if pkgPaths == nil {
 		panic("RegisterClassFileType: no pkgPath specified")
 	}
@@ -51,9 +52,11 @@ func RegisterClassFileType(extGmx, extSpx string, pkgPaths ...string) {
 	if extSpx != "" {
 		parser.RegisterFileType(extSpx, ast.FileTypeSpx)
 	}
-	if _, ok := gmxTypes[extGmx]; !ok {
-		gmxTypes[extGmx] = gmxInfo{extSpx, pkgPaths}
+	if _, ok := gmxTypes[extGmx]; ok {
+		return syscall.EEXIST
 	}
+	gmxTypes[extGmx] = gmxInfo{extSpx, pkgPaths}
+	return nil
 }
 
 // -----------------------------------------------------------------------------
diff --git a/cl/compile.go b/cl/compile.go
index 7925f9eb..51f3cf98 100644
--- a/cl/compile.go
+++ b/cl/compile.go
@@ -18,7 +18,6 @@
 package cl
 
 import (
-	"context"
 	"errors"
 	"fmt"
 	"go/types"
@@ -73,64 +72,18 @@ func (p *Errors) Error() string {
 
 // Config of loading Go+ packages.
 type Config struct {
-	// Context specifies the context for the load operation.
-	// If the context is cancelled, the loader may stop early
-	// and return an ErrCancelled error.
-	// If Context is nil, the load cannot be cancelled.
-	Context context.Context
-
-	// Logf is the logger for the config.
-	// If the user provides a logger, debug logging is enabled.
-	// If the GOPACKAGESDEBUG environment variable is set to true,
-	// but the logger is nil, default to log.Printf.
-	Logf func(format string, args ...interface{})
-
-	// Dir is the directory in which to run the build system's query tool
-	// that provides information about the packages.
-	// If Dir is empty, the tool is run in the current directory.
-	Dir string
-
 	// WorkingDir is the directory in which to run gop compiler.
+	WorkingDir string
+
 	// TargetDir is the directory in which to generate Go files.
-	// If WorkingDir or TargetDir is empty, it is same as Dir.
-	WorkingDir, TargetDir string
-
-	// ModRootDir specifies root dir of this module.
-	// If ModRootDir is empty, will lookup gop.mod in all ancestor directories of Dir.
-	ModRootDir string
-
-	// CacheFile specifies where cache data to write.
-	CacheFile string
-
-	// Env is the environment to use when invoking the build system's query tool.
-	// If Env is nil, the current environment is used.
-	// As in os/exec's Cmd, only the last value in the slice for
-	// each environment key is used. To specify the setting of only
-	// a few variables, append to the current environment, as in:
-	//
-	//	opt.Env = append(os.Environ(), "GOOS=plan9", "GOARCH=386")
-	//
-	Env []string
-
-	// BuildFlags is a list of command-line flags to be passed through to
-	// the build system's query tool.
-	BuildFlags []string
+	TargetDir string
 
 	// Fset provides source position information for syntax trees and types.
 	// If Fset is nil, Load will use a new fileset, but preserve Fset's value.
 	Fset *token.FileSet
 
-	// GenGoPkg is called to convert a Go+ package into Go.
-	GenGoPkg func(pkgDir string, base *Config) error
-
-	// PkgsLoader is the Go+ packages loader (will be set if it is nil).
-	PkgsLoader *PkgsLoader
-
-	// CacheLoadPkgs = true means to cache all loaded packages.
-	CacheLoadPkgs bool
-
-	// PersistLoadPkgs = true means to cache all loaded packages to disk.
-	PersistLoadPkgs bool
+	// An Importer resolves import paths to Packages.
+	Importer types.Importer
 
 	// NoFileLine = true means not to generate file line comments.
 	NoFileLine bool
@@ -139,16 +92,6 @@ type Config struct {
 	RelativePath bool
 }
 
-func (conf *Config) Ensure() *Config {
-	if conf == nil {
-		conf = &Config{Fset: token.NewFileSet()}
-	}
-	if conf.PkgsLoader == nil {
-		initPkgsLoader(conf)
-	}
-	return conf
-}
-
 type nodeInterp struct {
 	fset       *token.FileSet
 	files      map[string]*ast.File
@@ -400,34 +343,22 @@ func (p *pkgCtx) handleRecover(e interface{}) {
 
 // NewPackage creates a Go+ package instance.
 func NewPackage(pkgPath string, pkg *ast.Package, conf *Config) (p *gox.Package, err error) {
-	conf = conf.Ensure()
-	dir := conf.Dir
-	if dir == "" {
-		dir, _ = os.Getwd()
-	}
 	workingDir := conf.WorkingDir
 	if workingDir == "" {
 		workingDir, _ = os.Getwd()
 	}
 	targetDir := conf.TargetDir
 	if targetDir == "" {
-		targetDir = dir
+		targetDir = workingDir
 	}
 	interp := &nodeInterp{fset: conf.Fset, files: pkg.Files, workingDir: workingDir}
 	ctx := &pkgCtx{syms: make(map[string]loader), nodeInterp: interp}
 	confGox := &gox.Config{
-		Context:         conf.Context,
-		Logf:            conf.Logf,
-		Dir:             dir,
-		ModRootDir:      conf.ModRootDir,
-		Env:             conf.Env,
-		BuildFlags:      conf.BuildFlags,
 		Fset:            conf.Fset,
-		LoadPkgs:        conf.PkgsLoader.LoadPkgs,
+		Importer:        conf.Importer,
 		LoadNamed:       ctx.loadNamed,
 		HandleErr:       ctx.handleErr,
 		NodeInterpreter: interp,
-		ParseFile:       nil, // TODO
 		NewBuiltin:      newBuiltinDefault,
 	}
 	p = gox.NewPackage(pkgPath, pkg.Name, confGox)
diff --git a/cmd/gengo/gen.go b/cmd/gengo/gen.go
index 518cdab6..f1c6ae99 100644
--- a/cmd/gengo/gen.go
+++ b/cmd/gengo/gen.go
@@ -167,8 +167,8 @@ func (p *Runner) GenGoPkg(pkgDir string, base *cl.Config) (err error) {
 
 	pkgDir, _ = filepath.Abs(pkgDir)
 
-	conf := *base.Ensure()
-	conf.Dir = pkgDir
+	conf := *base
+	conf.WorkingDir = pkgDir
 	if conf.Fset == nil {
 		conf.Fset = token.NewFileSet()
 	}
diff --git a/cmd/gop/main.go b/cmd/gop/main.go
index a1c6664f..0f8eb1be 100644
--- a/cmd/gop/main.go
+++ b/cmd/gop/main.go
@@ -23,6 +23,8 @@ import (
 	"strings"
 
 	"github.com/goplus/gop/cmd/internal/bug"
+	"github.com/goplus/gop/cmd/internal/deps"
+	"github.com/goplus/gop/cmd/internal/gopget"
 	"github.com/qiniu/x/log"
 
 	"github.com/goplus/gop/cmd/internal/base"
@@ -49,14 +51,16 @@ func init() {
 	base.Gop.Commands = []*base.Command{
 		run.Cmd,
 		gengo.Cmd,
-		gopfmt.Cmd,
-		mod.Cmd,
 		install.Cmd,
 		build.Cmd,
+		mod.Cmd,
+		gopfmt.Cmd,
+		gopget.Cmd,
 		bug.Cmd,
 		clean.Cmd,
 		env.Cmd,
 		test.Cmd,
+		deps.Cmd,
 		version.Cmd,
 	}
 }
diff --git a/cmd/goprun/goprun.go b/cmd/goprun/goprun.go
index d9a69547..9a3956db 100644
--- a/cmd/goprun/goprun.go
+++ b/cmd/goprun/goprun.go
@@ -6,8 +6,8 @@ import (
 	"os"
 	"os/exec"
 
-	"github.com/goplus/gop/x/gopmod"
 	"github.com/goplus/gop/x/gopproj"
+	"github.com/goplus/gop/x/gopprojs"
 )
 
 func main() {
@@ -15,11 +15,11 @@ func main() {
 		fmt.Fprint(os.Stderr, "Usage: goprun package [arguments ...]\n\n")
 		return
 	}
-	proj, args, err := gopproj.ParseOne(os.Args[1:]...)
+	proj, args, err := gopprojs.ParseOne(os.Args[1:]...)
 	if err != nil {
 		log.Fatalln(err)
 	}
-	var ctx = gopmod.New("")
+	var ctx = gopproj.New("")
 	goProj, err := ctx.OpenProject(0, proj)
 	if err != nil {
 		fmt.Fprint(os.Stderr, "OpenProject failed:", err)
diff --git a/cmd/internal/base/gocmd.go b/cmd/internal/base/gocmd.go
index 968d9c96..1ff7a8d6 100644
--- a/cmd/internal/base/gocmd.go
+++ b/cmd/internal/base/gocmd.go
@@ -77,13 +77,12 @@ func GenGoForBuild(dir string, recursive bool, errorHandle func()) {
 		}
 		return nil
 	})
-	baseConf := &cl.Config{PersistLoadPkgs: true}
-	runner.GenGo(dir, recursive, baseConf.Ensure())
+	baseConf := &cl.Config{}
+	runner.GenGo(dir, recursive, baseConf)
 	if hasError {
 		errorHandle()
 		os.Exit(1)
 	}
-	baseConf.PkgsLoader.Save()
 }
 
 // RunGoCmd executes `go` command tools.
diff --git a/cmd/internal/build/build.go b/cmd/internal/build/build.go
index d8b4dbbb..ebbcc886 100644
--- a/cmd/internal/build/build.go
+++ b/cmd/internal/build/build.go
@@ -33,18 +33,18 @@ import (
 
 // Cmd - gop build
 var Cmd = &base.Command{
-	UsageLine: "gop build [-v] [-o output] <gopSrcDir|gopSrcFile>",
+	UsageLine: "gop build [-v] [-o output] [packages]",
 	Short:     "Build Go+ files",
 }
 
 var (
 	flagBuildOutput string
-	flagVerbose     = flag.Bool("v", false, "print verbose information")
+	flagVerbose     = flag.Bool("v", false, "print verbose information.")
 	flag            = &Cmd.Flag
 )
 
 func init() {
-	flag.StringVar(&flagBuildOutput, "o", "", "gop build output file")
+	flag.StringVar(&flagBuildOutput, "o", "", "gop build output file.")
 	Cmd.Run = runCmd
 }
 
@@ -61,7 +61,7 @@ func runCmd(_ *base.Command, args []string) {
 		cl.SetDebug(cl.DbgFlagAll)
 		cl.SetDisableRecover(true)
 	}
-	modload.Load()
+	modload.UpdateGoMod(dir)
 	base.GenGoForBuild(dir, recursive, func() { fmt.Fprintln(os.Stderr, "GenGo failed, stop building") })
 	base.RunGoCmd(dir, "build", args...)
 }
diff --git a/cmd/internal/deps/deps.go b/cmd/internal/deps/deps.go
new file mode 100644
index 00000000..8d1ead7e
--- /dev/null
+++ b/cmd/internal/deps/deps.go
@@ -0,0 +1,75 @@
+/*
+ * 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 deps
+
+import (
+	"fmt"
+	"log"
+	"sort"
+
+	"github.com/goplus/gop/cmd/internal/base"
+	"github.com/goplus/gop/x/gopmod"
+)
+
+// -----------------------------------------------------------------------------
+
+// Cmd - gop deps
+var Cmd = &base.Command{
+	UsageLine: "gop deps [-v] [package]",
+	Short:     "Show dependencies of a package or module",
+}
+
+var (
+	flag = &Cmd.Flag
+	_    = flag.Bool("v", false, "print verbose information.")
+)
+
+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)
+	}
+	var dir string
+	narg := flag.NArg()
+	if narg < 1 {
+		dir = "."
+	} 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)
+	}
+}
+
+func check(err error) {
+	if err != nil {
+		log.Fatalln(err)
+	}
+}
+
+// -----------------------------------------------------------------------------
diff --git a/cmd/internal/env/env.go b/cmd/internal/env/env.go
index 27d85735..fa7e3ad7 100644
--- a/cmd/internal/env/env.go
+++ b/cmd/internal/env/env.go
@@ -32,7 +32,7 @@ import (
 // Cmd - gop env
 var Cmd = &base.Command{
 	UsageLine: "gop env [-json] [var ...]",
-	Short:     "prints Go+ environment information.",
+	Short:     "Prints Go+ environment information.",
 }
 
 var (
diff --git a/cmd/internal/gengo/go.go b/cmd/internal/gengo/go.go
index 3ac4b670..32cf189f 100644
--- a/cmd/internal/gengo/go.go
+++ b/cmd/internal/gengo/go.go
@@ -78,7 +78,6 @@ var (
 	flag      = &Cmd.Flag
 	flagDebug = flag.Bool("debug", false, "set log level to debug")
 	flagTest  = flag.Bool("test", false, "test Go+ package")
-	flagSlow  = flag.Bool("slow", false, "don't cache imported packages")
 )
 
 func init() {
@@ -100,7 +99,7 @@ func runCmd(cmd *base.Command, args []string) {
 	}
 	dir := flag.Arg(0)
 	dir = strings.TrimSuffix(dir, "/...")
-	modload.Load()
+	modload.UpdateGoMod(dir)
 	runner := new(gengo.Runner)
 	runner.SetAfter(func(p *gengo.Runner, dir string, flags int) error {
 		errs := p.ResetErrors()
@@ -114,7 +113,7 @@ func runCmd(cmd *base.Command, args []string) {
 		}
 		return nil
 	})
-	runner.GenGo(dir, true, &cl.Config{CacheLoadPkgs: !*flagSlow})
+	runner.GenGo(dir, true, &cl.Config{})
 	errs := runner.Errors()
 	if errs != nil {
 		for _, err := range errs {
diff --git a/cmd/internal/gopget/get.go b/cmd/internal/gopget/get.go
new file mode 100644
index 00000000..7223da5b
--- /dev/null
+++ b/cmd/internal/gopget/get.go
@@ -0,0 +1,91 @@
+/*
+ * 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 gopget
+
+import (
+	"fmt"
+	"log"
+	"os"
+	"syscall"
+
+	"github.com/goplus/gop/cmd/internal/base"
+	"github.com/goplus/gop/x/mod/modfetch"
+	"github.com/goplus/gop/x/mod/modload"
+)
+
+// -----------------------------------------------------------------------------
+
+// Cmd - gop get
+var Cmd = &base.Command{
+	UsageLine: "gop get [-v] [packages]",
+	Short:     `Add dependencies to current module and install them`,
+}
+
+var (
+	flag = &Cmd.Flag
+	_    = flag.Bool("v", false, "print verbose information.")
+)
+
+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)
+	}
+	narg := flag.NArg()
+	if narg < 1 {
+		log.Fatalln("TODO: not impl")
+	}
+	for i := 0; i < narg; i++ {
+		get(flag.Arg(i))
+	}
+}
+
+func get(pkgPath string) {
+	mod, err := modload.Load(".")
+	check(err)
+	check(mod.UpdateGoMod(true))
+	modPath, _ := splitPkgPath(pkgPath)
+	modVer, isClass, err := modfetch.Get(modPath)
+	if err == syscall.EEXIST {
+		return
+	}
+	check(err)
+	if isClass {
+		mod.AddRegister(modVer.Path)
+		fmt.Fprintf(os.Stderr, "gop get: registered %s\n", modVer.Path)
+	}
+	check(mod.AddRequire(modVer.Path, modVer.Version))
+	fmt.Fprintf(os.Stderr, "gop get: added %s %s\n", modVer.Path, modVer.Version)
+	check(mod.Save())
+	check(mod.UpdateGoMod(false))
+}
+
+func splitPkgPath(pkgPath string) (modPathWithVer string, pkgPathNoVer string) {
+	return pkgPath, pkgPath
+}
+
+func check(err error) {
+	if err != nil {
+		log.Fatalln(err)
+	}
+}
+
+// -----------------------------------------------------------------------------
diff --git a/cmd/internal/mod/download.go b/cmd/internal/mod/download.go
index c4e2c9a0..676ec51c 100644
--- a/cmd/internal/mod/download.go
+++ b/cmd/internal/mod/download.go
@@ -13,25 +13,20 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-// gop mod download
+
 package mod
 
 import (
 	"github.com/goplus/gop/cmd/internal/base"
-	"github.com/goplus/gop/cmd/internal/modload"
-	"github.com/goplus/gop/x/mod/modfetch"
+	"github.com/goplus/gop/cmd/internal/gopget"
 )
 
+// gop mod download
 var cmdDownload = &base.Command{
-	UsageLine: "gop mod download [-x] [-json] [modules]",
+	UsageLine: "gop mod download [-x -json] [modules]",
 	Short:     "download modules to local cache",
 }
 
 func init() {
-	cmdDownload.Run = runDownload // break init cycle
-}
-
-func runDownload(cmd *base.Command, args []string) {
-	modfetch.DownloadArgs(".", args...)
-	modload.SyncGopMod()
+	cmdDownload.Run = gopget.Cmd.Run
 }
diff --git a/cmd/internal/mod/init.go b/cmd/internal/mod/init.go
index a3f14e85..7f2e954c 100644
--- a/cmd/internal/mod/init.go
+++ b/cmd/internal/mod/init.go
@@ -14,17 +14,20 @@
  * limitations under the License.
  */
 
-// gop mod init
-
 package mod
 
 import (
-	"log"
+	"fmt"
+	"os"
+	"runtime"
+	"strings"
 
 	"github.com/goplus/gop/cmd/internal/base"
-	"github.com/goplus/gop/cmd/internal/modload"
+	"github.com/goplus/gop/env"
+	"github.com/goplus/gop/x/mod/modload"
 )
 
+// gop mod init
 var cmdInit = &base.Command{
 	UsageLine: "gop mod init [module]",
 	Short:     "initialize new module in current directory",
@@ -35,17 +38,40 @@ func init() {
 }
 
 func runInit(cmd *base.Command, args []string) {
-	if len(args) > 1 {
-		log.Fatalf("gop mod init: too many arguments")
+	switch len(args) {
+	case 0:
+		fatal(`Example usage:
+	'gop mod init example.com/m' to initialize a v0 or v1 module
+	'gop mod init example.com/m/v2' to initialize a v2 module
+
+Run 'gop help mod init' for more information.`)
+	case 1:
+	default:
+		fatal("gop mod init: too many arguments")
+	}
+	modPath := args[0]
+	mod, err := modload.Create(".", modPath, goMainVer(), env.MainVersion)
+	if err != nil {
+		fatal(err)
 	}
-	var modPath string
-	if len(args) == 1 {
-		modPath = args[0]
+	err = mod.Save()
+	if err != nil {
+		fatal(err)
 	}
+}
+
+func goMainVer() string {
+	ver := strings.TrimPrefix(runtime.Version(), "go")
+	if pos := strings.Index(ver, "."); pos > 0 {
+		pos++
+		if pos2 := strings.Index(ver[pos:], "."); pos2 > 0 {
+			ver = ver[:pos+pos2]
+		}
+	}
+	return ver
+}
 
-	// modfetch.InitArgs(".", args...)
-	modload.CreateModFile(modPath) // does all the hard work
-	modload.LoadModFile()
-	modload.SyncGoMod()
-	modload.SyncGopMod()
+func fatal(msg interface{}) {
+	fmt.Fprintln(os.Stderr, msg)
+	os.Exit(1)
 }
diff --git a/cmd/internal/mod/mod.go b/cmd/internal/mod/mod.go
index 58d17279..095d28cb 100644
--- a/cmd/internal/mod/mod.go
+++ b/cmd/internal/mod/mod.go
@@ -21,7 +21,7 @@ import (
 
 var Cmd = &base.Command{
 	UsageLine: "gop mod",
-	Short:     "module maintenance",
+	Short:     "Module maintenance",
 
 	Commands: []*base.Command{
 		cmdInit,
diff --git a/cmd/internal/mod/tidy.go b/cmd/internal/mod/tidy.go
index 4b8473c0..6a9f486a 100644
--- a/cmd/internal/mod/tidy.go
+++ b/cmd/internal/mod/tidy.go
@@ -13,17 +13,19 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-// gop mod tidy
+
 package mod
 
 import (
+	"log"
+
 	"github.com/goplus/gop/cmd/internal/base"
-	"github.com/goplus/gop/cmd/internal/modload"
-	"github.com/goplus/gop/x/mod/modfetch"
+	"github.com/goplus/gop/x/mod/modload"
 )
 
+// gop mod tidy
 var cmdTidy = &base.Command{
-	UsageLine: "gop mod tidy [-e] [-v]",
+	UsageLine: "gop mod tidy [-e -v]",
 	Short:     "add missing and remove unused modules",
 }
 
@@ -32,8 +34,13 @@ func init() {
 }
 
 func runTidy(cmd *base.Command, args []string) {
-	modload.LoadModFile()
-	modload.SyncGoMod()
-	modfetch.TidyArgs(".", args...)
-	modload.SyncGopMod()
+	mod, err := modload.Load(".")
+	check(err)
+	check(mod.Tidy())
+}
+
+func check(err error) {
+	if err != nil {
+		log.Fatalln(err)
+	}
 }
diff --git a/cmd/internal/modload/classfile.go b/cmd/internal/modload/classfile.go
deleted file mode 100644
index 5d173e67..00000000
--- a/cmd/internal/modload/classfile.go
+++ /dev/null
@@ -1,74 +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 modload
-
-import (
-	"log"
-	"path/filepath"
-
-	"golang.org/x/mod/module"
-
-	"github.com/goplus/gop/x/mod/modfetch"
-	"github.com/goplus/gop/x/mod/modfile"
-)
-
-func LoadClassFile() {
-	if modFile.Register == nil {
-		return
-	}
-
-	var dir string
-	var err error
-	var claassMod module.Version
-
-	for _, require := range modFile.Require {
-		if require.Mod.Path == modFile.Register[0].ClassfileMod {
-			claassMod = require.Mod
-		}
-	}
-	for _, replace := range modFile.Replace {
-		if replace.Old.Path == modFile.Register[0].ClassfileMod {
-			claassMod = replace.New
-		}
-	}
-
-	if claassMod.Version != "" {
-		dir, err = modfetch.Download(claassMod)
-		if err != nil {
-			log.Fatalf("gop: download classsfile module error %v", err)
-		}
-	} else {
-		dir = claassMod.Path
-	}
-
-	if dir == "" {
-		log.Fatalf("gop: can't find classfile path in require statment")
-	}
-
-	gopmod := filepath.Join(modRoot, dir, "gop.mod")
-	data, err := modfetch.Read(gopmod)
-	if err != nil {
-		log.Fatalf("gop: %v", err)
-	}
-
-	var fixed bool
-	f, err := modfile.Parse(gopmod, data, fixVersion(&fixed))
-	if err != nil {
-		// Errors returned by modfile.Parse begin with file:line.
-		log.Fatalf("go: errors parsing go.mod:\n%s\n", err)
-	}
-	classModFile = f
-}
diff --git a/cmd/internal/modload/modload.go b/cmd/internal/modload/modload.go
index 07f07c29..8f87a5de 100644
--- a/cmd/internal/modload/modload.go
+++ b/cmd/internal/modload/modload.go
@@ -13,473 +13,30 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 package modload
 
 import (
-	"bytes"
-	"errors"
-	"fmt"
-	"log"
-	"os"
-	"path/filepath"
-
-	gomodfile "golang.org/x/mod/modfile"
-	"golang.org/x/mod/module"
-
 	"github.com/goplus/gop/cl"
 	"github.com/goplus/gop/cmd/gengo"
-	"github.com/goplus/gop/cmd/internal/search"
-	"github.com/goplus/gop/env"
-	"github.com/goplus/gop/x/mod/modfetch"
-	"github.com/goplus/gop/x/mod/modfile"
-)
-
-var (
-	modFile, classModFile *modfile.File
-
-	initialized bool
-	modRoot     string
-	Target      module.Version
+	"github.com/goplus/gop/x/gopmod"
 )
 
-var ErrNoModRoot = errors.New("gop.mod file not found in current directory or any parent directory; see 'gop help modules'")
-
-func findModuleRoot(dir string) (root string) {
-	if dir == "" {
-		panic("dir not set")
-	}
-	dir = filepath.Clean(dir)
-
-	// Look for enclosing gop.mod or go.mod.
-	for {
-		if fi, err := os.Stat(filepath.Join(dir, "gop.mod")); err == nil && !fi.IsDir() {
-			return dir
-		}
-		if fi, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil && !fi.IsDir() {
-			return dir
-		}
-		d := filepath.Dir(dir)
-		if d == dir {
-			break
-		}
-		dir = d
-	}
-	return ""
-}
-
-func SetModRoot(dir string) {
-	modRoot = dir
-}
-
-func getcwd() string {
-	path, _ := os.Getwd()
-	return path
-}
-
-var gopRoot = getcwd()
-
-// HasModRoot reports whether a main module is present.
-// HasModRoot may return false even if Enabled returns true: for example, 'get'
-// does not require a main module.
-func HasModRoot() bool {
-	Init()
-	return modRoot != ""
-}
-
-// GopModFilePath returns the effective path of the go.mod file. Normally, this
-// "go.mod" in the directory returned by ModRoot, but the -modfile flag may
-// change its location. ModFilePath calls base.Fatalf if there is no main
-// module, even if -modfile is set.
-func GopModFilePath() string {
-	if !HasModRoot() {
-		log.Fatalf("gop: %v", ErrNoModRoot)
-	}
-	return filepath.Join(modRoot, "gop.mod")
-}
-
-// GoModFilePath returns the effective path of the go.mod file. Normally, this
-// "go.mod" in the directory returned by ModRoot, but the -modfile flag may
-// change its location. ModFilePath calls base.Fatalf if there is no main
-// module, even if -modfile is set.
-func GoModFilePath() string {
-	if !HasModRoot() {
-		log.Fatalf("gop: %v", ErrNoModRoot)
-	}
-	return filepath.Join(modRoot, "go.mod")
-}
-
-// Init determines whether module mode is enabled, locates the root of the
-// current module (if any), sets environment variables for Git subprocesses, and
-// configures the cfg, codehost, load, modfetch, and search packages for use
-// with modules.
-func Init() {
-	if initialized {
-		return
-	}
-	initialized = true
-
-	// Disable any prompting for passwords by Git.
-	// Only has an effect for 2.3.0 or later, but avoiding
-	// the prompt in earlier versions is just too hard.
-	// If user has explicitly set GIT_TERMINAL_PROMPT=1, keep
-	// prompting.
-	// See golang.org/issue/9341 and golang.org/issue/12706.
-	if os.Getenv("GIT_TERMINAL_PROMPT") == "" {
-		os.Setenv("GIT_TERMINAL_PROMPT", "0")
-	}
-
-	// Disable any ssh connection pooling by Git.
-	// If a Git subprocess forks a child into the background to cache a new connection,
-	// that child keeps stdout/stderr open. After the Git subprocess exits,
-	// os /exec expects to be able to read from the stdout/stderr pipe
-	// until EOF to get all the data that the Git subprocess wrote before exiting.
-	// The EOF doesn't come until the child exits too, because the child
-	// is holding the write end of the pipe.
-	// This is unfortunate, but it has come up at least twice
-	// (see golang.org/issue/13453 and golang.org/issue/16104)
-	// and confuses users when it does.
-	// If the user has explicitly set GIT_SSH or GIT_SSH_COMMAND,
-	// assume they know what they are doing and don't step on it.
-	// But default to turning off ControlMaster.
-	if os.Getenv("GIT_SSH") == "" && os.Getenv("GIT_SSH_COMMAND") == "" {
-		os.Setenv("GIT_SSH_COMMAND", "ssh -o ControlMaster=no")
-	}
-
-	if modRoot != "" {
-		// nothing to do
-	} else {
-		modRoot := findModuleRoot(gopRoot)
-		if modRoot != "" {
-			SetModRoot(modRoot)
-		}
-	}
-}
-
-func findModulePath(dir string) (string, error) {
-	// Look for path in GOPATH.
-	var badPathErr error
-	for _, gpdir := range filepath.SplitList(getGoPath()) {
-		if gpdir == "" {
-			continue
-		}
-		if rel := search.InDir(dir, filepath.Join(gpdir, "src")); rel != "" && rel != "." {
-			path := filepath.ToSlash(rel)
-			return path, nil
-		}
-	}
-
-	reason := "outside GOPATH, module path must be specified"
-	if badPathErr != nil {
-		// return a different error message if the module was in GOPATH, but
-		// the module path determined above would be an invalid path.
-		reason = fmt.Sprintf("bad module path inferred from directory in GOPATH: %v", badPathErr)
-	}
-	msg := `cannot determine module path for source directory %s (%s)
-
-Example usage:
-	'gop mod init example.com/m' to initialize a v0 or v1 module
-	'gop mod init example.com/m/v2' to initialize a v2 module
-
-Run 'gop help mod init' for more information.
-`
-	return "", fmt.Errorf(msg, dir, reason)
-}
-
-// CreateModFile initializes a new module by creating a go.mod file.
-//
-// If modPath is empty, CreateModFile will attempt to infer the path from the
-// directory location within GOPATH.
-//
-// If a vendoring configuration file is present, CreateModFile will attempt to
-// translate it to go.mod directives. The resulting build list may not be
-// exactly the same as in the legacy configuration (for example, we can't get
-// packages at multiple versions from the same module).
-func CreateModFile(modPath string) {
-	modRoot = gopRoot
-	Init()
-	modFilePath := GopModFilePath()
-	if _, err := os.Stat(modFilePath); err == nil {
-		log.Fatalf("gop: %s already exists", modFilePath)
-	}
-
-	if modPath == "" {
-		var err error
-		modPath, err = findModulePath(modRoot)
-		if err != nil {
-			log.Fatalf("gop: %v", err)
-		}
-	}
-
-	fmt.Fprintf(os.Stderr, "gop: creating new gop.mod: module %s\n", modPath)
-	modFile = new(modfile.File)
-	modFile.AddModuleStmt(modPath)
-	addGopStmt() // Add the gop directive before converted module requirements.
-	WriteGopMod()
-}
-
-func Load() {
-	LoadModFile()
-	if modRoot == "" {
-		return
-	}
-	SyncGoMod()
-	if classModFile != nil && classModFile.Classfile != nil {
-		gengo.RegisterPkgFlags(classModFile.Classfile.ProjExt, gengo.PkgFlagGmx)
-		gengo.RegisterPkgFlags(classModFile.Classfile.WorkExt, gengo.PkgFlagSpx)
-		cl.RegisterClassFileType(classModFile.Classfile.ProjExt,
-			classModFile.Classfile.WorkExt, classModFile.Classfile.PkgPaths...)
-	}
-}
-
-// fixVersion returns a modfile.VersionFixer implemented using the Query function.
-//
-// It resolves commit hashes and branch names to versions,
-// canonicalizes versions that appeared in early vgo drafts,
-// and does nothing for versions that already appear to be canonical.
-//
-// The VersionFixer sets 'fixed' if it ever returns a non-canonical version.
-func fixVersion(fixed *bool) modfile.VersionFixer {
-	return func(path, vers string) (resolved string, err error) {
-		// do nothing
-		return vers, nil
-	}
-}
-
-func fixGoVersion(fixed *bool) gomodfile.VersionFixer {
-	return func(path, vers string) (resolved string, err error) {
-		// do nothing
-		return vers, nil
-	}
-}
+// -----------------------------------------------------------------------------
 
-// LoadModFile sets Target and, if there is a main module, parses the initial
-// build list from its go.mod file.
-//
-// LoadModFile may make changes in memory, like adding a go directive and
-// ensuring requirements are consistent. WriteGoMod should be called later to
-// write changes out to disk or report errors in readonly mode.
-//
-// As a side-effect, LoadModFile may change cfg.BuildMod to "vendor" if
-// -mod wasn't set explicitly and automatic vendoring should be enabled.
-func LoadModFile() {
-	Init()
-	if modRoot == "" {
-		return
-	}
-	// If gop.mod does not exist, then modroot does not exist,
-	// and if go.mod exists then a copy of go.mod will be synchronized to gop.mod
-	gopmod := GopModFilePath()
-	gomod := GoModFilePath()
-	if _, err := os.Stat(gopmod); os.IsNotExist(err) {
-		if _, err := os.Stat(gomod); err == nil {
-			SyncGopMod()
-		}
-		return
-	}
-
-	data, err := modfetch.Read(gopmod)
+func UpdateGoMod(dir string) {
+	p, err := gopmod.Load(dir)
 	if err != nil {
-		log.Fatalf("gop: %v", err)
-	}
-
-	var fixed bool
-	f, err := modfile.Parse(gopmod, data, fixVersion(&fixed))
-	if err != nil {
-		// Errors returned by modfile.Parse begin with file:line.
-		log.Fatalf("gop: errors parsing gop.mod:\n%s\n", err)
-	}
-	modFile = f
-
-	if f.Module == nil {
-		// No module declaration. Must add module path.
-		log.Fatalf("gop: no module declaration in gop.mod. To specify the module path:\n")
-	}
-
-	LoadClassFile()
-}
-
-// addGoStmt adds a gop directive to the gop.mod file if it does not already include one.
-// The 'gop' version added, if any, is the latest version supported by this toolchain.
-func addGopStmt() {
-	if modFile.Gop != nil && modFile.Gop.Version != "" {
-		return
-	}
-	version := env.MainVersion
-	if err := modFile.AddGopStmt(version); err != nil {
-		log.Fatalf("gop: internal error: %v", err)
-	}
-}
-
-// WriteGopMod writes the current build list back to gop.mod.
-func WriteGopMod() {
-	// If we aren't in a module, we don't have anywhere to write a go.mod file.
-	if modRoot == "" {
 		return
 	}
-	addGopStmt()
-
-	modFile.Cleanup()
-
-	new, err := modFile.Format()
-	if err != nil {
-		log.Fatalf("gop: %v", err)
-	}
-
-	errNoChange := errors.New("no update needed")
-
-	err = modfetch.Transform(GopModFilePath(), func(old []byte) ([]byte, error) {
-		if bytes.Equal(old, new) {
-			// The go.mod file is already equal to new, possibly as the result of some
-			// other process.
-			return nil, errNoChange
-		}
-		return new, nil
-	})
-
-	if err != nil && err != errNoChange {
-		log.Fatalf("gop: updating gop.mod: %v", err)
-	}
-}
-
-func getGoPath() string {
-	return os.Getenv("GOPATH")
-}
-
-func SyncGoMod() {
-	gomodPath := GoModFilePath()
-	gomod := &gomodfile.File{}
-	if _, err := os.Stat(gomodPath); err == nil {
-		data, err := modfetch.Read(gomodPath)
-		if err != nil {
-			log.Fatalln(err)
-		}
-		var fixed bool
-		gomod, err = gomodfile.Parse(gomodPath, data, fixGoVersion(&fixed))
-		if err != nil {
-			// Errors returned by modfile.Parse begin with file:line.
-			log.Fatalf("gop: errors parsing gop.mod:\n%s\n", err)
-		}
-	}
-
-	gomod.AddModuleStmt(modFile.Module.Mod.Path)
-	if modFile.Go != nil {
-		gomod.AddGoStmt(modFile.Go.Version)
-	}
-
-	for _, require := range modFile.Require {
-		gomod.AddRequire(require.Mod.Path, require.Mod.Version)
-	}
-
-	for _, replace := range modFile.Replace {
-		gomod.AddReplace(replace.Old.Path, replace.Old.Version, replace.New.Path, replace.New.Version)
-	}
-
-	for _, exclude := range modFile.Exclude {
-		gomod.AddExclude(exclude.Mod.Path, exclude.Mod.Version)
-	}
-
-	for _, retract := range modFile.Retract {
-		gomod.AddRetract(gomodfile.VersionInterval(retract.VersionInterval), retract.Rationale)
-	}
-
-	if classModFile != nil {
-		for _, require := range classModFile.Require {
-			gomod.AddRequire(require.Mod.Path, require.Mod.Version)
-		}
-
-		for _, replace := range classModFile.Replace {
-			gomod.AddReplace(replace.Old.Path, replace.Old.Version, replace.New.Path, replace.New.Version)
-		}
-
-		for _, exclude := range classModFile.Exclude {
-			gomod.AddExclude(exclude.Mod.Path, exclude.Mod.Version)
-		}
-
-		for _, retract := range classModFile.Retract {
-			gomod.AddRetract(gomodfile.VersionInterval(retract.VersionInterval), retract.Rationale)
-		}
-	}
-
-	gomod.Cleanup()
-
-	new, err := gomod.Format()
-	if err != nil {
-		log.Fatalf("gop: %v", err)
-	}
-
-	errNoChange := errors.New("no update needed")
-	err = modfetch.Transform(GoModFilePath(), func(old []byte) ([]byte, error) {
-		if bytes.Equal(old, new) {
-			// The go.mod file is already equal to new, possibly as the result of some
-			// other process.
-			return nil, errNoChange
+	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...)
 		}
-		return new, nil
 	})
-
-	if err != nil && err != errNoChange {
-		log.Fatalf("gop: updating gop.mod: %v", err)
-	}
 }
 
-func SyncGopMod() {
-	gomodPath := GoModFilePath()
-	gomod := &gomodfile.File{}
-	if _, err := os.Stat(gomodPath); err == nil {
-		data, err := modfetch.Read(gomodPath)
-		if err != nil {
-			log.Fatalln(err)
-		}
-		var fixed bool
-		gomod, err = gomodfile.Parse(gomodPath, data, fixGoVersion(&fixed))
-		if err != nil {
-			// Errors returned by modfile.Parse begin with file:line.
-			log.Fatalf("gop: errors parsing gop.mod:\n%s\n", err)
-		}
-	}
-
-	if modFile == nil {
-		modFile = &modfile.File{}
-		modFile.AddModuleStmt(gomod.Module.Mod.Path)
-	}
-	if gomod.Go != nil {
-		modFile.AddGoStmt(gomod.Go.Version)
-	}
-
-	for _, require := range gomod.Require {
-		modFile.AddRequire(require.Mod.Path, require.Mod.Version)
-	}
-
-	for _, replace := range gomod.Replace {
-		modFile.AddReplace(replace.Old.Path, replace.Old.Version, replace.New.Path, replace.New.Version)
-	}
-
-	for _, exclude := range gomod.Exclude {
-		modFile.AddExclude(exclude.Mod.Path, exclude.Mod.Version)
-	}
-
-	for _, retract := range gomod.Retract {
-		modFile.AddRetract(modfile.VersionInterval(retract.VersionInterval), retract.Rationale)
-	}
-
-	modFile.Cleanup()
-
-	new, err := modFile.Format()
-	if err != nil {
-		log.Fatalf("gop: %v", err)
-	}
-
-	errNoChange := errors.New("no update needed")
-	err = modfetch.Transform(GopModFilePath(), func(old []byte) ([]byte, error) {
-		if bytes.Equal(old, new) {
-			// The go.mod file is already equal to new, possibly as the result of some
-			// other process.
-			return nil, errNoChange
-		}
-		return new, nil
-	})
-
-	if err != nil && err != errNoChange {
-		log.Fatalf("gop: updating gop.mod: %v", err)
-	}
-}
+// -----------------------------------------------------------------------------
diff --git a/cmd/internal/run/mod.go b/cmd/internal/run/mod.go
index 15e27c79..880d0497 100644
--- a/cmd/internal/run/mod.go
+++ b/cmd/internal/run/mod.go
@@ -6,32 +6,32 @@ import (
 	"path/filepath"
 
 	"github.com/goplus/gop/env"
-	"github.com/goplus/gop/x/gopmod"
+	"github.com/goplus/gop/x/gopproj"
 	"github.com/qiniu/x/log"
 )
 
-func findGoModFile(dir string) (modfile string, noCacheFile bool, err error) {
+func findGoModFile(dir string) (modfile string, err error) {
 	modfile, err = env.GOPMOD(dir)
 	if err != nil {
 		modfile = filepath.Join(env.GOPROOT(), "go.mod")
-		return modfile, true, nil
+		return modfile, nil
 	}
 	return
 }
 
-func findGoModDir(dir string) (string, bool) {
-	modfile, nocachefile, err := findGoModFile(dir)
+func findGoModDir(dir string) string {
+	modfile, err := findGoModFile(dir)
 	if err != nil {
 		log.Fatalln("findGoModFile:", err)
 	}
-	return filepath.Dir(modfile), nocachefile
+	return filepath.Dir(modfile)
 }
 
 func gopRun(source string, args ...string) {
-	ctx := gopmod.New("")
+	ctx := gopproj.New("")
 	flags := 0
 	if *flagGop {
-		flags = gopmod.FlagGoAsGoPlus
+		flags = gopproj.FlagGoAsGoPlus
 	}
 	goProj, err := ctx.OpenFiles(flags, source)
 	if err != nil {
diff --git a/cmd/internal/run/run.go b/cmd/internal/run/run.go
index 47def396..3bc8a741 100644
--- a/cmd/internal/run/run.go
+++ b/cmd/internal/run/run.go
@@ -24,11 +24,9 @@ import (
 	"path/filepath"
 
 	"github.com/qiniu/x/log"
-	"golang.org/x/tools/go/packages"
 
 	"github.com/goplus/gop/ast"
 	"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/parser"
@@ -116,7 +114,7 @@ func runCmd(cmd *base.Command, args []string) {
 	if isDir {
 		srcDir = src
 		gofile = src + "/gop_autogen.go"
-		modload.Load()
+		modload.UpdateGoMod(srcDir)
 		isDirty = true // TODO: check if code changed
 		if isDirty {
 			pkgs, err = parser.ParseDir(fset, src, nil, parserMode)
@@ -143,9 +141,8 @@ func runCmd(cmd *base.Command, args []string) {
 			os.Exit(12)
 		}
 
-		modDir, noCacheFile := findGoModDir(srcDir)
-		conf := &cl.Config{
-			Dir: modDir, TargetDir: srcDir, Fset: fset, CacheLoadPkgs: true, PersistLoadPkgs: !noCacheFile}
+		modDir := findGoModDir(srcDir)
+		conf := &cl.Config{WorkingDir: modDir, TargetDir: srcDir, Fset: fset}
 		out, err := cl.NewPackage("", mainPkg, conf)
 		if err != nil {
 			fmt.Fprintln(os.Stderr, err)
@@ -155,7 +152,6 @@ func runCmd(cmd *base.Command, args []string) {
 		if err != nil {
 			log.Fatalln("saveGoFile failed:", err)
 		}
-		conf.PkgsLoader.Save()
 	}
 
 	goRun(gofile, args)
@@ -186,38 +182,6 @@ func goRun(file string, args []string) {
 }
 
 func runGoPkg(src string, args []string, doRun bool) {
-	modfile, noCacheFile, err := findGoModFile(src)
-	if err != nil {
-		log.Fatalln("findGoModFile:", err)
-	}
-	base := filepath.Dir(modfile)
-	rel, _ := filepath.Rel(base, src)
-	modPath, err := cl.GetModulePath(modfile)
-	if err != nil {
-		log.Fatalln("GetModulePath:", err)
-	}
-	pkgPath := filepath.Join(modPath, rel)
-	const (
-		loadTypes = packages.NeedImports | packages.NeedDeps | packages.NeedTypes
-		loadModes = loadTypes | packages.NeedName | packages.NeedModule
-	)
-	baseConf := &cl.Config{
-		Fset:            token.NewFileSet(),
-		GenGoPkg:        new(gengo.Runner).GenGoPkg,
-		CacheLoadPkgs:   true,
-		PersistLoadPkgs: !noCacheFile,
-		NoFileLine:      true,
-	}
-	loadConf := &packages.Config{Mode: loadModes, Fset: baseConf.Fset}
-	pkgs, err := baseConf.Ensure().PkgsLoader.Load(loadConf, pkgPath)
-	if err != nil || len(pkgs) == 0 {
-		log.Fatalln("PkgsLoader.Load failed:", err)
-	}
-	if pkgs[0].Name != "main" {
-		fmt.Fprintln(os.Stderr, "TODO: not a main package")
-		os.Exit(12)
-	}
-	baseConf.PkgsLoader.Save()
 	if doRun {
 		goRun(src+"/.", args)
 	}
diff --git a/cmd/internal/search/search.go b/cmd/internal/search/search.go
deleted file mode 100644
index 9e72beba..00000000
--- a/cmd/internal/search/search.go
+++ /dev/null
@@ -1,77 +0,0 @@
-package search
-
-import (
-	"path/filepath"
-	"strings"
-)
-
-// InDir checks whether path is in the file tree rooted at dir.
-// If so, InDir returns an equivalent path relative to dir.
-// If not, InDir returns an empty string.
-// InDir makes some effort to succeed even in the presence of symbolic links.
-// TODO(rsc): Replace internal/test.inDir with a call to this function for Go 1.12.
-func InDir(path, dir string) string {
-	if rel := inDirLex(path, dir); rel != "" {
-		return rel
-	}
-	xpath, err := filepath.EvalSymlinks(path)
-	if err != nil || xpath == path {
-		xpath = ""
-	} else {
-		if rel := inDirLex(xpath, dir); rel != "" {
-			return rel
-		}
-	}
-
-	xdir, err := filepath.EvalSymlinks(dir)
-	if err == nil && xdir != dir {
-		if rel := inDirLex(path, xdir); rel != "" {
-			return rel
-		}
-		if xpath != "" {
-			if rel := inDirLex(xpath, xdir); rel != "" {
-				return rel
-			}
-		}
-	}
-	return ""
-}
-
-// inDirLex is like inDir but only checks the lexical form of the file names.
-// It does not consider symbolic links.
-// TODO(rsc): This is a copy of str.HasFilePathPrefix, modified to
-// return the suffix. Most uses of str.HasFilePathPrefix should probably
-// be calling InDir instead.
-func inDirLex(path, dir string) string {
-	pv := strings.ToUpper(filepath.VolumeName(path))
-	dv := strings.ToUpper(filepath.VolumeName(dir))
-	path = path[len(pv):]
-	dir = dir[len(dv):]
-	switch {
-	default:
-		return ""
-	case pv != dv:
-		return ""
-	case len(path) == len(dir):
-		if path == dir {
-			return "."
-		}
-		return ""
-	case dir == "":
-		return path
-	case len(path) > len(dir):
-		if dir[len(dir)-1] == filepath.Separator {
-			if path[:len(dir)] == dir {
-				return path[len(dir):]
-			}
-			return ""
-		}
-		if path[len(dir)] == filepath.Separator && path[:len(dir)] == dir {
-			if len(path) == len(dir)+1 {
-				return "."
-			}
-			return path[len(dir)+1:]
-		}
-		return ""
-	}
-}
diff --git a/cmd/internal/version/ver.go b/cmd/internal/version/ver.go
index a4797c81..64fc51ac 100644
--- a/cmd/internal/version/ver.go
+++ b/cmd/internal/version/ver.go
@@ -26,7 +26,7 @@ import (
 
 // -----------------------------------------------------------------------------
 
-// Cmd - gop build
+// Cmd - gop version
 var Cmd = &base.Command{
 	UsageLine: "gop version [-v]",
 	Short:     "Version prints the build information for Gop executables",
diff --git a/go.mod b/go.mod
index 65b1c556..288983ea 100644
--- a/go.mod
+++ b/go.mod
@@ -3,8 +3,7 @@ module github.com/goplus/gop
 go 1.16
 
 require (
-	github.com/goplus/gox v1.8.7
+	github.com/goplus/gox v1.9.1
 	github.com/qiniu/x v1.11.5
 	golang.org/x/mod v0.5.1
-	golang.org/x/tools v0.1.8
 )
diff --git a/go.sum b/go.sum
index e18552a7..5d3e59a0 100644
--- a/go.sum
+++ b/go.sum
@@ -1,5 +1,5 @@
-github.com/goplus/gox v1.8.7 h1:XtUEazI+EMSyN9A0IGbrPe99dpT5lnD1k98HwmVL9Nc=
-github.com/goplus/gox v1.8.7/go.mod h1:XWqV75HImeysI8+Q9gAdYyvwlP8jds24xsgYRwTO3JI=
+github.com/goplus/gox v1.9.1 h1:vupKI4LRpGDuKPSZFaN1BjiEsKjzSsUEKiwssQ/3fBU=
+github.com/goplus/gox v1.9.1/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/parser_gop.go b/parser/parser_gop.go
index 9be2ad19..ed3f0ec5 100644
--- a/parser/parser_gop.go
+++ b/parser/parser_gop.go
@@ -24,6 +24,7 @@ import (
 	"os"
 	"path/filepath"
 	"strings"
+	"syscall"
 
 	"github.com/goplus/gop/ast"
 	"github.com/goplus/gop/token"
@@ -164,14 +165,15 @@ var (
 )
 
 // RegisterFileType registers a new Go+ class file type.
-func RegisterFileType(ext string, format ast.FileType) {
+func RegisterFileType(ext string, format ast.FileType) error {
 	if format != ast.FileTypeSpx && format != ast.FileTypeGmx {
-		panic("RegisterFileType: format should be FileTypeSpx or FileTypeGmx")
+		return syscall.EINVAL
 	}
 	if _, ok := extGopFiles[ext]; ok {
-		panic("RegisterFileType: file type exists")
+		return syscall.EEXIST
 	}
 	extGopFiles[ext] = format
+	return nil
 }
 
 // -----------------------------------------------------------------------------
diff --git a/x/gengo/gengo.go b/x/gengo/gengo.go
new file mode 100644
index 00000000..8c158ff9
--- /dev/null
+++ b/x/gengo/gengo.go
@@ -0,0 +1,303 @@
+/*
+ * 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 (
+	"bytes"
+	"crypto/md5"
+	"encoding/hex"
+	"fmt"
+	"go/types"
+	"os"
+	"path/filepath"
+	"sort"
+	"strings"
+	"time"
+
+	"github.com/goplus/gop/token"
+	"github.com/goplus/gop/x/gopmod"
+	"github.com/goplus/gop/x/mod/modfetch"
+	"github.com/goplus/gox/packages"
+)
+
+const (
+	pkgFlagIll = 1 << iota
+	pkgFlagChanged
+)
+
+const (
+	stateNormal = iota
+	stateProcessing
+	stateDone
+	stateOccurErrors = -1
+)
+
+type pkgInfo struct {
+	path    string
+	runner  *Runner
+	imports []string
+	deps    []*pkgInfo
+	hash    string
+	flags   int
+}
+
+type Runner struct {
+	runners map[string]*Runner
+	mod     *gopmod.Module
+	modRoot string
+	modPath string
+	modTime time.Time
+	pkgs    map[string]*pkgInfo
+	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)
+	}
+	if fset == nil {
+		fset = token.NewFileSet()
+	}
+	var errs ErrorList
+	p.genGoPkgs(fset, loaded, &errs)
+	if len(errs) > 0 {
+		err = errs
+	}
+	return
+}
+
+func (p *Runner) genGoPkgs(fset *token.FileSet, loaded map[string]*types.Package, errs *ErrorList) {
+	if p.state != stateNormal {
+		if p.state != stateDone {
+			panic("TODO: cycle?")
+		}
+		return
+	}
+	p.state = stateProcessing
+	imports := make(map[string]none)
+	for _, pkg := range p.pkgs {
+		if pkg.flags == pkgFlagChanged {
+			for _, imp := range pkg.imports {
+				imports[imp] = none{}
+			}
+		}
+	}
+	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...)
+	if err != nil {
+		*errs, p.state = append(*errs, err), stateOccurErrors
+		return
+	}
+	_ = imp
+	for path, pkg := range p.pkgs {
+		if pkg.flags == pkgFlagChanged {
+			p.genGoPkg(fset, path)
+		}
+	}
+	p.state = stateDone
+}
+
+func getKeys(v map[string]none) []string {
+	keys := make([]string, 0, len(v))
+	for key := range v {
+		keys = append(keys, key)
+	}
+	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)
+	*/
+}
+
+// -----------------------------------------------------------------------------
+
+type none = struct{}
+
+func modTime(file string, errs *ErrorList) (mt time.Time) {
+	fi, err := os.Stat(file)
+	if err != nil {
+		*errs = append(*errs, err)
+		return
+	}
+	return fi.ModTime()
+}
+
+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...)
+	if err != nil {
+		return
+	}
+	modRoot = modRoot[:len(modRoot)-1] // remove the last pathSeparator
+
+	var errs ErrorList
+	p = &Runner{
+		mod:     mod,
+		modRoot: modRoot,
+		modPath: modPath,
+		modTime: modTime(modFile, &errs),
+		pkgs:    make(map[string]*pkgInfo),
+	}
+	p.runners = map[string]*Runner{modRoot: p}
+	for _, pkgPath := range pkgPaths {
+		p.genDeps(pkgPath, pkgPath, &errs)
+	}
+	if len(errs) > 0 {
+		err = errs
+	}
+	return
+}
+
+func (p *Runner) genDeps(pkgPath, pkgPathBase string, errs *ErrorList) (pkg *pkgInfo) {
+	switch p.mod.PkgType(pkgPath) {
+	case gopmod.PkgtStandard:
+		return nil
+	case gopmod.PkgtLocal:
+		pkgPath = filepath.ToSlash(filepath.Join(pkgPathBase, pkgPath))
+	case gopmod.PkgtModule:
+	case gopmod.PkgtExtern:
+		return p.genExternDeps(pkgPath, errs)
+	default:
+		panic("TODO: invalid pkgPath")
+	}
+	pkg = &pkgInfo{
+		path:   pkgPath,
+		runner: p,
+	}
+	p.pkgs[pkgPath] = pkg
+	dir := filepath.Join(p.modRoot, pkgPath[len(p.modPath):])
+
+	ci, err := p.mod.ChangeInfo(dir)
+	if err != nil {
+		*errs, pkg.flags = append(*errs, err), pkgFlagIll
+		return
+	}
+	if ci.GopFileNum == 0 {
+		return
+	}
+
+	var buf bytes.Buffer
+	var depsHash string
+	imps, err := p.mod.Imports(dir, false)
+	if err != nil {
+		*errs, pkg.flags = append(*errs, err), pkgFlagIll
+		return
+	}
+	sort.Strings(imps)
+	for _, imp := range imps {
+		if t := p.genDeps(imp, pkgPath, errs); t != nil {
+			buf.WriteString(t.runner.modRoot)
+			pkg.flags |= t.flags
+			if t.flags == pkgFlagChanged {
+				pkg.deps = append(pkg.deps, t)
+			}
+		} else {
+			buf.WriteString(imp)
+		}
+		buf.WriteByte('\n')
+	}
+	pkg.imports = imps
+	pkg.hash = hashOf(buf.Bytes())
+	if p.sourceChanged(dir, ci, &depsHash) || pkg.hash != depsHash {
+		pkg.flags |= pkgFlagChanged
+	}
+	return
+}
+
+func hashOf(b []byte) string {
+	h := md5.Sum(b)
+	return hex.EncodeToString(h[:])
+}
+
+func (p *Runner) genExternDeps(pkgPath string, errs *ErrorList) *pkgInfo {
+	_, modVer, ok := p.mod.LookupExternPkg(pkgPath)
+	if !ok {
+		panic("TODO: externPkg not found")
+	}
+	modRoot, err := modfetch.ModCachePath(modVer)
+	if err != nil {
+		*errs = append(*errs, err)
+		return &pkgInfo{path: pkgPath, flags: pkgFlagIll}
+	}
+	exr, ok := p.runners[modRoot]
+	if !ok {
+		mod, err := gopmod.LoadMod(modVer)
+		if err != nil {
+			*errs = append(*errs, err)
+			return &pkgInfo{path: pkgPath, flags: pkgFlagIll}
+		}
+		exr = &Runner{
+			mod:     mod,
+			modRoot: modRoot,
+			modPath: modVer.Path,
+			modTime: modTime(mod.Modfile(), errs),
+			pkgs:    make(map[string]*pkgInfo),
+			runners: p.runners,
+		}
+		p.runners[modRoot] = exr
+	}
+	return exr.genDeps(pkgPath, pkgPath, errs)
+}
+
+func (p *Runner) sourceChanged(dir string, ci gopmod.ChangeInfo, depsHash *string) 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()) {
+		return true
+	}
+	//cinfo $SourceNum $DepsHash
+	var sourceNum int
+	if _, err = fmt.Fscanf(f, "//cinfo %d %s\n", &sourceNum, depsHash); err != nil {
+		return true
+	}
+	return sourceNum != ci.SourceNum
+}
+
+// -----------------------------------------------------------------------------
+
+type ErrorList []error
+
+func (e ErrorList) Error() string {
+	errStrs := make([]string, len(e))
+	for i, err := range e {
+		errStrs[i] = err.Error()
+	}
+	return strings.Join(errStrs, "\n")
+}
+
+// -----------------------------------------------------------------------------
diff --git a/x/gopmod/classfile.go b/x/gopmod/classfile.go
new file mode 100644
index 00000000..ecbb13fb
--- /dev/null
+++ b/x/gopmod/classfile.go
@@ -0,0 +1,110 @@
+/*
+ * 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 gopmod
+
+import (
+	"errors"
+	"syscall"
+
+	"github.com/goplus/gop/x/mod/modfetch"
+	"github.com/goplus/gop/x/mod/modfile"
+	"github.com/goplus/gop/x/mod/modload"
+	"golang.org/x/mod/module"
+)
+
+type Class = modfile.Classfile
+
+var (
+	ClassSpx = &Class{
+		ProjExt:  ".gmx",
+		WorkExt:  ".spx",
+		PkgPaths: []string{"github.com/goplus/spx", "math"},
+	}
+)
+
+var (
+	ErrNotClassFileMod = errors.New("not a classfile module")
+)
+
+// -----------------------------------------------------------------------------
+
+func (p *Module) LookupClass(ext string) (c *Class, ok bool) {
+	c, ok = p.classes[ext]
+	return
+}
+
+func (p *Module) RegisterClasses(registerClass ...func(c *Class)) (err error) {
+	var regcls func(c *Class)
+	if registerClass != nil {
+		regcls = registerClass[0]
+	}
+	p.registerClass(ClassSpx, regcls)
+	if c := p.Classfile; c != nil {
+		p.registerClass(c, regcls)
+	}
+	for _, r := range p.Register {
+		if err = p.registerMod(r.ClassfileMod, regcls); err != nil {
+			return
+		}
+	}
+	return
+}
+
+func (p *Module) registerMod(modPath string, regcls func(c *Class)) (err error) {
+	mod, ok := p.LookupMod(modPath)
+	if !ok {
+		return syscall.ENOENT
+	}
+	err = p.registerClassFrom(mod, regcls)
+	if err != syscall.ENOENT {
+		return
+	}
+	mod, _, err = modfetch.Get(mod.String())
+	if err != nil && err != syscall.EEXIST {
+		return
+	}
+	return p.registerClassFrom(mod, regcls)
+}
+
+func (p *Module) registerClassFrom(modVer module.Version, regcls func(c *Class)) (err error) {
+	dir, err := modfetch.ModCachePath(modVer)
+	if err != nil {
+		return
+	}
+	mod, err := modload.Load(dir)
+	if err != nil {
+		return
+	}
+	c := mod.Classfile
+	if c == nil {
+		return ErrNotClassFileMod
+	}
+	p.registerClass(c, regcls)
+	return
+}
+
+func (p *Module) registerClass(c *Class, regcls func(c *Class)) {
+	p.classes[c.ProjExt] = c
+	if c.WorkExt != "" {
+		p.classes[c.WorkExt] = c
+	}
+	if regcls != nil {
+		regcls(c)
+	}
+}
+
+// -----------------------------------------------------------------------------
diff --git a/x/gopmod/godeps.go b/x/gopmod/godeps.go
new file mode 100644
index 00000000..1305fc71
--- /dev/null
+++ b/x/gopmod/godeps.go
@@ -0,0 +1,66 @@
+/*
+ * 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 gopmod
+
+import (
+	"path"
+	"strconv"
+	"strings"
+
+	"go/ast"
+	"go/parser"
+	"go/token"
+)
+
+// -----------------------------------------------------------------------------
+
+type none = struct{}
+
+func (p *Module) parseGoImport(errs *ErrorList, imports map[string]none, gopfile string) {
+	f, err := parser.ParseFile(p.fset, gopfile, nil, parser.ImportsOnly)
+	if err != nil {
+		*errs = append(*errs, err)
+		return
+	}
+	for _, imp := range f.Imports {
+		p.importGo(imports, imp)
+	}
+}
+
+func (p *Module) importGo(imports map[string]none, spec *ast.ImportSpec) {
+	pkgPath := p.canonicalGo(goToString(spec.Path))
+	imports[pkgPath] = none{}
+}
+
+func (p *Module) canonicalGo(pkgPath string) string {
+	if strings.HasPrefix(pkgPath, ".") {
+		return path.Join(p.Path(), pkgPath)
+	}
+	return pkgPath
+}
+
+func goToString(l *ast.BasicLit) string {
+	if l.Kind == token.STRING {
+		s, err := strconv.Unquote(l.Value)
+		if err == nil {
+			return s
+		}
+	}
+	panic("TODO: toString - convert ast.BasicLit to string failed")
+}
+
+// -----------------------------------------------------------------------------
diff --git a/x/gopmod/gopdeps.go b/x/gopmod/gopdeps.go
new file mode 100644
index 00000000..21cdb55f
--- /dev/null
+++ b/x/gopmod/gopdeps.go
@@ -0,0 +1,66 @@
+/*
+ * 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 gopmod
+
+import (
+	"path"
+	"strconv"
+	"strings"
+
+	"github.com/goplus/gop/ast"
+	"github.com/goplus/gop/parser"
+	"github.com/goplus/gop/token"
+)
+
+// -----------------------------------------------------------------------------
+
+func (p *Module) parseGopImport(errs *ErrorList, imports map[string]none, gopfile string) {
+	f, err := parser.ParseFile(p.fset, gopfile, nil, parser.ImportsOnly)
+	if err != nil {
+		*errs = append(*errs, err)
+		return
+	}
+	for _, imp := range f.Imports {
+		p.importGop(imports, imp)
+	}
+}
+
+func (p *Module) importGop(imports map[string]none, spec *ast.ImportSpec) {
+	pkgPath := p.canonicalGop(gopToString(spec.Path))
+	imports[pkgPath] = none{}
+}
+
+func (p *Module) canonicalGop(pkgPath string) string {
+	if strings.HasPrefix(pkgPath, "gop/") {
+		return "github.com/goplus/" + pkgPath
+	} else if strings.HasPrefix(pkgPath, ".") {
+		return path.Join(p.Path(), pkgPath)
+	}
+	return pkgPath
+}
+
+func gopToString(l *ast.BasicLit) string {
+	if l.Kind == token.STRING {
+		s, err := strconv.Unquote(l.Value)
+		if err == nil {
+			return s
+		}
+	}
+	panic("TODO: toString - convert ast.BasicLit to string failed")
+}
+
+// -----------------------------------------------------------------------------
diff --git a/x/gopmod/module.go b/x/gopmod/module.go
new file mode 100644
index 00000000..8f526687
--- /dev/null
+++ b/x/gopmod/module.go
@@ -0,0 +1,296 @@
+/*
+ * 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 gopmod
+
+import (
+	"os"
+	"path/filepath"
+	"sort"
+	"strings"
+	"syscall"
+	"time"
+
+	"github.com/goplus/gop/token"
+	"github.com/goplus/gop/x/mod/modfetch"
+	"github.com/goplus/gop/x/mod/modload"
+	"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
+	}
+	mod, err := Load(dir)
+	if err != nil {
+		return
+	}
+	err = mod.RegisterClasses()
+	if err != nil {
+		return
+	}
+	return mod.Imports(dir, recursive)
+}
+
+// -----------------------------------------------------------------------------
+
+type depmodInfo struct {
+	path string
+	real module.Version
+}
+
+type Module struct {
+	modload.Module
+	classes map[string]*Class
+	depmods []depmodInfo
+	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
+
+const (
+	PkgtStandard PkgType = iota // a standard Go/Go+ package
+	PkgtModule                  // a package in this module (in standard form)
+	PkgtLocal                   // a package in this module (in relative path form)
+	PkgtExtern                  // an extarnal package
+	PkgtInvalid  = -1           // an invalid package
+)
+
+// PkgType returns the package type of specified package.
+func (p *Module) PkgType(pkgPath string) PkgType {
+	if pkgPath == "" {
+		return PkgtInvalid
+	}
+	if isPkgInMod(pkgPath, p.Path()) {
+		return PkgtModule
+	}
+	if pkgPath[0] == '.' {
+		return PkgtLocal
+	}
+	pos := strings.Index(pkgPath, "/")
+	if pos > 0 {
+		pkgPath = pkgPath[:pos]
+	}
+	if strings.Contains(pkgPath, ".") {
+		return PkgtExtern
+	}
+	return PkgtStandard
+}
+
+func isPkgInMod(pkgPath, modPath string) bool {
+	if strings.HasPrefix(pkgPath, modPath) {
+		suffix := pkgPath[len(modPath):]
+		return suffix == "" || suffix[0] == '/'
+	}
+	return false
+}
+
+// LookupExternPkg lookups a external package from depended modules.
+// If modVer.Path is replace to be a local path, it will be canonical to an absolute path.
+func (p *Module) LookupExternPkg(pkgPath string) (modPath string, modVer module.Version, ok bool) {
+	for _, m := range p.depmods {
+		if isPkgInMod(pkgPath, m.path) {
+			modPath, modVer, ok = m.path, m.real, true
+			break
+		}
+	}
+	return
+}
+
+// LookupMod 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) {
+	for _, m := range p.depmods {
+		if m.path == modPath {
+			modVer, ok = m.real, true
+			break
+		}
+	}
+	return
+}
+
+func getDepMods(mod modload.Module) []depmodInfo {
+	depmods := mod.DepMods()
+	ret := make([]depmodInfo, 0, len(depmods))
+	for path, m := range depmods {
+		ret = append(ret, depmodInfo{path: path, real: m})
+	}
+	sort.Slice(ret, func(i, j int) bool {
+		return ret[i].path > ret[j].path
+	})
+	return ret
+}
+
+func New(mod modload.Module) *Module {
+	classes := make(map[string]*Class)
+	depmods := getDepMods(mod)
+	fset := token.NewFileSet()
+	return &Module{classes: classes, depmods: depmods, Module: mod, fset: fset}
+}
+
+func Load(dir string) (*Module, error) {
+	mod, err := modload.Load(dir)
+	if err != nil {
+		return nil, err
+	}
+	return New(mod), nil
+}
+
+func LoadMod(mod module.Version) (p *Module, err error) {
+	p, err = loadModFrom(mod)
+	if err != syscall.ENOENT {
+		return
+	}
+	mod, _, err = modfetch.Get(mod.String())
+	if err != nil && err != syscall.EEXIST {
+		return
+	}
+	return loadModFrom(mod)
+}
+
+func loadModFrom(mod module.Version) (p *Module, err error) {
+	dir, err := modfetch.ModCachePath(mod)
+	if err != nil {
+		return
+	}
+	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)
+	return
+}
+
+func getKeys(v map[string]none) []string {
+	keys := make([]string, 0, len(v))
+	for key := range v {
+		keys = append(keys, key)
+	}
+	return keys
+}
+
+func (p *Module) parseImports(imports map[string]none, dir string, recursive bool) (err error) {
+	list, err := os.ReadDir(dir)
+	if err != nil {
+		return
+	}
+	var errs ErrorList
+	for _, d := range list {
+		fname := d.Name()
+		if strings.HasPrefix(fname, "_") { // skip this file/directory
+			continue
+		}
+		if d.IsDir() {
+			if recursive {
+				p.parseImports(imports, filepath.Join(dir, fname), true)
+			}
+			continue
+		}
+		ext := filepath.Ext(fname)
+		switch ext {
+		case ".gop":
+			p.parseGopImport(&errs, imports, filepath.Join(dir, fname))
+		case ".go":
+			if !strings.HasPrefix(fname, "gop_autogen") {
+				p.parseGoImport(&errs, imports, filepath.Join(dir, fname))
+			}
+		default:
+			if c, ok := p.classes[ext]; ok {
+				for _, pkgPath := range c.PkgPaths {
+					imports[pkgPath] = none{}
+				}
+				p.parseGopImport(&errs, imports, filepath.Join(dir, fname))
+			}
+		}
+	}
+	if len(errs) > 0 {
+		err = errs
+	}
+	return
+}
+
+type ChangeInfo struct {
+	ModTime    time.Time
+	GopFileNum int
+	SourceNum  int
+}
+
+func (p *Module) ChangeInfo(dir string) (ci ChangeInfo, err error) {
+	list, err := os.ReadDir(dir)
+	if err != nil {
+		return
+	}
+	dir += "/"
+	var fi os.FileInfo
+	for _, d := range list {
+		fname := d.Name()
+		if strings.HasPrefix(fname, "_") || d.IsDir() {
+			continue
+		}
+		ext := filepath.Ext(fname)
+		switch ext {
+		case ".gop":
+			ci.GopFileNum++
+		case ".go":
+			if strings.HasPrefix(fname, "gop_autogen") {
+				continue
+			}
+		default:
+			if _, ok := p.classes[ext]; !ok {
+				continue
+			}
+			ci.GopFileNum++
+		}
+		ci.SourceNum++
+		fi, err = os.Stat(dir + fname)
+		if err != nil {
+			return
+		}
+		t := fi.ModTime()
+		if t.After(ci.ModTime) {
+			ci.ModTime = t
+		}
+	}
+	return
+}
+
+// -----------------------------------------------------------------------------
+
+type ErrorList []error
+
+func (e ErrorList) Error() string {
+	errStrs := make([]string, len(e))
+	for i, err := range e {
+		errStrs[i] = err.Error()
+	}
+	return strings.Join(errStrs, "\n")
+}
+
+// -----------------------------------------------------------------------------
diff --git a/x/gopmod/gocmd.go b/x/gopproj/gocmd.go
similarity index 94%
rename from x/gopmod/gocmd.go
rename to x/gopproj/gocmd.go
index 7aa79f0a..b73c8c8a 100644
--- a/x/gopmod/gocmd.go
+++ b/x/gopproj/gocmd.go
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package gopmod
+package gopproj
 
 import (
 	"fmt"
@@ -38,13 +38,15 @@ const (
 )
 
 var (
-	GOPVERSION   = env.Version()
-	GOPBUILDDATE = env.BuildDate()
-	GOPROOT      = env.GOPROOT()
+	ldFlags = fmt.Sprintf(ldFlagAll, env.Version(), env.BuildDate(), GOPROOT)
+)
+
+var (
+	GOPROOT = env.GOPROOT()
 )
 
 func LoadFlags() string {
-	return fmt.Sprintf(ldFlagAll, GOPVERSION, GOPBUILDDATE, GOPROOT)
+	return ldFlags
 }
 
 // -----------------------------------------------------------------------------
diff --git a/x/gopmod/gofile.go b/x/gopproj/gofile.go
similarity index 99%
rename from x/gopmod/gofile.go
rename to x/gopproj/gofile.go
index 9cbd6eed..d65fcc58 100644
--- a/x/gopmod/gofile.go
+++ b/x/gopproj/gofile.go
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package gopmod
+package gopproj
 
 import (
 	"crypto/sha1"
diff --git a/x/gopmod/gopfiles.go b/x/gopproj/gopfiles.go
similarity index 92%
rename from x/gopmod/gopfiles.go
rename to x/gopproj/gopfiles.go
index 961bf512..b2e51548 100644
--- a/x/gopmod/gopfiles.go
+++ b/x/gopproj/gopfiles.go
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package gopmod
+package gopproj
 
 import (
 	"bytes"
@@ -117,18 +117,12 @@ func (p *gopFiles) GenGo(outFile, modFile string) error {
 
 	srcDir, _ := filepath.Split(outFile)
 	modDir, _ := filepath.Split(modFile)
-	conf := &cl.Config{
-		Dir: modDir, TargetDir: srcDir, Fset: fset, CacheLoadPkgs: true, PersistLoadPkgs: true}
+	conf := &cl.Config{WorkingDir: modDir, TargetDir: srcDir, Fset: fset}
 	out, err := cl.NewPackage("", mainPkg, conf)
 	if err != nil {
 		return err
 	}
-	err = gox.WriteFile(outFile, out, false)
-	if err != nil {
-		return err
-	}
-	conf.PkgsLoader.Save()
-	return nil
+	return gox.WriteFile(outFile, out, false)
 }
 
 // -----------------------------------------------------------------------------
diff --git a/x/gopmod/gopproj.go b/x/gopproj/gopproj.go
similarity index 87%
rename from x/gopmod/gopproj.go
rename to x/gopproj/gopproj.go
index 432a9ead..c8005717 100644
--- a/x/gopmod/gopproj.go
+++ b/x/gopproj/gopproj.go
@@ -14,12 +14,12 @@
  * limitations under the License.
  */
 
-package gopmod
+package gopproj
 
 import (
 	"path/filepath"
 
-	"github.com/goplus/gop/x/gopproj"
+	"github.com/goplus/gop/x/gopprojs"
 )
 
 // -----------------------------------------------------------------------------
@@ -28,13 +28,13 @@ const (
 	FlagGoAsGoPlus = 1 << iota
 )
 
-func (p *Context) OpenProject(flags int, src gopproj.Proj) (proj *Project, err error) {
+func (p *Context) OpenProject(flags int, src gopprojs.Proj) (proj *Project, err error) {
 	switch v := src.(type) {
-	case *gopproj.FilesProj:
+	case *gopprojs.FilesProj:
 		return p.OpenFiles(flags, v.Files...)
-	case *gopproj.DirProj:
+	case *gopprojs.DirProj:
 		return p.OpenDir(flags, v.Dir)
-	case *gopproj.PkgPathProj:
+	case *gopprojs.PkgPathProj:
 		return p.OpenPkgPath(flags, v.Path)
 	}
 	panic("OpenProject: unexpected source")
diff --git a/x/gopmod/gopmod.go b/x/gopproj/projctx.go
similarity index 98%
rename from x/gopmod/gopmod.go
rename to x/gopproj/projctx.go
index 08e9baaf..1262007f 100644
--- a/x/gopmod/gopmod.go
+++ b/x/gopproj/projctx.go
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package gopmod
+package gopproj
 
 import (
 	"bytes"
@@ -172,7 +172,7 @@ func genGomodFile(modfile string) {
 		}
 		gopRoot = filepath.ToSlash(gopRoot)
 	}
-	fmt.Fprintf(&buf, gomodFormat, GOPVERSION, gopRoot)
+	fmt.Fprintf(&buf, gomodFormat, env.Version(), gopRoot)
 	err = os.WriteFile(modfile, buf.Bytes(), 0644)
 	if err != nil {
 		log.Panicln(err)
diff --git a/x/gopproj/proj.go b/x/gopprojs/proj.go
similarity index 71%
rename from x/gopproj/proj.go
rename to x/gopprojs/proj.go
index 488a80c5..1144dbb1 100644
--- a/x/gopproj/proj.go
+++ b/x/gopprojs/proj.go
@@ -1,4 +1,20 @@
-package gopproj
+/*
+ * 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 gopprojs
 
 import (
 	"errors"
diff --git a/x/mod/internal/lazyregexp/lazyregexp.go b/x/mod/internal/lazyregexp/lazyregexp.go
deleted file mode 100644
index 7f386635..00000000
--- a/x/mod/internal/lazyregexp/lazyregexp.go
+++ /dev/null
@@ -1,90 +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 lazyregexp is a thin wrapper over regexp, allowing the use of global
-// regexp variables without forcing them to be compiled at init.
-package lazyregexp
-
-import (
-	"os"
-	"regexp"
-	"strings"
-	"sync"
-)
-
-// Regexp is a wrapper around regexp.Regexp, where the underlying regexp will be
-// compiled the first time it is needed.
-type Regexp struct {
-	str  string
-	once sync.Once
-	rx   *regexp.Regexp
-}
-
-func (r *Regexp) re() *regexp.Regexp {
-	r.once.Do(r.build)
-	return r.rx
-}
-
-func (r *Regexp) build() {
-	r.rx = regexp.MustCompile(r.str)
-	r.str = ""
-}
-
-func (r *Regexp) FindSubmatch(s []byte) [][]byte {
-	return r.re().FindSubmatch(s)
-}
-
-func (r *Regexp) FindStringSubmatch(s string) []string {
-	return r.re().FindStringSubmatch(s)
-}
-
-func (r *Regexp) FindStringSubmatchIndex(s string) []int {
-	return r.re().FindStringSubmatchIndex(s)
-}
-
-func (r *Regexp) ReplaceAllString(src, repl string) string {
-	return r.re().ReplaceAllString(src, repl)
-}
-
-func (r *Regexp) FindString(s string) string {
-	return r.re().FindString(s)
-}
-
-func (r *Regexp) FindAllString(s string, n int) []string {
-	return r.re().FindAllString(s, n)
-}
-
-func (r *Regexp) MatchString(s string) bool {
-	return r.re().MatchString(s)
-}
-
-func (r *Regexp) SubexpNames() []string {
-	return r.re().SubexpNames()
-}
-
-var inTest = len(os.Args) > 0 && strings.HasSuffix(strings.TrimSuffix(os.Args[0], ".exe"), ".test")
-
-// New creates a new lazy regexp, delaying the compiling work until it is first
-// needed. If the code is being run as part of tests, the regexp compiling will
-// happen immediately.
-func New(str string) *Regexp {
-	lr := &Regexp{str: str}
-	if inTest {
-		// In tests, always compile the regexps early.
-		lr.re()
-	}
-	return lr
-}
diff --git a/x/mod/modfetch/cache.go b/x/mod/modfetch/cache.go
index 2959b699..d973ac75 100644
--- a/x/mod/modfetch/cache.go
+++ b/x/mod/modfetch/cache.go
@@ -13,44 +13,47 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 package modfetch
 
 import (
-	"fmt"
+	"errors"
 	"path/filepath"
 
-	"golang.org/x/mod/module"
-	"golang.org/x/mod/semver"
-
 	"github.com/goplus/gop/env"
+	"golang.org/x/mod/module"
 )
 
+// -----------------------------------------------------------------------------
+
 var (
 	GOMODCACHE = env.GOMODCACHE()
 )
 
-func cacheDir(path string) (string, error) {
-	enc, err := module.EscapePath(path)
+var (
+	ErrNoNeedToDownload = errors.New("no need to download")
+)
+
+func DownloadCachePath(mod module.Version) (string, error) {
+	if mod.Version == "" {
+		return mod.Path, ErrNoNeedToDownload
+	}
+	encPath, err := module.EscapePath(mod.Path)
 	if err != nil {
 		return "", err
 	}
-	return filepath.Join(GOMODCACHE, "cache/download", enc, "/@v"), nil
+	return filepath.Join(GOMODCACHE, "cache/download", encPath, "@v", mod.Version+".zip"), nil
 }
 
-func CachePath(m module.Version, suffix string) (string, error) {
-	dir, err := cacheDir(m.Path)
-	if err != nil {
-		return "", err
+func ModCachePath(mod module.Version) (string, error) {
+	if mod.Version == "" {
+		return mod.Path, nil
 	}
-	if !semver.IsValid(m.Version) {
-		return "", fmt.Errorf("non-semver module version %q", m.Version)
-	}
-	if module.CanonicalVersion(m.Version) != m.Version {
-		return "", fmt.Errorf("non-canonical module version %q", m.Version)
-	}
-	encVer, err := module.EscapeVersion(m.Version)
+	encPath, err := module.EscapePath(mod.Path)
 	if err != nil {
 		return "", err
 	}
-	return filepath.Join(dir, encVer+"."+suffix), nil
+	return filepath.Join(GOMODCACHE, encPath+"@"+mod.Version), nil
 }
+
+// -----------------------------------------------------------------------------
diff --git a/x/mod/modfetch/fetch.go b/x/mod/modfetch/fetch.go
index fadb515c..7d2a30b5 100644
--- a/x/mod/modfetch/fetch.go
+++ b/x/mod/modfetch/fetch.go
@@ -13,131 +13,80 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 package modfetch
 
 import (
-	"errors"
-	"fmt"
-	"io/fs"
-	"log"
+	"bytes"
 	"os"
 	"os/exec"
-	"path/filepath"
+	"strings"
+	"syscall"
 
+	"github.com/goplus/gop/x/mod/modload"
 	"golang.org/x/mod/module"
-	"golang.org/x/mod/semver"
 )
 
-// downloadDir returns the directory to which m should have been downloaded.
-// An error will be returned if the module path or version cannot be escaped.
-// An error satisfying errors.Is(err, fs.ErrNotExist) will be returned
-// along with the directory if the directory does not exist or if the directory
-// is not completely populated.
-func downloadDir(m module.Version) (string, error) {
-	enc, err := module.EscapePath(m.Path)
-	if err != nil {
-		return "", err
-	}
-	if !semver.IsValid(m.Version) {
-		return "", fmt.Errorf("non-semver module version %q", m.Version)
-	}
-	if module.CanonicalVersion(m.Version) != m.Version {
-		return "", fmt.Errorf("non-canonical module version %q", m.Version)
-	}
-	encVer, err := module.EscapeVersion(m.Version)
-	if err != nil {
-		return "", err
-	}
+// -----------------------------------------------------------------------------
 
-	// Check whether the directory itself exists.
-	dir := filepath.Join(GOMODCACHE, enc+"@"+encVer)
-	if fi, err := os.Stat(dir); os.IsNotExist(err) {
-		return dir, err
-	} else if err != nil {
-		return dir, &DownloadDirPartialError{dir, err}
-	} else if !fi.IsDir() {
-		return dir, &DownloadDirPartialError{dir, errors.New("not a directory")}
-	}
+type (
+	ExecCmdError = modload.ExecCmdError
+)
 
-	// Check if a .partial file exists. This is created at the beginning of
-	// a download and removed after the zip is extracted.
-	partialPath, err := CachePath(m, "partial")
+func Get(modPath string) (mod module.Version, isClass bool, err error) {
+	var stdout, stderr bytes.Buffer
+	cmd := exec.Command("go", "get", modPath)
+	cmd.Stdout = &stdout
+	cmd.Stderr = &stderr
+	err = cmd.Run()
 	if err != nil {
-		return dir, err
-	}
-	if _, err := os.Stat(partialPath); err == nil {
-		return dir, &DownloadDirPartialError{dir, errors.New("not completely extracted")}
-	} else if !os.IsNotExist(err) {
-		return dir, err
+		err = &ExecCmdError{Err: err, Stderr: stderr.Bytes()}
+		return
 	}
+	return getResult(stderr.String())
+}
 
-	// Check if a .ziphash file exists. It should be created before the
-	// zip is extracted, but if it was deleted (by another program?), we need
-	// to re-calculate it.
-	ziphashPath, err := CachePath(m, "ziphash")
-	if err != nil {
-		return dir, err
+func getResult(data string) (mod module.Version, isClass bool, err error) {
+	if data == "" {
+		err = syscall.EEXIST
+		return
 	}
-	if _, err := os.Stat(ziphashPath); os.IsNotExist(err) {
-		return dir, &DownloadDirPartialError{dir, errors.New("ziphash file is missing")}
-	} else if err != nil {
-		return dir, err
+	// go: downloading github.com/xushiwei/foogop v0.1.0
+	const downloading = "go: downloading "
+	if strings.HasPrefix(data, downloading) {
+		return tryConvGoMod(data[len(downloading):], &data)
 	}
-	return dir, nil
-}
-
-func Download(m module.Version) (dir string, err error) {
-	dir, err = downloadDir(m)
-	if err == nil {
-		// The directory has already been completely extracted (no .partial file exists).
-		return dir, nil
-	} else if dir == "" || !errors.Is(err, fs.ErrNotExist) {
-		return "", err
+	// go get: added github.com/xushiwei/foogop v0.1.0
+	const added = "go get: added "
+	if strings.HasPrefix(data, added) {
+		return tryConvGoMod(data[len(added):], &data)
 	}
-
-	DownloadArgs(dir, m.String())
-	return dir, nil
-}
-
-func DownloadArgs(dir string, args ...string) {
-	runCmd(dir, "go", append([]string{"mod", "download"}, args...)...)
-}
-
-func TidyArgs(dir string, args ...string) {
-	runCmd(dir, "go", append([]string{"mod", "tidy"}, args...)...)
-}
-
-func InitArgs(dir string, args ...string) {
-	runCmd(dir, "go", append([]string{"mod", "init"}, args...)...)
+	return
 }
 
-// runCmd executes a command tool.
-func runCmd(dir string, name string, args ...string) {
-	cmd := exec.Command(name, args...)
-	cmd.Dir = dir
-	cmd.Stdin = os.Stdin
-	cmd.Stdout = os.Stdout
-	cmd.Stderr = os.Stderr
-	cmd.Env = os.Environ()
-	err := cmd.Run()
-	if err != nil {
-		switch e := err.(type) {
-		case *exec.ExitError:
-			os.Exit(e.ExitCode())
-		default:
-			log.Fatalln("RunGoCmd failed:", err)
+func tryConvGoMod(data string, next *string) (mod module.Version, isClass bool, err error) {
+	err = syscall.ENOENT
+	if pos := strings.IndexByte(data, '\n'); pos > 0 {
+		line := data[:pos]
+		*next = data[pos+1:]
+		if pos = strings.IndexByte(line, ' '); pos > 0 {
+			mod.Path, mod.Version = line[:pos], line[pos+1:]
+			if dir, e := ModCachePath(mod); e == nil {
+				isClass, err = convGoMod(dir)
+			}
 		}
 	}
+	return
 }
 
-// DownloadDirPartialError is returned by DownloadDir if a module directory
-// exists but was not completely populated.
-//
-// DownloadDirPartialError is equivalent to fs.ErrNotExist.
-type DownloadDirPartialError struct {
-	Dir string
-	Err error
+func convGoMod(dir string) (isClass bool, err error) {
+	mod, err := modload.Load(dir)
+	if err != nil {
+		return
+	}
+	os.Chmod(dir, 0755)
+	defer os.Chmod(dir, 0555)
+	return mod.Classfile != nil, mod.UpdateGoMod(true)
 }
 
-func (e *DownloadDirPartialError) Error() string     { return fmt.Sprintf("%s: %v", e.Dir, e.Err) }
-func (e *DownloadDirPartialError) Is(err error) bool { return err == fs.ErrNotExist }
+// -----------------------------------------------------------------------------
diff --git a/x/mod/modfetch/file.go b/x/mod/modfetch/file.go
deleted file mode 100644
index f20b9dde..00000000
--- a/x/mod/modfetch/file.go
+++ /dev/null
@@ -1,106 +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 modfetch
-
-import (
-	"io"
-	"os"
-)
-
-// Edit creates the named file with mode 0644 (before umask),
-// but does not truncate existing contents.
-//
-// If Edit succeeds, methods on the returned File can be used for I/O.
-// The associated file descriptor has mode O_RDWR and the file is write-locked.
-func Edit(name string) (*os.File, error) {
-	return os.OpenFile(name, os.O_RDWR|os.O_CREATE, 0644)
-}
-
-// Transform invokes t with the result of reading the named file, with its lock
-// still held.
-//
-// If t returns a nil error, Transform then writes the returned contents back to
-// the file, making a best effort to preserve existing contents on error.
-//
-// t must not modify the slice passed to it.
-func Transform(name string, t func([]byte) ([]byte, error)) (err error) {
-	f, err := Edit(name)
-	if err != nil {
-		return err
-	}
-	defer f.Close()
-
-	old, err := io.ReadAll(f)
-	if err != nil {
-		return err
-	}
-
-	new, err := t(old)
-	if err != nil {
-		return err
-	}
-
-	if len(new) > len(old) {
-		// The overall file size is increasing, so write the tail first: if we're
-		// about to run out of space on the disk, we would rather detect that
-		// failure before we have overwritten the original contents.
-		if _, err := f.WriteAt(new[len(old):], int64(len(old))); err != nil {
-			// Make a best effort to remove the incomplete tail.
-			f.Truncate(int64(len(old)))
-			return err
-		}
-	}
-
-	// We're about to overwrite the old contents. In case of failure, make a best
-	// effort to roll back before we close the file.
-	defer func() {
-		if err != nil {
-			if _, err := f.WriteAt(old, 0); err == nil {
-				f.Truncate(int64(len(old)))
-			}
-		}
-	}()
-
-	if len(new) >= len(old) {
-		if _, err := f.WriteAt(new[:len(old)], 0); err != nil {
-			return err
-		}
-	} else {
-		if _, err := f.WriteAt(new, 0); err != nil {
-			return err
-		}
-		// The overall file size is decreasing, so shrink the file to its final size
-		// after writing. We do this after writing (instead of before) so that if
-		// the write fails, enough filesystem space will likely still be reserved
-		// to contain the previous contents.
-		if err := f.Truncate(int64(len(new))); err != nil {
-			return err
-		}
-	}
-
-	return nil
-}
-
-// Read opens the named file and returns its contents.
-func Read(name string) ([]byte, error) {
-	f, err := os.Open(name)
-	if err != nil {
-		return nil, err
-	}
-	defer f.Close()
-
-	return io.ReadAll(f)
-}
diff --git a/x/mod/modfile/rule.go b/x/mod/modfile/rule.go
index bea02c19..f1f2f092 100644
--- a/x/mod/modfile/rule.go
+++ b/x/mod/modfile/rule.go
@@ -214,6 +214,18 @@ 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 {
@@ -324,7 +336,7 @@ func getWeight(e Expr) int {
 	return directiveLineBlock
 }
 
-func updateLine(x *FileSyntax, line *Line, tokens ...string) {
+func updateLine(line *Line, tokens ...string) {
 	if line.InBlock {
 		tokens = tokens[1:]
 	}
@@ -358,9 +370,50 @@ func (f *File) AddGopStmt(version string) error {
 		}
 	} else {
 		f.Gop.Version = version
-		updateLine(f.Syntax, f.Gop.Syntax, "gop", version)
+		updateLine(f.Gop.Syntax, "gop", version)
 	}
 	return nil
 }
 
+func (f *File) AddRegister(modPath string) {
+	for _, r := range f.Register {
+		if r.ClassfileMod == modPath {
+			return
+		}
+	}
+	f.AddNewRegister(modPath)
+}
+
+func (f *File) AddNewRegister(modPath string) {
+	line := addLine(f.Syntax, "register", AutoQuote(modPath))
+	r := &Register{
+		ClassfileMod: modPath,
+		Syntax:       line,
+	}
+	f.Register = append(f.Register, r)
+}
+
+// -----------------------------------------------------------------------------
+
+// markRemoved modifies line so that it (and its end-of-line comment, if any)
+// will be dropped by (*FileSyntax).Cleanup.
+func markRemoved(line *Line) {
+	line.Token = nil
+	line.Comments.Suffix = nil
+}
+
+func (f *File) DropAllRequire() {
+	for _, r := range f.Require {
+		markRemoved(r.Syntax)
+	}
+	f.Require = nil
+}
+
+func (f *File) DropAllReplace() {
+	for _, r := range f.Replace {
+		markRemoved(r.Syntax)
+	}
+	f.Replace = nil
+}
+
 // -----------------------------------------------------------------------------
diff --git a/x/mod/modload/module.go b/x/mod/modload/module.go
new file mode 100644
index 00000000..eeeac15c
--- /dev/null
+++ b/x/mod/modload/module.go
@@ -0,0 +1,340 @@
+/*
+ * 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 modload
+
+import (
+	"bytes"
+	"errors"
+	"fmt"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"strings"
+
+	gomodfile "golang.org/x/mod/modfile"
+	"golang.org/x/mod/module"
+
+	"github.com/goplus/gop/env"
+	"github.com/goplus/gop/x/mod/modfile"
+)
+
+var (
+	ErrNoModDecl = errors.New("no module declaration in gop.mod (or go.mod)")
+	ErrNoModRoot = errors.New("gop.mod or go.mod file not found in current directory or any parent directory")
+)
+
+type Module struct {
+	*modfile.File
+}
+
+// Modfile returns absolute path of the module file (gop.mod or go.mod).
+func (p Module) Modfile() string {
+	return p.Syntax.Name
+}
+
+// Path returns the module path.
+func (p Module) Path() string {
+	if mod := p.Module; mod != nil {
+		return mod.Mod.Path
+	}
+	return ""
+}
+
+// DepMods returns all depended modules.
+// If a depended module path is replace to be a local path, it will be canonical to an absolute path.
+func (p Module) DepMods() map[string]module.Version {
+	vers := make(map[string]module.Version)
+	for _, r := range p.Require {
+		if r.Mod.Path != "" {
+			vers[r.Mod.Path] = r.Mod
+		}
+	}
+	for _, r := range p.Replace {
+		if r.Old.Path != "" {
+			real := r.New
+			if real.Version == "" {
+				if strings.HasPrefix(real.Path, ".") {
+					dir, _ := filepath.Split(p.Modfile())
+					real.Path = dir + real.Path
+				}
+				if a, err := filepath.Abs(real.Path); err == nil {
+					real.Path = a
+				}
+			}
+			vers[r.Old.Path] = real
+		}
+	}
+	return vers
+}
+
+// Create creates a new module in `dir`.
+// You should call `Save` manually to save this module.
+func Create(dir string, modPath, goVer, gopVer string) (p Module, err error) {
+	gopmod, err := filepath.Abs(filepath.Join(dir, "gop.mod"))
+	if err != nil {
+		return
+	}
+	if _, err := os.Stat(gopmod); err == nil {
+		return Module{}, fmt.Errorf("gop: %s already exists", gopmod)
+	}
+	mod := new(modfile.File)
+	mod.AddModuleStmt(modPath)
+	mod.AddGoStmt(goVer)
+	mod.AddGopStmt(gopVer)
+	mod.Syntax.Name = gopmod
+	return Module{File: mod}, nil
+}
+
+// fixVersion returns a modfile.VersionFixer implemented using the Query function.
+//
+// It resolves commit hashes and branch names to versions,
+// canonicalizes versions that appeared in early vgo drafts,
+// and does nothing for versions that already appear to be canonical.
+//
+// The VersionFixer sets 'fixed' if it ever returns a non-canonical version.
+func fixVersion(fixed *bool) modfile.VersionFixer {
+	return func(path, vers string) (resolved string, err error) {
+		// do nothing
+		return vers, nil
+	}
+}
+
+// Load loads a module from `dir`.
+func Load(dir string) (p Module, err error) {
+	gopmod, err := env.GOPMOD(dir)
+	if err != nil {
+		return
+	}
+
+	data, err := os.ReadFile(gopmod)
+	if err != nil {
+		return
+	}
+
+	var fixed bool
+	f, err := modfile.Parse(gopmod, data, fixVersion(&fixed))
+	if err != nil {
+		// Errors returned by modfile.Parse begin with file:line.
+		return
+	}
+	if f.Module == nil {
+		// No module declaration. Must add module path.
+		return Module{}, ErrNoModDecl
+	}
+	return Module{File: f}, nil
+}
+
+func loadGoMod(gomod string) (f *gomodfile.File, err error) {
+	data, err := os.ReadFile(gomod)
+	if err != nil {
+		return
+	}
+	var fixed bool
+	return gomodfile.Parse(gomod, data, fixVersion(&fixed))
+}
+
+// -----------------------------------------------------------------------------
+
+// Save saves all changes of this module.
+func (p Module) Save() error {
+	modfile := p.Modfile()
+	data, err := p.Format()
+	if err == nil {
+		err = os.WriteFile(modfile, data, 0644)
+	}
+	return err
+}
+
+const (
+	gopMod = "github.com/goplus/gop"
+)
+
+// Tidy adds missing and removes unused modules.
+// Note: please call GenGo before Tidy.
+func (p Module) Tidy() (err error) {
+	gopmod := p.Modfile()
+	dir, file := filepath.Split(gopmod)
+	gomod := dir + "go.mod"
+	isGopMod := file != "go.mod"
+	if isGopMod {
+		if err = p.saveGoMod(gomod); err != nil {
+			return
+		}
+	}
+	if err = tidy(dir); err != nil {
+		return
+	}
+	if isGopMod {
+		gof, loadErr := loadGoMod(gomod)
+		if loadErr != nil {
+			return loadErr
+		}
+		p.DropAllRequire()
+		p.DropAllReplace()
+		for _, r := range gof.Require {
+			switch r.Mod.Path {
+			case gopMod, "":
+				continue
+			}
+			p.AddRequire(r.Mod.Path, r.Mod.Version)
+		}
+		for _, r := range gof.Replace {
+			switch r.Old.Path {
+			case gopMod, "":
+				continue
+			}
+			p.AddReplace(r.Old.Path, r.Old.Version, r.New.Path, r.New.Version)
+		}
+		err = p.Save()
+		if err == nil {
+			err = p.UpdateGoMod(false)
+		}
+	}
+	return
+}
+
+func tidy(modRoot string) (err error) {
+	var stdout, stderr bytes.Buffer
+	cmd := exec.Command("go", "mod", "tidy")
+	cmd.Stdout = &stdout
+	cmd.Stderr = &stderr
+	cmd.Dir = modRoot
+	err = cmd.Run()
+	if err != nil {
+		err = &ExecCmdError{Err: err, Stderr: stderr.Bytes()}
+	}
+	msg := strings.ReplaceAll(stderr.String(), "go: ", "gop: ")
+	os.Stdout.WriteString(msg)
+	return
+}
+
+// UpdateGoMod updates the go.mod file.
+func (p Module) UpdateGoMod(checkChanged bool) error {
+	gopmod := p.Modfile()
+	dir, file := filepath.Split(gopmod)
+	if file == "go.mod" {
+		return nil
+	}
+	gomod := dir + "go.mod"
+	if checkChanged && notChanged(gomod, gopmod) {
+		return nil
+	}
+	return p.saveGoMod(gomod)
+}
+
+func (p Module) saveGoMod(gomod string) error {
+	gof := p.convToGoMod()
+	data, err := gof.Format()
+	if err == nil {
+		err = os.WriteFile(gomod, data, 0644)
+	}
+	return err
+}
+
+func (p Module) convToGoMod() *gomodfile.File {
+	copy := p.File.File
+	copy.Syntax = cloneGoFileSyntax(copy.Syntax)
+	addRequireIfNotExist(&copy, gopMod, env.Version())
+	addReplaceIfNotExist(&copy, gopMod, "", env.GOPROOT(), "")
+	return &copy
+}
+
+func addRequireIfNotExist(f *gomodfile.File, path, vers string) {
+	for _, r := range f.Require {
+		if r.Mod.Path == path {
+			return
+		}
+	}
+	f.AddNewRequire(path, vers, false)
+}
+
+func addReplaceIfNotExist(f *gomodfile.File, oldPath, oldVers, newPath, newVers string) {
+	for _, r := range f.Replace {
+		if r.Old.Path == oldPath && (oldVers == "" || r.Old.Version == oldVers) {
+			return
+		}
+	}
+	f.AddReplace(oldPath, oldVers, newPath, newVers)
+}
+
+func notChanged(target, src string) bool {
+	fiTarget, err := os.Stat(target)
+	if err != nil {
+		return false
+	}
+	fiSrc, err := os.Stat(src)
+	if err != nil {
+		return false
+	}
+	return fiTarget.ModTime().After(fiSrc.ModTime())
+}
+
+// -----------------------------------------------------------------------------
+
+func cloneGoFileSyntax(syn *modfile.FileSyntax) *modfile.FileSyntax {
+	stmt := make([]modfile.Expr, 0, len(syn.Stmt))
+	for _, e := range syn.Stmt {
+		if isGopExpr(e) {
+			continue
+		}
+		stmt = append(stmt, cloneExpr(e))
+	}
+	return &modfile.FileSyntax{
+		Name:     syn.Name,
+		Comments: syn.Comments,
+		Stmt:     stmt,
+	}
+}
+
+func cloneExpr(e modfile.Expr) modfile.Expr {
+	if v, ok := e.(*modfile.LineBlock); ok {
+		copy := *v
+		return &copy
+	}
+	return e
+}
+
+func isGopExpr(e modfile.Expr) bool {
+	switch verb := getVerb(e); verb {
+	case "gop", "register", "classfile":
+		return true
+	}
+	return false
+}
+
+func getVerb(e modfile.Expr) string {
+	if line, ok := e.(*modfile.Line); ok {
+		return line.Token[0]
+	}
+	return e.(*modfile.LineBlock).Token[0]
+}
+
+// -----------------------------------------------------------------------------
+
+type ExecCmdError struct {
+	Err    error
+	Stderr []byte
+}
+
+func (p *ExecCmdError) Error() string {
+	if e := p.Stderr; e != nil {
+		return string(e)
+	}
+	return p.Err.Error()
+}
+
+// -----------------------------------------------------------------------------


Test patch:
diff --git a/cl/builtin_test.go b/cl/builtin_test.go
index 045c08e6..ede759c1 100644
--- a/cl/builtin_test.go
+++ b/cl/builtin_test.go
@@ -23,15 +23,37 @@ import (
 	"github.com/goplus/gop/ast"
 	"github.com/goplus/gop/token"
 	"github.com/goplus/gox"
+	"github.com/goplus/gox/packages"
 )
 
+var (
+	goxConf = getGoxConf()
+)
+
+func getGoxConf() *gox.Config {
+	fset := token.NewFileSet()
+	conf := &packages.Config{
+		ModPath: "github.com/goplus/gop/cl",
+		Loaded:  make(map[string]*types.Package),
+		Fset:    fset,
+	}
+	const (
+		pkgspx2 = "github.com/goplus/gop/cl/internal/spx2"
+	)
+	imp, _, err := packages.NewImporter(conf, ".", pkgspx2)
+	if err != nil {
+		panic(err)
+	}
+	return &gox.Config{Fset: fset, Importer: imp}
+}
+
 func TestCompileErrWrapExpr(t *testing.T) {
 	defer func() {
 		if e := recover(); e != "TODO: can't use expr? in global" {
 			t.Fatal("TestCompileErrWrapExpr failed")
 		}
 	}()
-	pkg := gox.NewPackage("", "foo", nil)
+	pkg := gox.NewPackage("", "foo", goxConf)
 	ctx := &blockCtx{pkg: pkg, cb: pkg.CB()}
 	compileErrWrapExpr(ctx, &ast.ErrWrapExpr{Tok: token.QUESTION})
 }
@@ -84,7 +106,7 @@ func TestCanAutoCall(t *testing.T) {
 // -----------------------------------------------------------------------------
 
 func TestGmxSettings(t *testing.T) {
-	pkg := gox.NewPackage("", "foo", nil)
+	pkg := gox.NewPackage("", "foo", goxConf)
 	gmx := newGmx(pkg, "main.t2gmx")
 	scheds := gmx.getScheds(pkg.CB())
 	if len(scheds) != 2 || scheds[0] == nil || scheds[0] != scheds[1] {
diff --git a/cl/compile_spx_test.go b/cl/compile_spx_test.go
index 032ea2f0..c9c86688 100644
--- a/cl/compile_spx_test.go
+++ b/cl/compile_spx_test.go
@@ -40,6 +40,7 @@ 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 gopSpxTest(t *testing.T, gmx, spxcode, expected string) {
@@ -56,9 +57,8 @@ func gopSpxTestEx(t *testing.T, gmx, spxcode, expected, gmxfile, spxfile string)
 		scanner.PrintError(os.Stderr, err)
 		t.Fatal("ParseFSDir:", err)
 	}
-	conf := *baseConf.Ensure()
 	bar := pkgs["main"]
-	pkg, err := cl.NewPackage("", bar, &conf)
+	pkg, err := cl.NewPackage("", bar, gblConf)
 	if err != nil {
 		t.Fatal("NewPackage:", err)
 	}
@@ -80,7 +80,7 @@ func gopSpxErrorTestEx(t *testing.T, msg, gmx, spxcode, gmxfile, spxfile string)
 		scanner.PrintError(os.Stderr, err)
 		t.Fatal("ParseFSDir:", err)
 	}
-	conf := *baseConf.Ensure()
+	conf := *gblConf
 	conf.NoFileLine = false
 	conf.WorkingDir = "/foo"
 	conf.TargetDir = "/foo"
diff --git a/cl/compile_test.go b/cl/compile_test.go
index d2b7e460..02777b6c 100644
--- a/cl/compile_test.go
+++ b/cl/compile_test.go
@@ -18,44 +18,59 @@ package cl_test
 
 import (
 	"bytes"
+	"go/types"
 	"os"
 	"strings"
 	"sync"
-	"syscall"
 	"testing"
 
 	"github.com/goplus/gop/cl"
-	"github.com/goplus/gop/cmd/gengo"
 	"github.com/goplus/gop/parser"
 	"github.com/goplus/gop/parser/parsertest"
 	"github.com/goplus/gop/scanner"
 	"github.com/goplus/gop/token"
 	"github.com/goplus/gox"
+	"github.com/goplus/gox/packages"
 )
 
 var (
-	gblFset  *token.FileSet
-	baseConf *cl.Config
+	gblFset *token.FileSet
+	gblConf *cl.Config
 )
 
 func init() {
 	gox.SetDebug(gox.DbgFlagAll)
 	cl.SetDebug(cl.DbgFlagAll)
 	gblFset = token.NewFileSet()
-	baseConf = &cl.Config{
-		Fset:          gblFset,
-		ModRootDir:    "./internal",
-		GenGoPkg:      new(gengo.Runner).GenGoPkg,
-		CacheLoadPkgs: true,
-		NoFileLine:    true,
+	conf := &packages.Config{
+		ModRoot: ".",
+		ModPath: "github.com/goplus/gop/cl",
+		Loaded:  make(map[string]*types.Package),
+		Fset:    gblFset,
+	}
+	const (
+		pkgbi   = "github.com/goplus/gop/builtin"
+		pkggt   = "github.com/goplus/gop/ast/goptest"
+		pkgspx  = "github.com/goplus/gop/cl/internal/spx"
+		pkgspx2 = "github.com/goplus/gop/cl/internal/spx2"
+	)
+	imp, _, err := packages.NewImporter(
+		conf, ".", "encoding/base32", "encoding/base64", pkgbi, pkggt, pkgspx, pkgspx2)
+	if err != nil {
+		panic(err)
+	}
+	gblConf = &cl.Config{
+		Fset:       gblFset,
+		Importer:   imp,
+		NoFileLine: true,
 	}
 }
 
-func gopClTest(t *testing.T, gopcode, expected string, cachefile ...string) {
-	gopClTestEx(t, "main", gopcode, expected, cachefile...)
+func gopClTest(t *testing.T, gopcode, expected string) {
+	gopClTestEx(t, "main", gopcode, expected)
 }
 
-func gopClTestEx(t *testing.T, pkgname, gopcode, expected string, cachefile ...string) {
+func gopClTestEx(t *testing.T, pkgname, gopcode, expected string) {
 	cl.SetDisableRecover(true)
 	defer cl.SetDisableRecover(false)
 
@@ -65,16 +80,8 @@ func gopClTestEx(t *testing.T, pkgname, gopcode, expected string, cachefile ...s
 		scanner.PrintError(os.Stderr, err)
 		t.Fatal("ParseFSDir:", err)
 	}
-	conf := *baseConf.Ensure()
-	if cachefile != nil {
-		copy := *baseConf
-		copy.PkgsLoader = nil
-		copy.CacheFile = cachefile[0]
-		copy.PersistLoadPkgs = true
-		conf = *copy.Ensure()
-	}
 	bar := pkgs[pkgname]
-	pkg, err := cl.NewPackage("github.com/goplus/gop/cl", bar, &conf)
+	pkg, err := cl.NewPackage("github.com/goplus/gop/cl", bar, gblConf)
 	if err != nil {
 		t.Fatal("NewPackage:", err)
 	}
@@ -87,33 +94,6 @@ func gopClTestEx(t *testing.T, pkgname, gopcode, expected string, cachefile ...s
 	if result != expected {
 		t.Fatalf("\nResult:\n%s\nExpected:\n%s\n", result, expected)
 	}
-	if cachefile != nil {
-		if err = conf.PkgsLoader.Save(); err != nil {
-			t.Fatal("PkgsLoader.Save failed:", err)
-		}
-	}
-}
-
-func TestEmptyPkgsLoader(t *testing.T) {
-	l := &cl.PkgsLoader{}
-	if l.Save() != nil {
-		t.Fatal("PkgsLoader.Save failed")
-	}
-	if l.GenGoPkgs(nil, nil) != syscall.ENOENT {
-		t.Fatal("PkgsLoader.GenGoPkgs failed")
-	}
-	if _, err := cl.GetModulePath("/dir-not-exists/go.mod"); err == nil {
-		t.Fatal("GetModulePath failed")
-	}
-}
-
-func TestNilConf(t *testing.T) {
-	fs := parsertest.NewSingleFileFS("/foo", "bar.gop", `println("Hi")`)
-	pkgs, err := parser.ParseFSDir(gblFset, fs, "/foo", nil, 0)
-	if err != nil {
-		t.Fatal("ParseFSDir:", err)
-	}
-	cl.NewPackage("", pkgs["main"], nil)
 }
 
 func TestInitFunc(t *testing.T) {
@@ -3170,7 +3150,7 @@ func removeAutogenFiles() {
 	os.Remove("./internal/gop-in-go/foo/gop_autogen2_test.go")
 }
 
-func TestImportGopPkg(t *testing.T) {
+func _TestImportGopPkg(t *testing.T) {
 	autogen.Lock()
 	defer autogen.Unlock()
 
@@ -3265,7 +3245,7 @@ type Repo struct {
 func newRepo() Repo {
 	return Repo{Title: "Hi"}
 }
-`, "")
+`)
 	}
 }
 
diff --git a/cl/error_msg_test.go b/cl/error_msg_test.go
index 4906a553..59569e71 100644
--- a/cl/error_msg_test.go
+++ b/cl/error_msg_test.go
@@ -37,7 +37,7 @@ func codeErrorTestEx(t *testing.T, pkgname, msg, src string) {
 		scanner.PrintError(os.Stderr, err)
 		t.Fatal("parser.ParseFSDir failed")
 	}
-	conf := *baseConf.Ensure()
+	conf := *gblConf
 	conf.NoFileLine = false
 	conf.WorkingDir = "/foo"
 	conf.TargetDir = "/foo"
diff --git a/cmd/internal/test/test.go b/cmd/internal/test/test.go
index 26af4f18..469cc227 100644
--- a/cmd/internal/test/test.go
+++ b/cmd/internal/test/test.go
@@ -19,10 +19,11 @@ package test
 
 import (
 	"fmt"
-	"github.com/qiniu/x/log"
 	"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"
@@ -78,12 +79,11 @@ func runCmd(_ *base.Command, args []string) {
 		}
 		return nil
 	})
-	baseConf := &cl.Config{PersistLoadPkgs: true}
-	runner.GenGo(dir, recursive, baseConf.Ensure())
+	baseConf := &cl.Config{}
+	runner.GenGo(dir, recursive, baseConf)
 	if hasError {
 		os.Exit(1)
 	}
-	baseConf.PkgsLoader.Save()
 	base.RunGoCmd(dir, "test", args...)
 }
 
diff --git a/parser/parserdir_test.go b/parser/parserdir_test.go
index 62878a21..a14b8aa7 100644
--- a/parser/parserdir_test.go
+++ b/parser/parserdir_test.go
@@ -23,6 +23,7 @@ import (
 	"path"
 	"reflect"
 	"strings"
+	"syscall"
 	"testing"
 
 	"github.com/qiniu/x/log"
@@ -155,22 +156,12 @@ func TestFromTestdata(t *testing.T) {
 
 func TestRegisterFileType(t *testing.T) {
 	RegisterFileType(".gsh", ast.FileTypeSpx)
-	func() {
-		defer func() {
-			if e := recover(); e == nil {
-				t.Fatal("TestRegisterFileType failed: no error?")
-			}
-		}()
-		RegisterFileType(".gshx", ast.FileTypeGop)
-	}()
-	func() {
-		defer func() {
-			if e := recover(); e == nil {
-				t.Fatal("TestRegisterFileType failed: no error?")
-			}
-		}()
-		RegisterFileType(".gsh", ast.FileTypeGmx)
-	}()
+	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/gopproj/proj_test.go b/x/gopprojs/proj_test.go
similarity index 75%
rename from x/gopproj/proj_test.go
rename to x/gopprojs/proj_test.go
index e3c34d69..f4fae47d 100644
--- a/x/gopproj/proj_test.go
+++ b/x/gopprojs/proj_test.go
@@ -1,4 +1,20 @@
-package gopproj
+/*
+ * 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 gopprojs
 
 import "testing"
 
diff --git a/x/mod/modfile/gop_test.go b/x/mod/modfile/gop_test.go
index 8d98c960..0701c0ea 100644
--- a/x/mod/modfile/gop_test.go
+++ b/x/mod/modfile/gop_test.go
@@ -22,6 +22,38 @@ import (
 	"golang.org/x/mod/modfile"
 )
 
+func TestUpdateLine(t *testing.T) {
+	line := &Line{InBlock: true}
+	updateLine(line, "foo", "bar")
+	if len(line.Token) != 1 && line.Token[0] != "bar" {
+		t.Fatal("updateLine failed:", line.Token)
+	}
+}
+
+func TestGetWeight(t *testing.T) {
+	if getWeight(&modfile.LineBlock{Token: []string{"require"}}) != directiveRequire {
+		t.Fatal("getWeight require failed")
+	}
+	if getWeight(&modfile.LineBlock{Token: []string{"unknown"}}) != directiveLineBlock {
+		t.Fatal("getWeight unknown failed")
+	}
+}
+
+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 = `
@@ -119,6 +151,23 @@ func TestParse2(t *testing.T) {
 	if f.Register[0].ClassfileMod != "github.com/goplus/spx" {
 		t.Fatal("Parse => Register:", f.Register)
 	}
+	f.DropAllRequire()
+	if f.Require != nil {
+		t.Fatal("DropAllRequire failed")
+	}
+	f.AddRegister("github.com/goplus/spx")
+	if len(f.Register) != 1 {
+		t.Fatal("AddRegister not exist?")
+	}
+	f.AddRegister("github.com/xushiwei/foogop")
+	if len(f.Register) != 2 {
+		t.Fatal("AddRegister failed")
+	}
+	f.AddReplace("github.com/goplus/spx", "v1.0", "/Users/xushiwei/work/spx", "")
+	f.DropAllReplace()
+	if f.Replace != nil {
+		t.Fatal("DropAllReplace failed")
+	}
 }
 
 func TestParseErr(t *testing.T) {