Repository: goplus/gop
Pull request #1077 (2022-01-03 03:25:48)

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


Patch:
diff --git a/cl/classfile.go b/cl/classfile.go
index 6d4cd590..15e3646f 100644
--- a/cl/classfile.go
+++ b/cl/classfile.go
@@ -22,7 +22,6 @@ import (
 	"go/types"
 	"path/filepath"
 	"strings"
-	"syscall"
 
 	"github.com/goplus/gop/ast"
 	"github.com/goplus/gop/parser"
@@ -44,7 +43,7 @@ var (
 )
 
 // RegisterClassFileType registers Go+ class file types.
-func RegisterClassFileType(extGmx, extSpx string, pkgPaths ...string) error {
+func RegisterClassFileType(extGmx, extSpx string, pkgPaths ...string) {
 	if pkgPaths == nil {
 		panic("RegisterClassFileType: no pkgPath specified")
 	}
@@ -52,11 +51,9 @@ func RegisterClassFileType(extGmx, extSpx string, pkgPaths ...string) error {
 	if extSpx != "" {
 		parser.RegisterFileType(extSpx, ast.FileTypeSpx)
 	}
-	if _, ok := gmxTypes[extGmx]; ok {
-		return syscall.EEXIST
+	if _, ok := gmxTypes[extGmx]; !ok {
+		gmxTypes[extGmx] = gmxInfo{extSpx, pkgPaths}
 	}
-	gmxTypes[extGmx] = gmxInfo{extSpx, pkgPaths}
-	return nil
 }
 
 // -----------------------------------------------------------------------------
diff --git a/cl/compile.go b/cl/compile.go
index 51f3cf98..7925f9eb 100644
--- a/cl/compile.go
+++ b/cl/compile.go
@@ -18,6 +18,7 @@
 package cl
 
 import (
+	"context"
 	"errors"
 	"fmt"
 	"go/types"
@@ -72,18 +73,64 @@ func (p *Errors) Error() string {
 
 // Config of loading Go+ packages.
 type Config struct {
-	// WorkingDir is the directory in which to run gop compiler.
-	WorkingDir string
+	// 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.
 	// TargetDir is the directory in which to generate Go files.
-	TargetDir string
+	// 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
 
 	// 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
 
-	// An Importer resolves import paths to Packages.
-	Importer types.Importer
+	// 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
 
 	// NoFileLine = true means not to generate file line comments.
 	NoFileLine bool
@@ -92,6 +139,16 @@ 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
@@ -343,22 +400,34 @@ 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 = workingDir
+		targetDir = dir
 	}
 	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,
-		Importer:        conf.Importer,
+		LoadPkgs:        conf.PkgsLoader.LoadPkgs,
 		LoadNamed:       ctx.loadNamed,
 		HandleErr:       ctx.handleErr,
 		NodeInterpreter: interp,
+		ParseFile:       nil, // TODO
 		NewBuiltin:      newBuiltinDefault,
 	}
 	p = gox.NewPackage(pkgPath, pkg.Name, confGox)
diff --git a/cl/_packages.go b/cl/packages.go
similarity index 51%
rename from cl/_packages.go
rename to cl/packages.go
index a4dbd82b..9b45551c 100644
--- a/cl/_packages.go
+++ b/cl/packages.go
@@ -17,13 +17,17 @@
 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"
 )
 
@@ -44,15 +48,46 @@ 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
@@ -79,4 +114,48 @@ 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/cmd/gengo/gen.go b/cmd/gengo/gen.go
index f1c6ae99..518cdab6 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
-	conf.WorkingDir = pkgDir
+	conf := *base.Ensure()
+	conf.Dir = pkgDir
 	if conf.Fset == nil {
 		conf.Fset = token.NewFileSet()
 	}
diff --git a/cmd/gop/main.go b/cmd/gop/main.go
index 0f8eb1be..a1c6664f 100644
--- a/cmd/gop/main.go
+++ b/cmd/gop/main.go
@@ -23,8 +23,6 @@ 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"
@@ -51,16 +49,14 @@ 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 9a3956db..d9a69547 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 := gopprojs.ParseOne(os.Args[1:]...)
+	proj, args, err := gopproj.ParseOne(os.Args[1:]...)
 	if err != nil {
 		log.Fatalln(err)
 	}
-	var ctx = gopproj.New("")
+	var ctx = gopmod.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 1ff7a8d6..968d9c96 100644
--- a/cmd/internal/base/gocmd.go
+++ b/cmd/internal/base/gocmd.go
@@ -77,12 +77,13 @@ func GenGoForBuild(dir string, recursive bool, errorHandle func()) {
 		}
 		return nil
 	})
-	baseConf := &cl.Config{}
-	runner.GenGo(dir, recursive, baseConf)
+	baseConf := &cl.Config{PersistLoadPkgs: true}
+	runner.GenGo(dir, recursive, baseConf.Ensure())
 	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 ebbcc886..d8b4dbbb 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] [packages]",
+	UsageLine: "gop build [-v] [-o output] <gopSrcDir|gopSrcFile>",
 	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.UpdateGoMod(dir)
+	modload.Load()
 	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
deleted file mode 100644
index 8d1ead7e..00000000
--- a/cmd/internal/deps/deps.go
+++ /dev/null
@@ -1,75 +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 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 fa7e3ad7..27d85735 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 32cf189f..3ac4b670 100644
--- a/cmd/internal/gengo/go.go
+++ b/cmd/internal/gengo/go.go
@@ -78,6 +78,7 @@ 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() {
@@ -99,7 +100,7 @@ func runCmd(cmd *base.Command, args []string) {
 	}
 	dir := flag.Arg(0)
 	dir = strings.TrimSuffix(dir, "/...")
-	modload.UpdateGoMod(dir)
+	modload.Load()
 	runner := new(gengo.Runner)
 	runner.SetAfter(func(p *gengo.Runner, dir string, flags int) error {
 		errs := p.ResetErrors()
@@ -113,7 +114,7 @@ func runCmd(cmd *base.Command, args []string) {
 		}
 		return nil
 	})
-	runner.GenGo(dir, true, &cl.Config{})
+	runner.GenGo(dir, true, &cl.Config{CacheLoadPkgs: !*flagSlow})
 	errs := runner.Errors()
 	if errs != nil {
 		for _, err := range errs {
diff --git a/cmd/internal/gopget/get.go b/cmd/internal/gopget/get.go
deleted file mode 100644
index 7223da5b..00000000
--- a/cmd/internal/gopget/get.go
+++ /dev/null
@@ -1,91 +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 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 676ec51c..c4e2c9a0 100644
--- a/cmd/internal/mod/download.go
+++ b/cmd/internal/mod/download.go
@@ -13,20 +13,25 @@
  * 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/gopget"
+	"github.com/goplus/gop/cmd/internal/modload"
+	"github.com/goplus/gop/x/mod/modfetch"
 )
 
-// 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 = gopget.Cmd.Run
+	cmdDownload.Run = runDownload // break init cycle
+}
+
+func runDownload(cmd *base.Command, args []string) {
+	modfetch.DownloadArgs(".", args...)
+	modload.SyncGopMod()
 }
diff --git a/cmd/internal/mod/init.go b/cmd/internal/mod/init.go
index 7f2e954c..a3f14e85 100644
--- a/cmd/internal/mod/init.go
+++ b/cmd/internal/mod/init.go
@@ -14,20 +14,17 @@
  * limitations under the License.
  */
 
+// gop mod init
+
 package mod
 
 import (
-	"fmt"
-	"os"
-	"runtime"
-	"strings"
+	"log"
 
 	"github.com/goplus/gop/cmd/internal/base"
-	"github.com/goplus/gop/env"
-	"github.com/goplus/gop/x/mod/modload"
+	"github.com/goplus/gop/cmd/internal/modload"
 )
 
-// gop mod init
 var cmdInit = &base.Command{
 	UsageLine: "gop mod init [module]",
 	Short:     "initialize new module in current directory",
@@ -38,40 +35,17 @@ func init() {
 }
 
 func runInit(cmd *base.Command, args []string) {
-	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)
+	if len(args) > 1 {
+		log.Fatalf("gop mod init: too many arguments")
 	}
-	err = mod.Save()
-	if err != nil {
-		fatal(err)
+	var modPath string
+	if len(args) == 1 {
+		modPath = args[0]
 	}
-}
-
-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
-}
 
-func fatal(msg interface{}) {
-	fmt.Fprintln(os.Stderr, msg)
-	os.Exit(1)
+	// modfetch.InitArgs(".", args...)
+	modload.CreateModFile(modPath) // does all the hard work
+	modload.LoadModFile()
+	modload.SyncGoMod()
+	modload.SyncGopMod()
 }
diff --git a/cmd/internal/mod/mod.go b/cmd/internal/mod/mod.go
index 095d28cb..58d17279 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 6a9f486a..4b8473c0 100644
--- a/cmd/internal/mod/tidy.go
+++ b/cmd/internal/mod/tidy.go
@@ -13,19 +13,17 @@
  * 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/x/mod/modload"
+	"github.com/goplus/gop/cmd/internal/modload"
+	"github.com/goplus/gop/x/mod/modfetch"
 )
 
-// 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",
 }
 
@@ -34,13 +32,8 @@ func init() {
 }
 
 func runTidy(cmd *base.Command, args []string) {
-	mod, err := modload.Load(".")
-	check(err)
-	check(mod.Tidy())
-}
-
-func check(err error) {
-	if err != nil {
-		log.Fatalln(err)
-	}
+	modload.LoadModFile()
+	modload.SyncGoMod()
+	modfetch.TidyArgs(".", args...)
+	modload.SyncGopMod()
 }
diff --git a/cmd/internal/modload/classfile.go b/cmd/internal/modload/classfile.go
new file mode 100644
index 00000000..5d173e67
--- /dev/null
+++ b/cmd/internal/modload/classfile.go
@@ -0,0 +1,74 @@
+/*
+ * 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 8f87a5de..07f07c29 100644
--- a/cmd/internal/modload/modload.go
+++ b/cmd/internal/modload/modload.go
@@ -13,30 +13,473 @@
  * 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/x/gopmod"
+	"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
 )
 
-// -----------------------------------------------------------------------------
+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
+	}
+}
 
-func UpdateGoMod(dir string) {
-	p, err := gopmod.Load(dir)
+// 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)
 	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
 	}
-	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...)
+	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
 		}
+		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 880d0497..15e27c79 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/gopproj"
+	"github.com/goplus/gop/x/gopmod"
 	"github.com/qiniu/x/log"
 )
 
-func findGoModFile(dir string) (modfile string, err error) {
+func findGoModFile(dir string) (modfile string, noCacheFile bool, err error) {
 	modfile, err = env.GOPMOD(dir)
 	if err != nil {
 		modfile = filepath.Join(env.GOPROOT(), "go.mod")
-		return modfile, nil
+		return modfile, true, nil
 	}
 	return
 }
 
-func findGoModDir(dir string) string {
-	modfile, err := findGoModFile(dir)
+func findGoModDir(dir string) (string, bool) {
+	modfile, nocachefile, err := findGoModFile(dir)
 	if err != nil {
 		log.Fatalln("findGoModFile:", err)
 	}
-	return filepath.Dir(modfile)
+	return filepath.Dir(modfile), nocachefile
 }
 
 func gopRun(source string, args ...string) {
-	ctx := gopproj.New("")
+	ctx := gopmod.New("")
 	flags := 0
 	if *flagGop {
-		flags = gopproj.FlagGoAsGoPlus
+		flags = gopmod.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 3bc8a741..47def396 100644
--- a/cmd/internal/run/run.go
+++ b/cmd/internal/run/run.go
@@ -24,9 +24,11 @@ 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"
@@ -114,7 +116,7 @@ func runCmd(cmd *base.Command, args []string) {
 	if isDir {
 		srcDir = src
 		gofile = src + "/gop_autogen.go"
-		modload.UpdateGoMod(srcDir)
+		modload.Load()
 		isDirty = true // TODO: check if code changed
 		if isDirty {
 			pkgs, err = parser.ParseDir(fset, src, nil, parserMode)
@@ -141,8 +143,9 @@ func runCmd(cmd *base.Command, args []string) {
 			os.Exit(12)
 		}
 
-		modDir := findGoModDir(srcDir)
-		conf := &cl.Config{WorkingDir: modDir, TargetDir: srcDir, Fset: fset}
+		modDir, noCacheFile := findGoModDir(srcDir)
+		conf := &cl.Config{
+			Dir: modDir, TargetDir: srcDir, Fset: fset, CacheLoadPkgs: true, PersistLoadPkgs: !noCacheFile}
 		out, err := cl.NewPackage("", mainPkg, conf)
 		if err != nil {
 			fmt.Fprintln(os.Stderr, err)
@@ -152,6 +155,7 @@ func runCmd(cmd *base.Command, args []string) {
 		if err != nil {
 			log.Fatalln("saveGoFile failed:", err)
 		}
+		conf.PkgsLoader.Save()
 	}
 
 	goRun(gofile, args)
@@ -182,6 +186,38 @@ 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
new file mode 100644
index 00000000..9e72beba
--- /dev/null
+++ b/cmd/internal/search/search.go
@@ -0,0 +1,77 @@
+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 64fc51ac..a4797c81 100644
--- a/cmd/internal/version/ver.go
+++ b/cmd/internal/version/ver.go
@@ -26,7 +26,7 @@ import (
 
 // -----------------------------------------------------------------------------
 
-// Cmd - gop version
+// Cmd - gop build
 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 288983ea..65b1c556 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,8 @@ module github.com/goplus/gop
 go 1.16
 
 require (
-	github.com/goplus/gox v1.9.1
+	github.com/goplus/gox v1.8.7
 	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 5d3e59a0..e18552a7 100644
--- a/go.sum
+++ b/go.sum
@@ -1,5 +1,5 @@
-github.com/goplus/gox v1.9.1 h1:vupKI4LRpGDuKPSZFaN1BjiEsKjzSsUEKiwssQ/3fBU=
-github.com/goplus/gox v1.9.1/go.mod h1:PznHkzl2HARBf7+s2reqcwKm1Z1a6Wae6EntQEh0iJI=
+github.com/goplus/gox v1.8.7 h1:XtUEazI+EMSyN9A0IGbrPe99dpT5lnD1k98HwmVL9Nc=
+github.com/goplus/gox v1.8.7/go.mod h1:XWqV75HImeysI8+Q9gAdYyvwlP8jds24xsgYRwTO3JI=
 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 ed3f0ec5..9be2ad19 100644
--- a/parser/parser_gop.go
+++ b/parser/parser_gop.go
@@ -24,7 +24,6 @@ import (
 	"os"
 	"path/filepath"
 	"strings"
-	"syscall"
 
 	"github.com/goplus/gop/ast"
 	"github.com/goplus/gop/token"
@@ -165,15 +164,14 @@ var (
 )
 
 // RegisterFileType registers a new Go+ class file type.
-func RegisterFileType(ext string, format ast.FileType) error {
+func RegisterFileType(ext string, format ast.FileType) {
 	if format != ast.FileTypeSpx && format != ast.FileTypeGmx {
-		return syscall.EINVAL
+		panic("RegisterFileType: format should be FileTypeSpx or FileTypeGmx")
 	}
 	if _, ok := extGopFiles[ext]; ok {
-		return syscall.EEXIST
+		panic("RegisterFileType: file type exists")
 	}
 	extGopFiles[ext] = format
-	return nil
 }
 
 // -----------------------------------------------------------------------------
diff --git a/x/gengo/gengo.go b/x/gengo/gengo.go
deleted file mode 100644
index 8c158ff9..00000000
--- a/x/gengo/gengo.go
+++ /dev/null
@@ -1,303 +0,0 @@
-/*
- * Copyright (c) 2021 The GoPlus Authors (goplus.org). All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package gengo
-
-import (
-	"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
deleted file mode 100644
index ecbb13fb..00000000
--- a/x/gopmod/classfile.go
+++ /dev/null
@@ -1,110 +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 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/gopproj/gocmd.go b/x/gopmod/gocmd.go
similarity index 94%
rename from x/gopproj/gocmd.go
rename to x/gopmod/gocmd.go
index b73c8c8a..7aa79f0a 100644
--- a/x/gopproj/gocmd.go
+++ b/x/gopmod/gocmd.go
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package gopproj
+package gopmod
 
 import (
 	"fmt"
@@ -38,15 +38,13 @@ const (
 )
 
 var (
-	ldFlags = fmt.Sprintf(ldFlagAll, env.Version(), env.BuildDate(), GOPROOT)
-)
-
-var (
-	GOPROOT = env.GOPROOT()
+	GOPVERSION   = env.Version()
+	GOPBUILDDATE = env.BuildDate()
+	GOPROOT      = env.GOPROOT()
 )
 
 func LoadFlags() string {
-	return ldFlags
+	return fmt.Sprintf(ldFlagAll, GOPVERSION, GOPBUILDDATE, GOPROOT)
 }
 
 // -----------------------------------------------------------------------------
diff --git a/x/gopmod/godeps.go b/x/gopmod/godeps.go
deleted file mode 100644
index 1305fc71..00000000
--- a/x/gopmod/godeps.go
+++ /dev/null
@@ -1,66 +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 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/gopproj/gofile.go b/x/gopmod/gofile.go
similarity index 99%
rename from x/gopproj/gofile.go
rename to x/gopmod/gofile.go
index d65fcc58..9cbd6eed 100644
--- a/x/gopproj/gofile.go
+++ b/x/gopmod/gofile.go
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package gopproj
+package gopmod
 
 import (
 	"crypto/sha1"
diff --git a/x/gopmod/gopdeps.go b/x/gopmod/gopdeps.go
deleted file mode 100644
index 21cdb55f..00000000
--- a/x/gopmod/gopdeps.go
+++ /dev/null
@@ -1,66 +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 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/gopproj/gopfiles.go b/x/gopmod/gopfiles.go
similarity index 92%
rename from x/gopproj/gopfiles.go
rename to x/gopmod/gopfiles.go
index b2e51548..961bf512 100644
--- a/x/gopproj/gopfiles.go
+++ b/x/gopmod/gopfiles.go
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package gopproj
+package gopmod
 
 import (
 	"bytes"
@@ -117,12 +117,18 @@ func (p *gopFiles) GenGo(outFile, modFile string) error {
 
 	srcDir, _ := filepath.Split(outFile)
 	modDir, _ := filepath.Split(modFile)
-	conf := &cl.Config{WorkingDir: modDir, TargetDir: srcDir, Fset: fset}
+	conf := &cl.Config{
+		Dir: modDir, TargetDir: srcDir, Fset: fset, CacheLoadPkgs: true, PersistLoadPkgs: true}
 	out, err := cl.NewPackage("", mainPkg, conf)
 	if err != nil {
 		return err
 	}
-	return gox.WriteFile(outFile, out, false)
+	err = gox.WriteFile(outFile, out, false)
+	if err != nil {
+		return err
+	}
+	conf.PkgsLoader.Save()
+	return nil
 }
 
 // -----------------------------------------------------------------------------
diff --git a/x/gopproj/projctx.go b/x/gopmod/gopmod.go
similarity index 98%
rename from x/gopproj/projctx.go
rename to x/gopmod/gopmod.go
index 1262007f..08e9baaf 100644
--- a/x/gopproj/projctx.go
+++ b/x/gopmod/gopmod.go
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package gopproj
+package gopmod
 
 import (
 	"bytes"
@@ -172,7 +172,7 @@ func genGomodFile(modfile string) {
 		}
 		gopRoot = filepath.ToSlash(gopRoot)
 	}
-	fmt.Fprintf(&buf, gomodFormat, env.Version(), gopRoot)
+	fmt.Fprintf(&buf, gomodFormat, GOPVERSION, gopRoot)
 	err = os.WriteFile(modfile, buf.Bytes(), 0644)
 	if err != nil {
 		log.Panicln(err)
diff --git a/x/gopproj/gopproj.go b/x/gopmod/gopproj.go
similarity index 87%
rename from x/gopproj/gopproj.go
rename to x/gopmod/gopproj.go
index c8005717..432a9ead 100644
--- a/x/gopproj/gopproj.go
+++ b/x/gopmod/gopproj.go
@@ -14,12 +14,12 @@
  * limitations under the License.
  */
 
-package gopproj
+package gopmod
 
 import (
 	"path/filepath"
 
-	"github.com/goplus/gop/x/gopprojs"
+	"github.com/goplus/gop/x/gopproj"
 )
 
 // -----------------------------------------------------------------------------
@@ -28,13 +28,13 @@ const (
 	FlagGoAsGoPlus = 1 << iota
 )
 
-func (p *Context) OpenProject(flags int, src gopprojs.Proj) (proj *Project, err error) {
+func (p *Context) OpenProject(flags int, src gopproj.Proj) (proj *Project, err error) {
 	switch v := src.(type) {
-	case *gopprojs.FilesProj:
+	case *gopproj.FilesProj:
 		return p.OpenFiles(flags, v.Files...)
-	case *gopprojs.DirProj:
+	case *gopproj.DirProj:
 		return p.OpenDir(flags, v.Dir)
-	case *gopprojs.PkgPathProj:
+	case *gopproj.PkgPathProj:
 		return p.OpenPkgPath(flags, v.Path)
 	}
 	panic("OpenProject: unexpected source")
diff --git a/x/gopmod/module.go b/x/gopmod/module.go
deleted file mode 100644
index 8f526687..00000000
--- a/x/gopmod/module.go
+++ /dev/null
@@ -1,296 +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 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/gopprojs/proj.go b/x/gopproj/proj.go
similarity index 71%
rename from x/gopprojs/proj.go
rename to x/gopproj/proj.go
index 1144dbb1..488a80c5 100644
--- a/x/gopprojs/proj.go
+++ b/x/gopproj/proj.go
@@ -1,20 +1,4 @@
-/*
- * 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
+package gopproj
 
 import (
 	"errors"
diff --git a/x/mod/internal/lazyregexp/lazyregexp.go b/x/mod/internal/lazyregexp/lazyregexp.go
new file mode 100644
index 00000000..7f386635
--- /dev/null
+++ b/x/mod/internal/lazyregexp/lazyregexp.go
@@ -0,0 +1,90 @@
+/*
+ * 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 d973ac75..2959b699 100644
--- a/x/mod/modfetch/cache.go
+++ b/x/mod/modfetch/cache.go
@@ -13,47 +13,44 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
 package modfetch
 
 import (
-	"errors"
+	"fmt"
 	"path/filepath"
 
-	"github.com/goplus/gop/env"
 	"golang.org/x/mod/module"
-)
-
-// -----------------------------------------------------------------------------
+	"golang.org/x/mod/semver"
 
-var (
-	GOMODCACHE = env.GOMODCACHE()
+	"github.com/goplus/gop/env"
 )
 
 var (
-	ErrNoNeedToDownload = errors.New("no need to download")
+	GOMODCACHE = env.GOMODCACHE()
 )
 
-func DownloadCachePath(mod module.Version) (string, error) {
-	if mod.Version == "" {
-		return mod.Path, ErrNoNeedToDownload
-	}
-	encPath, err := module.EscapePath(mod.Path)
+func cacheDir(path string) (string, error) {
+	enc, err := module.EscapePath(path)
 	if err != nil {
 		return "", err
 	}
-	return filepath.Join(GOMODCACHE, "cache/download", encPath, "@v", mod.Version+".zip"), nil
+	return filepath.Join(GOMODCACHE, "cache/download", enc, "/@v"), nil
 }
 
-func ModCachePath(mod module.Version) (string, error) {
-	if mod.Version == "" {
-		return mod.Path, nil
+func CachePath(m module.Version, suffix string) (string, error) {
+	dir, err := cacheDir(m.Path)
+	if err != nil {
+		return "", err
 	}
-	encPath, err := module.EscapePath(mod.Path)
+	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
 	}
-	return filepath.Join(GOMODCACHE, encPath+"@"+mod.Version), nil
+	return filepath.Join(dir, encVer+"."+suffix), nil
 }
-
-// -----------------------------------------------------------------------------
diff --git a/x/mod/modfetch/fetch.go b/x/mod/modfetch/fetch.go
index 7d2a30b5..fadb515c 100644
--- a/x/mod/modfetch/fetch.go
+++ b/x/mod/modfetch/fetch.go
@@ -13,80 +13,131 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
 package modfetch
 
 import (
-	"bytes"
+	"errors"
+	"fmt"
+	"io/fs"
+	"log"
 	"os"
 	"os/exec"
-	"strings"
-	"syscall"
+	"path/filepath"
 
-	"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
+	}
 
-type (
-	ExecCmdError = modload.ExecCmdError
-)
+	// 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")}
+	}
 
-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()
+	// 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")
 	if err != nil {
-		err = &ExecCmdError{Err: err, Stderr: stderr.Bytes()}
-		return
+		return dir, err
 	}
-	return getResult(stderr.String())
-}
-
-func getResult(data string) (mod module.Version, isClass bool, err error) {
-	if data == "" {
-		err = syscall.EEXIST
-		return
+	if _, err := os.Stat(partialPath); err == nil {
+		return dir, &DownloadDirPartialError{dir, errors.New("not completely extracted")}
+	} else if !os.IsNotExist(err) {
+		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)
+
+	// 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
 	}
-	// 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)
+	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
 	}
-	return
+	return dir, nil
 }
 
-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)
-			}
-		}
+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
 	}
-	return
+
+	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...)...)
 }
 
-func convGoMod(dir string) (isClass bool, err error) {
-	mod, err := modload.Load(dir)
+// 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 {
-		return
+		switch e := err.(type) {
+		case *exec.ExitError:
+			os.Exit(e.ExitCode())
+		default:
+			log.Fatalln("RunGoCmd failed:", err)
+		}
 	}
-	os.Chmod(dir, 0755)
-	defer os.Chmod(dir, 0555)
-	return mod.Classfile != nil, mod.UpdateGoMod(true)
 }
 
-// -----------------------------------------------------------------------------
+// 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 (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
new file mode 100644
index 00000000..f20b9dde
--- /dev/null
+++ b/x/mod/modfetch/file.go
@@ -0,0 +1,106 @@
+/*
+ * 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 f1f2f092..bea02c19 100644
--- a/x/mod/modfile/rule.go
+++ b/x/mod/modfile/rule.go
@@ -214,18 +214,6 @@ func IsDirectoryPath(ns string) bool {
 	return modfile.IsDirectoryPath(ns)
 }
 
-func IsLocal(ns string) bool {
-	if len(ns) > 0 {
-		switch c := ns[0]; c {
-		case '/', '\\', '.':
-			return true
-		default:
-			return len(ns) >= 2 && ns[1] == ':' && ('A' <= c && c <= 'Z' || 'a' <= c && c <= 'z')
-		}
-	}
-	return false
-}
-
 // MustQuote reports whether s must be quoted in order to appear as
 // a single token in a gop.mod line.
 func MustQuote(s string) bool {
@@ -336,7 +324,7 @@ func getWeight(e Expr) int {
 	return directiveLineBlock
 }
 
-func updateLine(line *Line, tokens ...string) {
+func updateLine(x *FileSyntax, line *Line, tokens ...string) {
 	if line.InBlock {
 		tokens = tokens[1:]
 	}
@@ -370,50 +358,9 @@ func (f *File) AddGopStmt(version string) error {
 		}
 	} else {
 		f.Gop.Version = version
-		updateLine(f.Gop.Syntax, "gop", version)
+		updateLine(f.Syntax, 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
deleted file mode 100644
index eeeac15c..00000000
--- a/x/mod/modload/module.go
+++ /dev/null
@@ -1,340 +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 (
-	"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 ede759c1..045c08e6 100644
--- a/cl/builtin_test.go
+++ b/cl/builtin_test.go
@@ -23,37 +23,15 @@ 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", goxConf)
+	pkg := gox.NewPackage("", "foo", nil)
 	ctx := &blockCtx{pkg: pkg, cb: pkg.CB()}
 	compileErrWrapExpr(ctx, &ast.ErrWrapExpr{Tok: token.QUESTION})
 }
@@ -106,7 +84,7 @@ func TestCanAutoCall(t *testing.T) {
 // -----------------------------------------------------------------------------
 
 func TestGmxSettings(t *testing.T) {
-	pkg := gox.NewPackage("", "foo", goxConf)
+	pkg := gox.NewPackage("", "foo", nil)
 	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 c9c86688..032ea2f0 100644
--- a/cl/compile_spx_test.go
+++ b/cl/compile_spx_test.go
@@ -40,7 +40,6 @@ 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) {
@@ -57,8 +56,9 @@ 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, gblConf)
+	pkg, err := cl.NewPackage("", bar, &conf)
 	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 := *gblConf
+	conf := *baseConf.Ensure()
 	conf.NoFileLine = false
 	conf.WorkingDir = "/foo"
 	conf.TargetDir = "/foo"
diff --git a/cl/compile_test.go b/cl/compile_test.go
index 02777b6c..d2b7e460 100644
--- a/cl/compile_test.go
+++ b/cl/compile_test.go
@@ -18,59 +18,44 @@ 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
-	gblConf *cl.Config
+	gblFset  *token.FileSet
+	baseConf *cl.Config
 )
 
 func init() {
 	gox.SetDebug(gox.DbgFlagAll)
 	cl.SetDebug(cl.DbgFlagAll)
 	gblFset = token.NewFileSet()
-	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,
+	baseConf = &cl.Config{
+		Fset:          gblFset,
+		ModRootDir:    "./internal",
+		GenGoPkg:      new(gengo.Runner).GenGoPkg,
+		CacheLoadPkgs: true,
+		NoFileLine:    true,
 	}
 }
 
-func gopClTest(t *testing.T, gopcode, expected string) {
-	gopClTestEx(t, "main", gopcode, expected)
+func gopClTest(t *testing.T, gopcode, expected string, cachefile ...string) {
+	gopClTestEx(t, "main", gopcode, expected, cachefile...)
 }
 
-func gopClTestEx(t *testing.T, pkgname, gopcode, expected string) {
+func gopClTestEx(t *testing.T, pkgname, gopcode, expected string, cachefile ...string) {
 	cl.SetDisableRecover(true)
 	defer cl.SetDisableRecover(false)
 
@@ -80,8 +65,16 @@ func gopClTestEx(t *testing.T, pkgname, gopcode, expected string) {
 		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, gblConf)
+	pkg, err := cl.NewPackage("github.com/goplus/gop/cl", bar, &conf)
 	if err != nil {
 		t.Fatal("NewPackage:", err)
 	}
@@ -94,6 +87,33 @@ func gopClTestEx(t *testing.T, pkgname, gopcode, expected string) {
 	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) {
@@ -3150,7 +3170,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()
 
@@ -3245,7 +3265,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 59569e71..4906a553 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 := *gblConf
+	conf := *baseConf.Ensure()
 	conf.NoFileLine = false
 	conf.WorkingDir = "/foo"
 	conf.TargetDir = "/foo"
diff --git a/cmd/internal/test/test.go b/cmd/internal/test/test.go
index 469cc227..26af4f18 100644
--- a/cmd/internal/test/test.go
+++ b/cmd/internal/test/test.go
@@ -19,11 +19,10 @@ 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"
@@ -79,11 +78,12 @@ func runCmd(_ *base.Command, args []string) {
 		}
 		return nil
 	})
-	baseConf := &cl.Config{}
-	runner.GenGo(dir, recursive, baseConf)
+	baseConf := &cl.Config{PersistLoadPkgs: true}
+	runner.GenGo(dir, recursive, baseConf.Ensure())
 	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 a14b8aa7..62878a21 100644
--- a/parser/parserdir_test.go
+++ b/parser/parserdir_test.go
@@ -23,7 +23,6 @@ import (
 	"path"
 	"reflect"
 	"strings"
-	"syscall"
 	"testing"
 
 	"github.com/qiniu/x/log"
@@ -156,12 +155,22 @@ func TestFromTestdata(t *testing.T) {
 
 func TestRegisterFileType(t *testing.T) {
 	RegisterFileType(".gsh", ast.FileTypeSpx)
-	if err := RegisterFileType(".gshx", ast.FileTypeGop); err != syscall.EINVAL {
-		t.Fatal("TestRegisterFileType failed:", err)
-	}
-	if err := RegisterFileType(".gsh", ast.FileTypeGmx); err != syscall.EEXIST {
-		t.Fatal("TestRegisterFileType failed:", err)
-	}
+	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)
+	}()
 }
 
 // -----------------------------------------------------------------------------
diff --git a/x/gopprojs/proj_test.go b/x/gopproj/proj_test.go
similarity index 75%
rename from x/gopprojs/proj_test.go
rename to x/gopproj/proj_test.go
index f4fae47d..e3c34d69 100644
--- a/x/gopprojs/proj_test.go
+++ b/x/gopproj/proj_test.go
@@ -1,20 +1,4 @@
-/*
- * 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
+package gopproj
 
 import "testing"
 
diff --git a/x/mod/modfile/gop_test.go b/x/mod/modfile/gop_test.go
index 0701c0ea..8d98c960 100644
--- a/x/mod/modfile/gop_test.go
+++ b/x/mod/modfile/gop_test.go
@@ -22,38 +22,6 @@ 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 = `
@@ -151,23 +119,6 @@ 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) {