Repository: goplus/gop
Pull request #1754 (2024-02-17 14:48:03)

v1.2.2


Patch:
diff --git a/README.md b/README.md
index 8448ffd2..b92e844d 100644
--- a/README.md
+++ b/README.md
@@ -44,8 +44,30 @@ The Go+ programming language is designed for engineering, STEM education, and da
 For more details, see [Quick Start](doc/docs.md).
 
 
+## Command Style Code
+
+Different from the function call style of most languages, Go+ recommends command style code:
+
+```coffee
+println "Hello world"
+```
+
+To emphasize our preference for command style, we introduce `echo` as an alias for `println`:
+
+```coffee
+echo "Hello world"
+```
+
+For more discussion on coding style, see https://tutorial.goplus.org/hello-world.
+
+
 ## Go+ Classfiles
 
+```
+One language can change the whole world.
+Go+ is a "DSL" for all domains.
+```
+
 Rob Pike once said that if he could only introduce one feature to Go, he would choose `interface` instead of `goroutine`. `classfile` is as important to Go+ as `interface` is to Go.
 
 In the design philosophy of Go+, we do not recommend `DSL` (Domain Specific Language). But `SDF` (Specific Domain Friendliness) is very important. The Go+ philosophy about `SDF` is:
@@ -57,28 +79,59 @@ Abstract domain knowledge for it.
 
 Go+ introduces `classfile` to abstract domain knowledge.
 
-Sound a bit abstract? Let's take web programming as an example. First let us initialize a hello project:
+Sound a bit abstract? Let's see some Go+ classfiles.
 
-```sh
-gop mod init hello
-```
+* STEM Education: [spx: A Go+ 2D Game Engine](https://github.com/goplus/spx)
+* Web Programming: [yap: Yet Another HTTP Web Framework](https://github.com/goplus/yap)
+* Web Programming: [yaptest: HTTP Test Framework](https://github.com/goplus/yap#yaptest-http-test-framework)
+* Web Programming: [ydb: Database Framework](https://github.com/goplus/yap#ydb-database-framework)
+* DevOps: [gsh: Go+ DevOps Tools](https://github.com/qiniu/x/tree/main/gsh)
+* Unit Test: [classfile: Unit Test](https://github.com/goplus/gop/blob/main/doc/classfile.md#classfile-unit-test)
+* Mechanism: [What's Classfile](doc/classfile.md#whats-classfile)
+* Mechanism: [Go+ Classfiles](doc/classfile.md)
 
-Then we have it reference a classfile called `yap` as the HTTP Web Framework:
 
-```sh
-gop get github.com/goplus/yap@latest
-```
+### spx: A Go+ 2D Game Engine
+
+![Screen Shot1](https://github.com/goplus/spx/blob/main/tutorial/01-Weather/1.jpg) ![Screen Shot2](https://github.com/goplus/spx/blob/main/tutorial/01-Weather/2.jpg)
 
-We can use it to implement a static file server:
+Through this example you can learn how to implement dialogues between multiple actors.
+
+Here are some codes in [Kai.spx](https://github.com/goplus/spx/blob/main/tutorial/01-Weather/Kai.spx):
 
 ```coffee
-static "/foo", FS("public")
-static "/"    # Equivalent to static "/", FS("static")
+onStart => {
+	say "Where do you come from?", 2
+	broadcast "1"
+}
 
-run ":8080"
+onMsg "2", => {
+	say "What's the climate like in your country?", 3
+	broadcast "3"
+}
 ```
 
-We can also add the ability to handle dynamic GET/POST requests:
+We call `onStart` and `onMsg` to listen events. `onStart` is called when the program is started. And `onMsg` is called when someone calls `broadcast` to broadcast a message.
+
+When the program starts, Kai says `Where do you come from?`, and then broadcasts the message `1`. Who will recieve this message? Let's see codes in [Jaime.spx](https://github.com/goplus/spx/blob/main/tutorial/01-Weather/Jaime.spx):
+
+```coffee
+onMsg "1", => {
+	say "I come from England.", 2
+	broadcast "2"
+}
+```
+
+Yes, Jaime recieves the message `1` and says `I come from England.`. Then he broadcasts the message `2`. Kai recieves it and says `What's the climate like in your country?`.
+
+The following procedures are very similar. In this way you can implement dialogues between multiple actors.
+
+See [spx: A Go+ 2D Game Engine](https://github.com/goplus/spx) for more details.
+
+
+### yap: Yet Another HTTP Web Framework
+
+Demo of serving static files and ability to handle dynamic GET/POST requests:
 
 ```coffee
 static "/foo", FS("public")
@@ -93,21 +146,28 @@ get "/p/:id", ctx => {
 run ":8080"
 ```
 
-Save this code to `hello_yap.gox` file and execute:
+If you run it and visit http://localhost:8080/p/123, you will get:
 
-```sh
-mkdir -p yap/static yap/public    # Static resources can be placed in these directories
-gop mod tidy
-gop run .
+```
+{"id":"123"}
 ```
 
-A simplest web program is running now. At this time, if you visit http://localhost:8080/p/123, you will get:
+See [yap: Yet Another HTTP Web Framework](https://github.com/goplus/yap) for more details.
 
+
+### gsh: Go+ DevOps Tools
+
+Yes, now you can write `shell script` in Go+. It supports all shell commands.
+
+Let's create a file named `./example.gsh` and write the following code:
+
+```coffee
+mkdir "testgsh"
 ```
-{"id":"123"}
-```
 
-Why is `yap` so easy to use? How does it do it? Click here to learn more about the [Go+ Classfiles](doc/classfile.md) mechanism and [YAP HTTP Web Framework](https://github.com/goplus/yap).
+Don't need a `go.mod` file, just enter `gop run ./example.gsh` directly to run.
+
+See [gsh: Go+ DevOps Tools](https://github.com/qiniu/x/tree/main/gsh) for more details.
 
 
 ## Key Features of Go+
@@ -155,8 +215,6 @@ $ brew install goplus
 
 ### from source code
 
-For now, we suggest you install Go+ from source code.
-
 Note: Requires go1.18 or later
 
 ```bash
@@ -181,26 +239,24 @@ all.bat
 * [Gobang](https://github.com/xushiwei/Gobang)
 * [Dinosaur](https://github.com/xushiwei/Dinosaur)
 
-### HTTP Web Framework
-
-* [yap: Yet Another Go/Go+ HTTP Web Framework](https://github.com/goplus/yap)
-
-### HTTP Test
+### Web Programming
 
+* [yap: Yet Another HTTP Web Framework](https://github.com/goplus/yap)
 * [yaptest: HTTP Test Framework](https://github.com/goplus/yap/tree/main/ytest)
+* [ydb: Database Framework](https://github.com/goplus/yap#ydb-database-framework)
 
-### DevOps tools
+### DevOps Tools
 
-* [Go+ DevOps Tools](https://github.com/goplus/gop/blob/main/doc/dsl-vs-sdf.md#demo-go-devops-tools)
+* [gsh: Go+ DevOps Tools](https://github.com/qiniu/x/tree/main/gsh)
 
-### Data processing
+### Data Processing
 
-* [HTML DOM Query Language for Go+](https://github.com/goplus/hdq)
+* [hdq: HTML DOM Query Language for Go+](https://github.com/goplus/hdq)
 
 
 ## IDE Plugins
 
-* vscode: https://github.com/goplus/vscode-gop
+* vscode: [Go/Go+ for Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=goplus.gop)
 
 
 ## Contributing
diff --git a/VERSION b/VERSION
deleted file mode 100644
index 6a5e98a7..00000000
--- a/VERSION
+++ /dev/null
@@ -1,1 +0,0 @@
-v1.2.1
diff --git a/build_install_run.go b/build_install_run.go
index 0a665241..5102ded2 100644
--- a/build_install_run.go
+++ b/build_install_run.go
@@ -27,7 +27,6 @@ import (
 // -----------------------------------------------------------------------------
 
 // InstallDir installs a Go+ package directory.
-// if conf != nil && conf.Context == nil, it will be set with `gox.NewContext()`.
 func InstallDir(dir string, conf *Config, install *gocmd.InstallConfig) (err error) {
 	_, _, err = GenGo(dir, conf, false)
 	if err != nil {
@@ -37,7 +36,6 @@ func InstallDir(dir string, conf *Config, install *gocmd.InstallConfig) (err err
 }
 
 // InstallPkgPath installs a Go+ package.
-// if conf != nil && conf.Context == nil, it will be set with `gox.NewContext()`.
 func InstallPkgPath(workDir, pkgPath string, conf *Config, install *gocmd.InstallConfig) (err error) {
 	localDir, recursively, err := GenGoPkgPath(workDir, pkgPath, conf, true)
 	if err != nil {
@@ -56,7 +54,6 @@ func cwdParam(recursively bool) string {
 }
 
 // InstallFiles installs specified Go+ files.
-// if conf != nil && conf.Context == nil, it will be set with `gox.NewContext()`.
 func InstallFiles(files []string, conf *Config, install *gocmd.InstallConfig) (err error) {
 	files, err = GenGoFiles("", files, conf)
 	if err != nil {
@@ -80,7 +77,6 @@ func chdir(dir string) string {
 // -----------------------------------------------------------------------------
 
 // BuildDir builds a Go+ package directory.
-// if conf != nil && conf.Context == nil, it will be set with `gox.NewContext()`.
 func BuildDir(dir string, conf *Config, build *gocmd.BuildConfig) (err error) {
 	_, _, err = GenGo(dir, conf, false)
 	if err != nil {
@@ -90,7 +86,6 @@ func BuildDir(dir string, conf *Config, build *gocmd.BuildConfig) (err error) {
 }
 
 // BuildPkgPath builds a Go+ package.
-// if conf != nil && conf.Context == nil, it will be set with `gox.NewContext()`.
 func BuildPkgPath(workDir, pkgPath string, conf *Config, build *gocmd.BuildConfig) (err error) {
 	localDir, recursively, err := GenGoPkgPath(workDir, pkgPath, conf, false)
 	if err != nil {
@@ -102,7 +97,6 @@ func BuildPkgPath(workDir, pkgPath string, conf *Config, build *gocmd.BuildConfi
 }
 
 // BuildFiles builds specified Go+ files.
-// if conf != nil && conf.Context == nil, it will be set with `gox.NewContext()`.
 func BuildFiles(files []string, conf *Config, build *gocmd.BuildConfig) (err error) {
 	files, err = GenGoFiles("", files, conf)
 	if err != nil {
@@ -128,18 +122,24 @@ func restoreDirAndMod(old string, mod os.FileMode) {
 
 // -----------------------------------------------------------------------------
 
+// If no go.mod and used Go+, use GOPROOT as buildDir.
+func getBuildDir(conf *Config) string {
+	if conf != nil && conf.GopDeps != nil && *conf.GopDeps != 0 {
+		return conf.Gop.Root
+	}
+	return ""
+}
+
 // RunDir runs an application from a Go+ package directory.
-// if conf != nil && conf.Context == nil, it will be set with `gox.NewContext()`.
 func RunDir(dir string, args []string, conf *Config, run *gocmd.RunConfig) (err error) {
 	_, _, err = GenGo(dir, conf, false)
 	if err != nil {
 		return errors.NewWith(err, `GenGo(dir, conf, false)`, -2, "gop.GenGo", dir, conf, false)
 	}
-	return gocmd.RunDir(dir, args, run)
+	return gocmd.RunDir(getBuildDir(conf), dir, args, run)
 }
 
 // RunPkgPath runs an application from a Go+ package.
-// if conf != nil && conf.Context == nil, it will be set with `gox.NewContext()`.
 func RunPkgPath(pkgPath string, args []string, chDir bool, conf *Config, run *gocmd.RunConfig) (err error) {
 	localDir, recursively, err := GenGoPkgPath("", pkgPath, conf, true)
 	if err != nil {
@@ -153,23 +153,21 @@ func RunPkgPath(pkgPath string, args []string, chDir bool, conf *Config, run *go
 		defer os.Chdir(old)
 		localDir = "."
 	}
-	return gocmd.RunDir(localDir, args, run)
+	return gocmd.RunDir("", localDir, args, run)
 }
 
 // RunFiles runs an application from specified Go+ files.
-// if conf != nil && conf.Context == nil, it will be set with `gox.NewContext()`.
 func RunFiles(autogen string, files []string, args []string, conf *Config, run *gocmd.RunConfig) (err error) {
 	files, err = GenGoFiles(autogen, files, conf)
 	if err != nil {
 		return errors.NewWith(err, `GenGoFiles(autogen, files, conf)`, -2, "gop.GenGoFiles", autogen, files, conf)
 	}
-	return gocmd.RunFiles(files, args, run)
+	return gocmd.RunFiles(getBuildDir(conf), files, args, run)
 }
 
 // -----------------------------------------------------------------------------
 
 // TestDir tests a Go+ package directory.
-// if conf != nil && conf.Context == nil, it will be set with `gox.NewContext()`.
 func TestDir(dir string, conf *Config, test *gocmd.TestConfig) (err error) {
 	_, _, err = GenGo(dir, conf, true)
 	if err != nil {
@@ -179,7 +177,6 @@ func TestDir(dir string, conf *Config, test *gocmd.TestConfig) (err error) {
 }
 
 // TestPkgPath tests a Go+ package.
-// if conf != nil && conf.Context == nil, it will be set with `gox.NewContext()`.
 func TestPkgPath(workDir, pkgPath string, conf *Config, test *gocmd.TestConfig) (err error) {
 	localDir, recursively, err := GenGoPkgPath(workDir, pkgPath, conf, false)
 	if err != nil {
@@ -191,7 +188,6 @@ func TestPkgPath(workDir, pkgPath string, conf *Config, test *gocmd.TestConfig)
 }
 
 // TestFiles tests specified Go+ files.
-// if conf != nil && conf.Context == nil, it will be set with `gox.NewContext()`.
 func TestFiles(files []string, conf *Config, test *gocmd.TestConfig) (err error) {
 	files, err = GenGoFiles("", files, conf)
 	if err != nil {
diff --git a/cl/classfile.go b/cl/classfile.go
index 725811e0..3ebf85e7 100644
--- a/cl/classfile.go
+++ b/cl/classfile.go
@@ -267,6 +267,9 @@ func gmxMainFunc(pkg *gox.Package, ctx *pkgCtx, noAutoGenMain bool) func() {
 				new := pkg.Builtin().Ref("new")
 				cb := fn.BodyStart(pkg).Val(new).Val(o).Call(1).MemberVal("Main")
 
+				// force remove //line comments for main func
+				cb.SetComments(nil, false)
+
 				sig := cb.Get(-1).Type.(*types.Signature)
 				narg := gmxMainNarg(sig)
 				if narg > 0 {
diff --git a/cl/compile.go b/cl/compile.go
index 8f3aa742..9f05b1e2 100644
--- a/cl/compile.go
+++ b/cl/compile.go
@@ -368,9 +368,10 @@ type blockCtx struct {
 	classRecv  *ast.FieldList // available when gmxSettings != nil
 	fileScope  *types.Scope   // only valid when isGopFile
 	rec        *typesRecorder
-	fileLine   bool
-	isClass    bool
-	isGopFile  bool // is Go+ file or not
+
+	fileLine  bool
+	isClass   bool
+	isGopFile bool // is Go+ file or not
 }
 
 func (bc *blockCtx) recorder() *typesRecorder {
@@ -427,7 +428,7 @@ func (p *pkgCtx) loadSymbol(name string) bool {
 	if enableRecover {
 		defer func() {
 			if e := recover(); e != nil {
-				p.handleRecover(e)
+				p.handleRecover(e, nil)
 			}
 		}()
 	}
@@ -443,16 +444,22 @@ func (p *pkgCtx) loadSymbol(name string) bool {
 	return false
 }
 
-func (p *pkgCtx) handleRecover(e interface{}) {
+func (p *pkgCtx) handleRecover(e interface{}, src ast.Node) {
+	err := p.recoverErr(e, src)
+	p.handleErr(err)
+}
+
+func (p *pkgCtx) recoverErr(e interface{}, src ast.Node) error {
 	err, ok := e.(error)
 	if !ok {
-		if msg, ok := e.(string); ok {
-			err = errors.New(msg)
+		if src != nil {
+			text := p.LoadExpr(src)
+			err = p.newCodeErrorf(src.Pos(), "compile `%v`: %v", text, e)
 		} else {
-			panic(e)
+			err = fmt.Errorf("%v", e)
 		}
 	}
-	p.handleErr(err)
+	return err
 }
 
 const (
@@ -502,7 +509,7 @@ func NewPackage(pkgPath string, pkg *ast.Package, conf *Config) (p *gox.Package,
 	if enableRecover {
 		defer func() {
 			if e := recover(); e != nil {
-				ctx.handleRecover(e)
+				ctx.handleRecover(e, nil)
 				err = ctx.errs.ToError()
 			}
 		}()
@@ -1315,7 +1322,7 @@ func loadImport(ctx *blockCtx, spec *ast.ImportSpec) {
 	if enableRecover {
 		defer func() {
 			if e := recover(); e != nil {
-				ctx.handleRecover(e)
+				ctx.handleRecover(e, spec)
 			}
 		}()
 	}
@@ -1431,7 +1438,10 @@ func loadVars(ctx *blockCtx, v *ast.ValueSpec, doc *ast.CommentGroup, global boo
 				switch e := val.(type) {
 				case *ast.LambdaExpr, *ast.LambdaExpr2:
 					if len(v.Values) == 1 {
-						sig := checkLambdaFuncType(ctx, e, typ, clLambaAssign, v.Names[0])
+						sig, err := checkLambdaFuncType(ctx, e, typ, clLambaAssign, v.Names[0])
+						if err != nil {
+							panic(err)
+						}
 						compileLambda(ctx, e, sig)
 					} else {
 						panic(ctx.newCodeErrorf(e.Pos(), "lambda unsupport multiple assignment"))
diff --git a/cl/expr.go b/cl/expr.go
index 6aa0a865..7c007366 100644
--- a/cl/expr.go
+++ b/cl/expr.go
@@ -26,6 +26,7 @@ import (
 	"reflect"
 	"strconv"
 	"strings"
+	"syscall"
 
 	"github.com/goplus/gop/ast"
 	"github.com/goplus/gop/printer"
@@ -60,35 +61,38 @@ const (
 	clIdentCanAutoCall = 1 << iota // allow auto property
 	clIdentAllowBuiltin
 	clIdentLHS
-	clIdentSelectorExpr // this ident is X of ast.SelectorExpr
+	clIdentSelectorExpr // this ident is X (not Sel) of ast.SelectorExpr
 	clIdentGoto
 	clCallWithTwoValue
 	clCommandWithoutArgs
-	clMayBuiltinNew // maybe is builtin directive `new`
+	clCommandIdent
 )
 
 const (
 	objNormal = iota
 	objPkgRef
 	objCPkgRef
+	objGopExec
 )
 
 const errorPkgPath = "github.com/qiniu/x/errors"
 
 func compileIdent(ctx *blockCtx, ident *ast.Ident, flags int) (pkg gox.PkgRef, kind int) {
 	fvalue := (flags&clIdentSelectorExpr) != 0 || (flags&clIdentLHS) == 0
+	cb := ctx.cb
 	name := ident.Name
 	if name == "_" {
 		if fvalue {
 			panic(ctx.newCodeError(ident.Pos(), "cannot use _ as value"))
 		}
-		ctx.cb.VarRef(nil)
+		cb.VarRef(nil)
 		return
 	}
 
+	var recv *types.Var
 	var oldo types.Object
 	scope := ctx.pkg.Types.Scope()
-	at, o := ctx.cb.Scope().LookupParent(name, token.NoPos)
+	at, o := cb.Scope().LookupParent(name, token.NoPos)
 	if o != nil {
 		if at != scope && at != types.Universe { // local object
 			goto find
@@ -96,19 +100,16 @@ func compileIdent(ctx *blockCtx, ident *ast.Ident, flags int) (pkg gox.PkgRef, k
 	}
 
 	if ctx.isClass { // in a Go+ class file
-		if fn := ctx.cb.Func(); fn != nil {
-			sig := fn.Ancestor().Type().(*types.Signature)
-			if recv := sig.Recv(); recv != nil {
-				ctx.cb.Val(recv)
-				chkFlag := flags // &^ clCommandWithoutArgs (TODO: why?)
-				if chkFlag&clIdentSelectorExpr != 0 {
-					chkFlag = clIdentCanAutoCall
-				}
-				if compileMember(ctx, ident, name, chkFlag) == nil { // class member object
-					return
-				}
-				ctx.cb.InternalStack().PopN(1)
+		if recv = classRecv(cb); recv != nil {
+			cb.Val(recv)
+			chkFlag := flags
+			if chkFlag&clIdentSelectorExpr != 0 { // TODO: remove this condition
+				chkFlag = clIdentCanAutoCall
+			}
+			if compileMember(ctx, ident, name, chkFlag) == nil { // class member object
+				return
 			}
+			cb.InternalStack().PopN(1)
 		}
 	}
 
@@ -151,6 +152,11 @@ func compileIdent(ctx *blockCtx, ident *ast.Ident, flags int) (pkg gox.PkgRef, k
 		}
 		oldo, o = o, obj
 	} else if o == nil {
+		// for support Gop_Exec, see TestSpxGopExec
+		if (clCommandIdent&flags) != 0 && recv != nil && gopExecVal(cb, recv, ident) == nil {
+			kind = objGopExec
+			return
+		}
 		if (clIdentGoto & flags) != 0 {
 			l := ident.Obj.Data.(*ast.Ident)
 			panic(ctx.newCodeErrorf(l.Pos(), "label %v is not defined", l.Name))
@@ -160,13 +166,13 @@ func compileIdent(ctx *blockCtx, ident *ast.Ident, flags int) (pkg gox.PkgRef, k
 
 find:
 	if fvalue {
-		ctx.cb.Val(o, ident)
+		cb.Val(o, ident)
 	} else {
-		ctx.cb.VarRef(o, ident)
+		cb.VarRef(o, ident)
 	}
 	if rec := ctx.recorder(); rec != nil {
-		e := ctx.cb.Get(-1)
-		if oldo != nil && gox.IsTypeEx(e.Type) {
+		e := cb.Get(-1)
+		if oldo != nil && gox.IsTypeEx(e.Type) { // for builtin object
 			rec.recordIdent(ctx, ident, oldo)
 			return
 		}
@@ -175,6 +181,19 @@ find:
 	return
 }
 
+func classRecv(cb *gox.CodeBuilder) *types.Var {
+	if fn := cb.Func(); fn != nil {
+		sig := fn.Ancestor().Type().(*types.Signature)
+		return sig.Recv()
+	}
+	return nil
+}
+
+func gopExecVal(cb *gox.CodeBuilder, recv *types.Var, ident *ast.Ident) error {
+	_, e := cb.Val(recv).Member("Gop_Exec", gox.MemberFlagVal, ident)
+	return e
+}
+
 func isBuiltin(o types.Object) bool {
 	if _, ok := o.(*types.Builtin); ok {
 		return ok
@@ -187,8 +206,6 @@ func compileMember(ctx *blockCtx, v ast.Node, name string, flags int) error {
 	switch {
 	case (flags & clIdentLHS) != 0:
 		mflag = gox.MemberFlagRef
-	case (flags & clCommandWithoutArgs) != 0:
-		mflag = gox.MemberFlagMethodAlias
 	case (flags & clIdentCanAutoCall) != 0:
 		mflag = gox.MemberFlagAutoProperty
 	default:
@@ -220,14 +237,53 @@ func twoValue(inFlags []int) bool {
 	return inFlags != nil && (inFlags[0]&clCallWithTwoValue) != 0
 }
 
+func identOrSelectorFlags(inFlags []int) (flags int, cmdNoArgs bool) {
+	if inFlags == nil {
+		return clIdentCanAutoCall, false
+	}
+	flags = inFlags[0]
+	if cmdNoArgs = (flags & clCommandWithoutArgs) != 0; cmdNoArgs {
+		flags &^= clCommandWithoutArgs
+	} else {
+		flags |= clIdentCanAutoCall
+	}
+	return
+}
+
+func callCmdNoArgs(ctx *blockCtx, src ast.Node, panicErr bool) (err error) {
+	if gox.IsFunc(ctx.cb.InternalStack().Get(-1).Type) {
+		if err = ctx.cb.CallWithEx(0, 0, src); err != nil {
+			if panicErr {
+				panic(err)
+			}
+		}
+	}
+	return
+}
+
 func compileExpr(ctx *blockCtx, expr ast.Expr, inFlags ...int) {
 	switch v := expr.(type) {
 	case *ast.Ident:
-		flags := clIdentCanAutoCall
-		if inFlags != nil {
-			flags |= inFlags[0]
+		flags, cmdNoArgs := identOrSelectorFlags(inFlags)
+		if cmdNoArgs {
+			flags |= clCommandIdent // for support Gop_Exec, see TestSpxGopExec
+		}
+		_, kind := compileIdent(ctx, v, flags)
+		if cmdNoArgs {
+			cb := ctx.cb
+			if kind == objGopExec {
+				cb.Val(v.Name, v)
+			} else {
+				err := callCmdNoArgs(ctx, expr, false)
+				if err == nil {
+					return
+				}
+				if !(ctx.isClass && tryGopExec(cb, v)) {
+					panic(err)
+				}
+			}
+			cb.CallWith(1, 0, v)
 		}
-		compileIdent(ctx, v, flags)
 	case *ast.BasicLit:
 		compileBasicLit(ctx, v)
 	case *ast.CallExpr:
@@ -237,11 +293,12 @@ func compileExpr(ctx *blockCtx, expr ast.Expr, inFlags ...int) {
 		}
 		compileCallExpr(ctx, v, flags)
 	case *ast.SelectorExpr:
-		flags := clIdentCanAutoCall
-		if inFlags != nil {
-			flags |= inFlags[0]
-		}
+		flags, cmdNoArgs := identOrSelectorFlags(inFlags)
 		compileSelectorExpr(ctx, v, flags)
+		if cmdNoArgs {
+			callCmdNoArgs(ctx, expr, true)
+			return
+		}
 	case *ast.BinaryExpr:
 		compileBinaryExpr(ctx, v)
 	case *ast.UnaryExpr:
@@ -282,12 +339,8 @@ func compileExpr(ctx *blockCtx, expr ast.Expr, inFlags ...int) {
 		compileErrWrapExpr(ctx, v, 0)
 	case *ast.FuncType:
 		ctx.cb.Typ(toFuncType(ctx, v, nil, nil), v)
-	case *ast.Ellipsis:
-		panic("compileEllipsis: ast.Ellipsis unexpected")
-	case *ast.KeyValueExpr:
-		panic("compileExpr: ast.KeyValueExpr unexpected")
 	default:
-		log.Panicln("compileExpr failed: unknown -", reflect.TypeOf(v))
+		log.Panicf("compileExpr failed: unknown - %T\n", v)
 	}
 	if rec := ctx.recorder(); rec != nil {
 		rec.recordExpr(ctx, expr, false)
@@ -472,9 +525,7 @@ func compilePkgRef(ctx *blockCtx, at gox.PkgRef, x *ast.Ident, flags, pkgKind in
 func identVal(ctx *blockCtx, x *ast.Ident, flags int, v types.Object, alias bool) bool {
 	autocall := false
 	if alias {
-		if (flags & clCommandWithoutArgs) != 0 {
-			autocall = true
-		} else if autocall = (flags & clIdentCanAutoCall) != 0; autocall {
+		if autocall = (flags & clIdentCanAutoCall) != 0; autocall {
 			if !gox.HasAutoProperty(v.Type()) {
 				return false
 			}
@@ -562,13 +613,20 @@ func (p *fnType) initFuncs(base int, funcs []types.Object) {
 }
 
 func compileCallExpr(ctx *blockCtx, v *ast.CallExpr, inFlags int) {
-	var flags gox.InstrFlags
+	var ifn *ast.Ident
 	switch fn := v.Fun.(type) {
 	case *ast.Ident:
-		if len(v.Args) == 1 && fn.Name == "new" { // maybe new(T)
-			flags = clMayBuiltinNew
+		if v.IsCommand() { // for support Gop_Exec, see TestSpxGopExec
+			inFlags |= clCommandIdent
+		}
+		if _, kind := compileIdent(ctx, fn, clIdentAllowBuiltin|inFlags); kind == objGopExec {
+			args := make([]ast.Expr, 1, len(v.Args)+1)
+			args[0] = toBasicLit(fn)
+			args = append(args, v.Args...)
+			v = &ast.CallExpr{Fun: fn, Args: args, Ellipsis: v.Ellipsis, NoParenEnd: v.NoParenEnd}
+		} else {
+			ifn = fn
 		}
-		compileIdent(ctx, fn, clIdentAllowBuiltin|inFlags)
 	case *ast.SelectorExpr:
 		compileSelectorExpr(ctx, fn, 0)
 	case *ast.ErrWrapExpr:
@@ -584,10 +642,14 @@ func compileCallExpr(ctx *blockCtx, v *ast.CallExpr, inFlags int) {
 	default:
 		compileExpr(ctx, fn)
 	}
-	var fnt = ctx.cb.Get(-1).Type
+	var err error
+	var stk = ctx.cb.InternalStack()
+	var base = stk.Len()
+	var fnt = stk.Get(-1).Type
+	var flags gox.InstrFlags
 	var ellipsis = v.Ellipsis != gotoken.NoPos
 	if ellipsis {
-		flags |= gox.InstrFlagEllipsis
+		flags = gox.InstrFlagEllipsis
 	}
 	if (inFlags & clCallWithTwoValue) != 0 {
 		flags |= gox.InstrFlagTwoValue
@@ -595,43 +657,80 @@ func compileCallExpr(ctx *blockCtx, v *ast.CallExpr, inFlags int) {
 	fn := &fnType{}
 	fn.load(fnt)
 	for fn != nil {
-		err := compileCallArgs(fn, ctx, v, ellipsis, flags)
-		if err == nil {
-			break
-		}
-		if fn.next == nil {
-			panic(err)
+		if err = compileCallArgs(fn, ctx, v, ellipsis, flags); err == nil {
+			if rec := ctx.recorder(); rec != nil {
+				rec.recordCallExpr(ctx, v, fnt)
+			}
+			return
 		}
+		stk.SetLen(base)
 		fn = fn.next
 	}
-	if rec := ctx.recorder(); rec != nil {
-		rec.recordCallExpr(ctx, v, fnt)
+	if ifn != nil && builtinOrGopExec(ctx, ifn, v, flags) == nil {
+		return
 	}
+	panic(err)
 }
 
-func compileCallArgs(fn *fnType, ctx *blockCtx, v *ast.CallExpr, ellipsis bool, flags gox.InstrFlags) (err error) {
+func toBasicLit(fn *ast.Ident) *ast.BasicLit {
+	return &ast.BasicLit{ValuePos: fn.NamePos, Kind: token.STRING, Value: strconv.Quote(fn.Name)}
+}
+
+// maybe builtin new/delete: see TestSpxNewObj, TestMayBuiltinDelete
+// maybe Gop_Exec: see TestSpxGopExec
+func builtinOrGopExec(ctx *blockCtx, ifn *ast.Ident, v *ast.CallExpr, flags gox.InstrFlags) error {
 	cb := ctx.cb
-	stk := cb.InternalStack()
-	n := stk.Len()
-	defer func() {
-		if r := recover(); r != nil {
-			if e, ok := r.(error); ok {
-				err = e
-			} else {
-				src := ctx.LoadExpr(v)
-				err = ctx.newCodeErrorf(v.Pos(), "compile func %v error: %v", src, r)
-			}
-			stk.SetLen(n)
+	switch name := ifn.Name; name {
+	case "new", "delete":
+		cb.InternalStack().PopN(1)
+		cb.Val(ctx.pkg.Builtin().Ref(name), ifn)
+		return fnCall(ctx, v, flags, 0)
+	default:
+		// for support Gop_Exec, see TestSpxGopExec
+		if v.IsCommand() && ctx.isClass && tryGopExec(cb, ifn) {
+			return fnCall(ctx, v, flags, 1)
 		}
-	}()
+	}
+	return syscall.ENOENT
+}
+
+func tryGopExec(cb *gox.CodeBuilder, ifn *ast.Ident) bool {
+	if recv := classRecv(cb); recv != nil {
+		cb.InternalStack().PopN(1)
+		if gopExecVal(cb, recv, ifn) == nil {
+			cb.Val(ifn.Name, ifn)
+			return true
+		}
+	}
+	return false
+}
+
+func fnCall(ctx *blockCtx, v *ast.CallExpr, flags gox.InstrFlags, extra int) error {
+	for _, arg := range v.Args {
+		compileExpr(ctx, arg)
+	}
+	return ctx.cb.CallWithEx(len(v.Args)+extra, flags, v)
+}
+
+func compileCallArgs(fn *fnType, ctx *blockCtx, v *ast.CallExpr, ellipsis bool, flags gox.InstrFlags) (err error) {
 	for i, arg := range v.Args {
 		switch expr := arg.(type) {
 		case *ast.LambdaExpr:
-			sig := checkLambdaFuncType(ctx, expr, fn.arg(i, ellipsis), clLambaArgument, v.Fun)
-			compileLambdaExpr(ctx, expr, sig)
+			sig, e := checkLambdaFuncType(ctx, expr, fn.arg(i, ellipsis), clLambaArgument, v.Fun)
+			if e != nil {
+				return e
+			}
+			if err = compileLambdaExpr(ctx, expr, sig); err != nil {
+				return
+			}
 		case *ast.LambdaExpr2:
-			sig := checkLambdaFuncType(ctx, expr, fn.arg(i, ellipsis), clLambaArgument, v.Fun)
-			compileLambdaExpr2(ctx, expr, sig)
+			sig, e := checkLambdaFuncType(ctx, expr, fn.arg(i, ellipsis), clLambaArgument, v.Fun)
+			if e != nil {
+				return e
+			}
+			if err = compileLambdaExpr2(ctx, expr, sig); err != nil {
+				return
+			}
 		case *ast.CompositeLit:
 			compileCompositeLit(ctx, expr, fn.arg(i, ellipsis), true)
 		case *ast.SliceLit:
@@ -647,9 +746,11 @@ func compileCallArgs(fn *fnType, ctx *blockCtx, v *ast.CallExpr, ellipsis bool,
 			}
 			typetype := fn.typetype && t != nil
 			if typetype {
-				stk.Pop()
+				ctx.cb.InternalStack().PopN(1)
+			}
+			if err = compileSliceLit(ctx, expr, t, true); err != nil {
+				return
 			}
-			compileSliceLit(ctx, expr, t)
 			if typetype {
 				return
 			}
@@ -657,21 +758,7 @@ func compileCallArgs(fn *fnType, ctx *blockCtx, v *ast.CallExpr, ellipsis bool,
 			compileExpr(ctx, arg)
 		}
 	}
-	if (flags & clMayBuiltinNew) != 0 { // maybe new(T): see TestSpxNewObj
-		arg := stk.Get(-1)
-		if _, ok := arg.Type.(*gox.TypeType); ok && isNonInstr(stk.Get(-2).Type) {
-			stk.PopN(2)
-			cb.Val(ctx.pkg.Builtin().Ref("new"))
-			stk.Push(arg)
-		}
-	}
-	cb.CallWith(len(v.Args), flags, v)
-	return
-}
-
-func isNonInstr(t types.Type) bool {
-	_, ok := t.(*gox.TyInstruction)
-	return !ok
+	return ctx.cb.CallWithEx(len(v.Args), flags, v)
 }
 
 type clLambaFlag string
@@ -683,7 +770,7 @@ const (
 )
 
 // check lambda func type
-func checkLambdaFuncType(ctx *blockCtx, lambda ast.Expr, ftyp types.Type, flag clLambaFlag, toNode ast.Node) *types.Signature {
+func checkLambdaFuncType(ctx *blockCtx, lambda ast.Expr, ftyp types.Type, flag clLambaFlag, toNode ast.Node) (*types.Signature, error) {
 	typ := ftyp
 retry:
 	switch t := typ.(type) {
@@ -693,26 +780,29 @@ retry:
 				break
 			}
 		}
-		return t
+		return t, nil
 	case *types.Named:
 		typ = t.Underlying()
 		goto retry
 	}
 	src := ctx.LoadExpr(toNode)
-	err := ctx.newCodeErrorf(lambda.Pos(), "cannot use lambda literal as type %v in %v to %v", ftyp, flag, src)
-	panic(err)
+	return nil, ctx.newCodeErrorf(lambda.Pos(), "cannot use lambda literal as type %v in %v to %v", ftyp, flag, src)
 }
 
 func compileLambda(ctx *blockCtx, lambda ast.Expr, sig *types.Signature) {
 	switch expr := lambda.(type) {
 	case *ast.LambdaExpr:
-		compileLambdaExpr(ctx, expr, sig)
+		if err := compileLambdaExpr(ctx, expr, sig); err != nil {
+			panic(err)
+		}
 	case *ast.LambdaExpr2:
-		compileLambdaExpr2(ctx, expr, sig)
+		if err := compileLambdaExpr2(ctx, expr, sig); err != nil {
+			panic(err)
+		}
 	}
 }
 
-func makeLambdaParams(ctx *blockCtx, pos token.Pos, lhs []*ast.Ident, in *types.Tuple) *types.Tuple {
+func makeLambdaParams(ctx *blockCtx, pos token.Pos, lhs []*ast.Ident, in *types.Tuple) (*types.Tuple, error) {
 	pkg := ctx.pkg
 	n := len(lhs)
 	if nin := in.Len(); n != nin {
@@ -724,11 +814,11 @@ func makeLambdaParams(ctx *blockCtx, pos token.Pos, lhs []*ast.Ident, in *types.
 		for i, v := range lhs {
 			has[i] = v.Name
 		}
-		panic(ctx.newCodeErrorf(
-			pos, "too %s arguments in lambda expression\n\thave (%s)\n\twant %v", fewOrMany, strings.Join(has, ", "), in))
+		return nil, ctx.newCodeErrorf(
+			pos, "too %s arguments in lambda expression\n\thave (%s)\n\twant %v", fewOrMany, strings.Join(has, ", "), in)
 	}
 	if n == 0 {
-		return nil
+		return nil, nil
 	}
 	params := make([]*types.Var, n)
 	for i, name := range lhs {
@@ -738,7 +828,7 @@ func makeLambdaParams(ctx *blockCtx, pos token.Pos, lhs []*ast.Ident, in *types.
 			rec.Def(name, param)
 		}
 	}
-	return types.NewTuple(params...)
+	return types.NewTuple(params...), nil
 }
 
 func makeLambdaResults(pkg *gox.Package, out *types.Tuple) *types.Tuple {
@@ -753,9 +843,12 @@ func makeLambdaResults(pkg *gox.Package, out *types.Tuple) *types.Tuple {
 	return types.NewTuple(results...)
 }
 
-func compileLambdaExpr(ctx *blockCtx, v *ast.LambdaExpr, sig *types.Signature) {
+func compileLambdaExpr(ctx *blockCtx, v *ast.LambdaExpr, sig *types.Signature) error {
 	pkg := ctx.pkg
-	params := makeLambdaParams(ctx, v.Pos(), v.Lhs, sig.Params())
+	params, err := makeLambdaParams(ctx, v.Pos(), v.Lhs, sig.Params())
+	if err != nil {
+		return err
+	}
 	results := makeLambdaResults(pkg, sig.Results())
 	ctx.cb.NewClosure(params, results, false).BodyStart(pkg)
 	if len(v.Lhs) > 0 {
@@ -765,11 +858,15 @@ func compileLambdaExpr(ctx *blockCtx, v *ast.LambdaExpr, sig *types.Signature) {
 		compileExpr(ctx, v)
 	}
 	ctx.cb.Return(len(v.Rhs)).End(v)
+	return nil
 }
 
-func compileLambdaExpr2(ctx *blockCtx, v *ast.LambdaExpr2, sig *types.Signature) {
+func compileLambdaExpr2(ctx *blockCtx, v *ast.LambdaExpr2, sig *types.Signature) error {
 	pkg := ctx.pkg
-	params := makeLambdaParams(ctx, v.Pos(), v.Lhs, sig.Params())
+	params, err := makeLambdaParams(ctx, v.Pos(), v.Lhs, sig.Params())
+	if err != nil {
+		return err
+	}
 	results := makeLambdaResults(pkg, sig.Results())
 	comments, once := ctx.cb.BackupComments()
 	fn := ctx.cb.NewClosure(params, results, false)
@@ -780,6 +877,7 @@ func compileLambdaExpr2(ctx *blockCtx, v *ast.LambdaExpr2, sig *types.Signature)
 	compileStmts(ctx, v.Body.List)
 	cb.End(v)
 	ctx.cb.SetComments(comments, once)
+	return nil
 }
 
 func compileFuncLit(ctx *blockCtx, v *ast.FuncLit) {
@@ -920,7 +1018,10 @@ func compileStructLitInKeyVal(ctx *blockCtx, elts []ast.Expr, t *types.Struct, t
 		}
 		switch expr := kv.Value.(type) {
 		case *ast.LambdaExpr, *ast.LambdaExpr2:
-			sig := checkLambdaFuncType(ctx, expr, t.Field(idx).Type(), clLambaField, kv.Key)
+			sig, err := checkLambdaFuncType(ctx, expr, t.Field(idx).Type(), clLambaField, kv.Key)
+			if err != nil {
+				panic(err)
+			}
 			compileLambda(ctx, expr, sig)
 		default:
 			compileExpr(ctx, kv.Value)
@@ -1039,7 +1140,14 @@ func compileCompositeLit(ctx *blockCtx, v *ast.CompositeLit, expected types.Type
 	}
 }
 
-func compileSliceLit(ctx *blockCtx, v *ast.SliceLit, typ types.Type) {
+func compileSliceLit(ctx *blockCtx, v *ast.SliceLit, typ types.Type, noPanic ...bool) (err error) {
+	if noPanic != nil {
+		defer func() {
+			if e := recover(); e != nil { // TODO: don't use defer to capture error
+				err = ctx.recoverErr(e, v)
+			}
+		}()
+	}
 	n := len(v.Elts)
 	for _, elt := range v.Elts {
 		compileExpr(ctx, elt)
@@ -1049,6 +1157,7 @@ func compileSliceLit(ctx *blockCtx, v *ast.SliceLit, typ types.Type) {
 	} else {
 		ctx.cb.SliceLitEx(typ, n, false, v)
 	}
+	return
 }
 
 func compileRangeExpr(ctx *blockCtx, v *ast.RangeExpr) {
diff --git a/cl/internal/spx/game.go b/cl/internal/spx/game.go
index 0b7123f9..6c00eed1 100644
--- a/cl/internal/spx/game.go
+++ b/cl/internal/spx/game.go
@@ -29,6 +29,15 @@ type MyGame struct {
 func Gopt_MyGame_Main(game interface{}) {
 }
 
+func (p *MyGame) Ls(n int) {}
+
+func (p *MyGame) Capout(doSth func()) (string, error) {
+	return "", nil
+}
+
+func (p *MyGame) Gop_Exec(name string, args ...any) {
+}
+
 func (p *MyGame) InitGameApp(args ...string) {
 }
 
diff --git a/cl/stmt.go b/cl/stmt.go
index 2eeec181..61dd7397 100644
--- a/cl/stmt.go
+++ b/cl/stmt.go
@@ -21,7 +21,6 @@ import (
 	"go/constant"
 	"log"
 	"path/filepath"
-	"reflect"
 
 	goast "go/ast"
 	gotoken "go/token"
@@ -48,22 +47,29 @@ func relFile(dir string, absFile string) string {
 
 func commentStmt(ctx *blockCtx, stmt ast.Stmt) {
 	if ctx.fileLine {
-		start := stmt.Pos()
-		pos := ctx.fset.Position(start)
-		if ctx.relBaseDir != "" {
-			pos.Filename = fileLineFile(ctx.relBaseDir, pos.Filename)
-		}
-		line := fmt.Sprintf("\n//line %s:%d:1", pos.Filename, pos.Line)
-		comments := &goast.CommentGroup{
-			List: []*goast.Comment{{Text: line}},
-		}
-		ctx.cb.SetComments(comments, false)
+		commentStmtEx(ctx.cb, ctx.pkgCtx, stmt)
+	}
+}
+
+func commentStmtEx(cb *gox.CodeBuilder, ctx *pkgCtx, stmt ast.Stmt) {
+	start := stmt.Pos()
+	pos := ctx.fset.Position(start)
+	if ctx.relBaseDir != "" {
+		pos.Filename = fileLineFile(ctx.relBaseDir, pos.Filename)
+	}
+	line := fmt.Sprintf("\n//line %s:%d:1", pos.Filename, pos.Line)
+	comments := &goast.CommentGroup{
+		List: []*goast.Comment{{Text: line}},
 	}
+	cb.SetComments(comments, false)
 }
 
 func commentFunc(ctx *blockCtx, fn *gox.Func, decl *ast.FuncDecl) {
 	start := decl.Name.Pos()
 	if ctx.fileLine && start != token.NoPos {
+		if decl.Doc != nil {
+			start = decl.Doc.Pos()
+		}
 		pos := ctx.fset.Position(start)
 		if ctx.relBaseDir != "" {
 			pos.Filename = fileLineFile(ctx.relBaseDir, pos.Filename)
@@ -75,11 +81,10 @@ func commentFunc(ctx *blockCtx, fn *gox.Func, decl *ast.FuncDecl) {
 			line = fmt.Sprintf("//line %s:%d:1", pos.Filename, pos.Line)
 		}
 		doc := &goast.CommentGroup{}
+		doc.List = append(doc.List, &goast.Comment{Text: line})
 		if decl.Doc != nil {
 			doc.List = append(doc.List, decl.Doc.List...)
-			doc.List = append(doc.List, &goast.Comment{Text: "//"})
 		}
-		doc.List = append(doc.List, &goast.Comment{Text: line})
 		fn.SetComments(ctx.pkg, doc)
 	} else if decl.Doc != nil {
 		fn.SetComments(ctx.pkg, decl.Doc)
@@ -102,7 +107,7 @@ func compileStmt(ctx *blockCtx, stmt ast.Stmt) {
 	if enableRecover {
 		defer func() {
 			if e := recover(); e != nil {
-				ctx.handleRecover(e)
+				ctx.handleRecover(e, stmt)
 				ctx.cb.ResetStmt()
 			}
 		}()
@@ -110,14 +115,9 @@ func compileStmt(ctx *blockCtx, stmt ast.Stmt) {
 	commentStmt(ctx, stmt)
 	switch v := stmt.(type) {
 	case *ast.ExprStmt:
-		inFlags := 0
-		if isCommandWithoutArgs(v.X) {
-			inFlags = clCommandWithoutArgs
-		}
-		compileExpr(ctx, v.X, inFlags)
-		if inFlags != 0 && gox.IsFunc(ctx.cb.InternalStack().Get(-1).Type) {
-			ctx.cb.CallWith(0, 0, v.X)
-		}
+		x := v.X
+		inFlags := checkCommandWithoutArgs(x)
+		compileExpr(ctx, x, inFlags)
 	case *ast.AssignStmt:
 		compileAssignStmt(ctx, v)
 	case *ast.ReturnStmt:
@@ -158,21 +158,21 @@ func compileStmt(ctx *blockCtx, stmt ast.Stmt) {
 	case *ast.EmptyStmt:
 		// do nothing
 	default:
-		log.Panicln("TODO - compileStmt failed: unknown -", reflect.TypeOf(v))
+		log.Panicf("compileStmt failed: unknown - %T\n", v)
 	}
 	ctx.cb.EndStmt()
 }
 
-func isCommandWithoutArgs(x ast.Expr) bool {
+func checkCommandWithoutArgs(x ast.Expr) int {
 retry:
-	switch t := x.(type) {
-	case *ast.Ident:
-		return true
-	case *ast.SelectorExpr:
-		x = t.X
+	if v, ok := x.(*ast.SelectorExpr); ok {
+		x = v.X
 		goto retry
 	}
-	return false
+	if _, ok := x.(*ast.Ident); ok {
+		return clCommandWithoutArgs
+	}
+	return 0
 }
 
 func compileReturnStmt(ctx *blockCtx, expr *ast.ReturnStmt) {
@@ -277,7 +277,10 @@ func compileAssignStmt(ctx *blockCtx, expr *ast.AssignStmt) {
 		case *ast.LambdaExpr, *ast.LambdaExpr2:
 			if len(expr.Lhs) == 1 && len(expr.Rhs) == 1 {
 				typ := ctx.cb.Get(-1).Type.(interface{ Elem() types.Type }).Elem()
-				sig := checkLambdaFuncType(ctx, e, typ, clLambaAssign, expr.Lhs[0])
+				sig, err := checkLambdaFuncType(ctx, e, typ, clLambaAssign, expr.Lhs[0])
+				if err != nil {
+					panic(err)
+				}
 				compileLambda(ctx, e, sig)
 			} else {
 				panic(ctx.newCodeErrorf(e.Pos(), "lambda unsupport multiple assignment"))
diff --git a/cmd/internal/build/build.go b/cmd/internal/build/build.go
index fdee3a44..4879418e 100644
--- a/cmd/internal/build/build.go
+++ b/cmd/internal/build/build.go
@@ -74,7 +74,7 @@ func runCmd(cmd *base.Command, args []string) {
 		log.Panicln("too many arguments:", args)
 	}
 
-	conf, err := gop.NewDefaultConf(".")
+	conf, err := gop.NewDefaultConf(".", true)
 	if err != nil {
 		log.Panicln("gop.NewDefaultConf:", err)
 	}
diff --git a/cmd/internal/gengo/go.go b/cmd/internal/gengo/go.go
index be9fa033..1ec8c305 100644
--- a/cmd/internal/gengo/go.go
+++ b/cmd/internal/gengo/go.go
@@ -71,7 +71,7 @@ func runCmd(cmd *base.Command, args []string) {
 		cl.SetDisableRecover(true)
 	}
 
-	conf, err := gop.NewDefaultConf(".")
+	conf, err := gop.NewDefaultConf(".", false)
 	if err != nil {
 		log.Panicln("gop.NewDefaultConf:", err)
 	}
diff --git a/cmd/internal/install/install.go b/cmd/internal/install/install.go
index f7962a7b..0c510c2c 100644
--- a/cmd/internal/install/install.go
+++ b/cmd/internal/install/install.go
@@ -71,7 +71,7 @@ func runCmd(cmd *base.Command, args []string) {
 		cl.SetDisableRecover(true)
 	}
 
-	conf, err := gop.NewDefaultConf(".")
+	conf, err := gop.NewDefaultConf(".", true)
 	if err != nil {
 		log.Panicln("gop.NewDefaultConf:", err)
 	}
diff --git a/cmd/internal/run/run.go b/cmd/internal/run/run.go
index e2a8b004..8adead75 100644
--- a/cmd/internal/run/run.go
+++ b/cmd/internal/run/run.go
@@ -80,10 +80,13 @@ func runCmd(cmd *base.Command, args []string) {
 	}
 
 	noChdir := *flagNoChdir
-	conf, err := gop.NewDefaultConf(".")
+	conf, err := gop.NewDefaultConf(".", true)
 	if err != nil {
 		log.Panicln("gop.NewDefaultConf:", err)
 	}
+	if !conf.Mod.HasModfile() { // if no go.mod, check GopDeps
+		conf.GopDeps = new(int)
+	}
 	confCmd := &gocmd.Config{Gop: conf.Gop}
 	confCmd.Flags = pass.Args
 	run(proj, args, !noChdir, conf, confCmd)
diff --git a/doc/classfile.md b/doc/classfile.md
index 09a940e7..6b11b7a3 100644
--- a/doc/classfile.md
+++ b/doc/classfile.md
@@ -1,6 +1,11 @@
 Go+ Classfiles
 =====
 
+```
+One language can change the world.
+Go+ is a "DSL" for all domains.
+```
+
 Rob Pike once said that if he could only introduce one feature to Go, he would choose `interface` instead of `goroutine`. `classfile` is as important to Go+ as `interface` is to Go.
 
 In the design philosophy of Go+, we do not recommend `DSL` (Domain Specific Language). But `SDF` (Specific Domain Friendliness) is very important. The Go+ philosophy about `SDF` is:
@@ -12,6 +17,14 @@ Abstract domain knowledge for it.
 
 Go+ introduces `classfile` to abstract domain knowledge.
 
+* STEM Education: [spx: A Go+ 2D Game Engine](https://github.com/goplus/spx)
+* Web Programming: [yap: Yet Another HTTP Web Framework](https://github.com/goplus/yap)
+* Web Programming: [yaptest: HTTP Test Framework](https://github.com/goplus/yap#yaptest-http-test-framework)
+* Web Programming: [ydb: Database Framework](https://github.com/goplus/yap#ydb-database-framework)
+* DevOps: [gsh: Go+ DevOps Tools](https://github.com/qiniu/x/tree/main/gsh)
+* Unit Test: [classfile: Unit Test](#classfile-unit-test)
+* Mechanism: [What's Classfile](#whats-classfile)
+
 Sound a bit abstract? Let's take web programming as an example. First let us initialize a hello project:
 
 ```sh
@@ -85,7 +98,7 @@ Then we create `hello.gop` file in the same directory:
 
 ```go
 rect := &Rect{10, 20}
-printlnrect.area
+println rect.area
 ```
 
 Then we execute `gop run .` to run it and get the result:
@@ -132,7 +145,7 @@ This is a bit abstract. Let's take the [2D Game Engine spx](https://github.com/g
 
 How does Go+ identify various class files of a classfile? by its filename. By convention, if we define a classfile called `foo`, then its project class is usually called `main_foo.gox`, and the worker class is usually called `xxx_foo.gox`. If this classfile does not have a worker class, then the project class only needs to ensure that the suffix is `_foo.gox`, and the class name can be freely chosen. For example, our previous `yap` framework only has project class, so a file name like `hello_yap.gox` can be correctly recognized.
 
-The earliest version of Go+ allows classfiles to be identified through custom file extensions. For example, the project class of the `spx classfile` is called `main.spx`, and the worker class is called `xxx.spx`. Although this ability to customize extensions is still retained for now, we do not recommend its use and there is no guarantee that it will continue to be available in the future. `spx` will likely be the only classfile to use a custom extension.
+The earliest version of Go+ allows classfiles to be identified through custom file extensions. For example, the project class of the `spx classfile` is called `main.spx`, and the worker class is called `xxx.spx`. Although this ability to customize extensions is still retained for now, we do not recommend its use and there is no guarantee that it will continue to be available in the future.
 
 
 ### classfile: Unit Test
@@ -174,23 +187,11 @@ If you want to run a subtest case, use `t.run`.
 
 ### yap: Yet Another Go/Go+ HTTP Web Framework
 
-This classfile has the file suffix `_yap.gox`.
-
-Before using `yap`, you need to add it to `go.mod` by using `go get`:
-
-```sh
-go get github.com/goplus/yap@latest
-```
-
-Find `require github.com/goplus/yap` statement in `go.mod` and add `//gop:class` at the end of the line:
-
-```go.mod
-require github.com/goplus/yap v0.7.2 //gop:class
-```
+This classfile has the file suffix `_yap.gox`. See [yap: Yet Another HTTP Web Framework](https://github.com/goplus/yap) for more details.
 
 #### Router and Parameters
 
-demo in Go+ classfile ([hello_yap.gox](https://github.com/goplus/yap/blob/v0.7.2/demo/classfile_hello/hello_yap.gox)):
+demo in Go+ classfile ([hello_yap.gox](https://github.com/goplus/yap/blob/main/demo/classfile_hello/hello_yap.gox)):
 
 ```coffee
 get "/p/:id", ctx => {
@@ -207,7 +208,7 @@ run ":8080"
 
 #### Static files
 
-Static files server demo in Go+ classfile ([staticfile_yap.gox](https://github.com/goplus/yap/blob/v0.7.2/demo/classfile_static/staticfile_yap.gox)):
+Static files server demo in Go+ classfile ([staticfile_yap.gox](https://github.com/goplus/yap/blob/main/demo/classfile_static/staticfile_yap.gox)):
 
 ```coffee
 static "/foo", FS("public")
@@ -218,7 +219,7 @@ run ":8080"
 
 #### YAP Template
 
-demo in Go+ classfile ([blog_yap.gox](https://github.com/goplus/yap/blob/v0.7.2/demo/classfile_blog/blog_yap.gox), [article_yap.html](https://github.com/goplus/yap/blob/v0.7.2/demo/classfile_blog/yap/article_yap.html)):
+demo in Go+ classfile ([blog_yap.gox](https://github.com/goplus/yap/blob/main/demo/classfile_blog/blog_yap.gox), [article_yap.html](https://github.com/goplus/yap/blob/main/demo/classfile_blog/yap/article_yap.html)):
 
 ```coffee
 get "/p/:id", ctx => {
@@ -230,17 +231,56 @@ get "/p/:id", ctx => {
 run ":8080"
 ```
 
+
 ### yaptest: HTTP Test Framework
 
-This classfile has the file suffix `_ytest.gox`.
+Suppose we have a web server named `foo` ([demo/foo/foo_yap.gox](https://github.com/goplus/yap/blob/main/ytest/demo/foo/foo_yap.gox)):
+
+```go
+get "/p/:id", ctx => {
+	ctx.json {
+		"id": ctx.param("id"),
+	}
+}
+
+run ":8080"
+```
+
+Then we create a yaptest file ([demo/foo/foo_ytest.gox](https://github.com/goplus/yap/blob/main/ytest/demo/foo/foo_ytest.gox)):
+
+```go
+mock "foo.com", new(foo)
+
+run "test get /p/$id", => {
+	id := "123"
+	get "http://foo.com/p/${id}"
+	ret 200
+	json {
+		"id": id,
+	}
+}
+```
+
+The directive `mock` creates the `foo` server by [mockhttp](https://pkg.go.dev/github.com/qiniu/x/mockhttp). Then we call the directive `run` to run a subtest.
 
-Before using `yaptest`, you need to add `yap` to `go.mod`:
+You can change the directive `mock` to `testServer` (see [demo/foo/bar_ytest.gox](https://github.com/goplus/yap/blob/main/ytest/demo/foo/bar_ytest.gox)), and keep everything else unchanged:
 
-```go.mod
-require github.com/goplus/yap v0.7.2 //gop:class
+```go
+testServer "foo.com", new(foo)
+
+run "test get /p/$id", => {
+	id := "123"
+	get "http://foo.com/p/${id}"
+	ret 200
+	json {
+		"id": id,
+	}
+}
 ```
 
-Then you can create a `example_ytest.gox` file to test your HTTP server:
+The directive `testServer` creates the `foo` server by [net/http/httptest](https://pkg.go.dev/net/http/httptest#NewServer) and obtained a random port as the service address. Then it calls the directive [host](https://pkg.go.dev/github.com/goplus/yap/ytest#App.Host) to map the random service address to `foo.com`. This makes all other code no need to changed.
+
+We can change this example more complicated:
 
 ```coffee
 host "https://example.com", "http://localhost:8080"
@@ -292,23 +332,18 @@ run "matchJsonObject", => {
 }
 ```
 
-### spx: A Go+ 2D Game Engine for STEM education
-
-This classfile has the file suffix `.spx`. It is the earliest classfile in the world.
+For more details, see [yaptest - Go+ HTTP Test Framework](https://github.com/goplus/yap/blob/main/ytest).
 
-Before using `spx`, you need to add it to `go.mod` by using `go get`:
 
-```sh
-go get github.com/goplus/spx@latest
-```
+### spx: A Go+ 2D Game Engine for STEM education
 
-It's also a built-in classfile so you don't need append `//gop:class` after `require github.com/goplus/spx`.
+This classfile has the file suffix `.spx`. It is the earliest classfile in the world.
 
 #### tutorial/01-Weather
 
 ![Screen Shot1](https://github.com/goplus/spx/blob/main/tutorial/01-Weather/1.jpg) ![Screen Shot2](https://github.com/goplus/spx/blob/main/tutorial/01-Weather/2.jpg)
 
-Through this example you can learn how to listen events and do somethings.
+Through this example you can learn how to implement dialogues between multiple actors.
 
 Here are some codes in [Kai.spx](https://github.com/goplus/spx/blob/main/tutorial/01-Weather/Kai.spx):
 
diff --git a/doc/dsl-vs-sdf.md b/doc/dsl-vs-sdf.md
deleted file mode 100644
index b3df6dfc..00000000
--- a/doc/dsl-vs-sdf.md
+++ /dev/null
@@ -1,174 +0,0 @@
-Go+ ClassFile: DSL vs. SDF
-=====
-
-Go+ doesn't support DSL (Domain Specific Language), but it's SDF (Specific Domain Friendly).
-
-The Go+ philosophy of Domain is:
-
-```
-Don't define a language for specific domain.
-Abstract domain knowledge for it.
-```
-
-Go+ introduces `ClassFile` to abstract domain knowledge.
-
-NOTE: `ClassFile` is still in beta and may be subject to change.
-
-What is `ClassFile`?
-
-TODO
-
-
-## DEMO: Go+ DevOps Tools
-
-This is a demo project to implement a new `ClassFile` work: [github.com/xushiwei/gsh](https://github.com/xushiwei/gsh).
-
-Before we dive into implementation of `github.com/xushiwei/gsh`, let's take a look how to use it.
-
-First, let's create a new module named [gshexample](https://github.com/xushiwei/gshexample):
-
-```sh
-mkdir gshexample
-cd gshexample
-gop mod init gshexample
-```
-
-Second, get `github.com/xushiwei/gsh` into `gshexample`:
-
-```sh
-gop get github.com/xushiwei/gsh
-```
-
-Third, create a Go+ source file named `./example.gsh` and write the following code:
-
-```coffee
-mkdir "testgsh"
-```
-
-Fourth, run this project:
-
-```sh
-gop mod tidy
-gop run .
-```
-
-It's strange to you that the file extension of Go+ source is not `.gop` but `.gsh`.
-
-But the Go+ compiler can recognize `.gsh` files as Go+ source because `github.com/xushiwei/gsh` register `.gsh` as its `ClassFile` file extension.
-
-We can change `./example.gsh` more complicated:
-
-```coffee
-type file struct {
-	name  string
-	fsize int
-}
-
-mkdir! "testgsh"
-
-mkdir "testgsh2"
-lastErr!
-
-mkdir "testgsh3"
-if lastErr != nil {
-	panic lastErr
-}
-
-capout => { ls }
-println output.fields
-
-capout => { ls "-l" }
-files := [file{flds[8], flds[4].int!} for e <- output.split("\n") if flds := e.fields; flds.len > 2]
-println files
-
-rmdir "testgsh", "testgsh2", "testgsh3"
-```
-
-### Check last error
-
-If we want to ensure `mkdir` successfully, there are three ways:
-
-The simplest way is:
-
-```coffee
-mkdir! "testsh"  # will panic if mkdir failed
-```
-
-The second way is:
-
-```coffee
-mkdir "testsh"
-lastErr!
-```
-
-Yes, `github.com/xushiwei/gsh` provides `lastErr` to check last error.
-
-The third way is:
-
-```coffee
-mkdir "testsh"
-if lastErr != nil {
-    panic lastErr
-}
-```
-
-This is the most familiar way to Go developers.
-
-### Capture output of commands
-
-And, `github.com/xushiwei/gsh` provides a way to capture output of commands:
-
-```coffee
-capout => {
-    ...
-}
-```
-
-Similar to `lastErr`, the captured output result is saved to `output`.
-
-For an example:
-
-```coffee
-capout => { ls "-l" }
-println output
-```
-
-Here is a possible output:
-
-```s
-total 72
--rw-r--r--  1 xushiwei  staff  11357 Jun 19 00:20 LICENSE
--rw-r--r--  1 xushiwei  staff    127 Jun 19 10:00 README.md
--rw-r--r--  1 xushiwei  staff    365 Jun 19 00:25 example.gsh
--rw-r--r--  1 xushiwei  staff    126 Jun 19 09:33 go.mod
--rw-r--r--  1 xushiwei  staff    165 Jun 19 09:33 go.sum
--rw-r--r--  1 xushiwei  staff   1938 Jun 19 10:00 gop_autogen.go
-```
-
-We can use [Go+ powerful built-in data processing capabilities](doc/docs.md#data-processing) to process captured `output`:
-
-```coffee
-type file struct {
-	name  string
-	fsize int
-}
-
-files := [file{flds[8], flds[4].int!} for e <- output.split("\n") if flds := e.fields; flds.len > 2]
-```
-
-In this example, we split `output` by `"\n"`, and for each entry `e`, split it by spaces (`e.fields`) and save into `flds`. Condition `flds.len > 2` is to remove special line of output:
-
-```s
-total 72
-```
-
-At last, pick file name and size of all selected entries and save into `files`.
-
-
-### Implementation of gsh
-
-To most developers, they just use `ClassFile`. They don't need to implement a `ClassFile` project.
-
-We provide a `ClassFile` template project: [github.com/goplus/classfile-project-template](https://github.com/goplus/classfile-project-template).
-
-TODO
diff --git a/gengo.go b/gengo.go
index 29becfdb..df3868e3 100644
--- a/gengo.go
+++ b/gengo.go
@@ -49,13 +49,11 @@ const (
 // -----------------------------------------------------------------------------
 
 // GenGo generates gop_autogen.go for a Go+ package directory.
-// if conf != nil && conf.Context == nil, it will be set with `gox.NewContext()`.
 func GenGo(dir string, conf *Config, genTestPkg bool) (string, bool, error) {
 	return GenGoEx(dir, conf, genTestPkg, 0)
 }
 
 // GenGoEx generates gop_autogen.go for a Go+ package directory.
-// if conf != nil && conf.Context == nil, it will be set with `gox.NewContext()`.
 func GenGoEx(dir string, conf *Config, genTestPkg bool, flags GenFlags) (string, bool, error) {
 	recursively := strings.HasSuffix(dir, "/...")
 	if recursively {
@@ -214,7 +212,6 @@ const (
 )
 
 // GenGoPkgPath generates gop_autogen.go for a Go+ package.
-// if conf != nil && conf.Context == nil, it will be set with `gox.NewContext()`.
 func GenGoPkgPath(workDir, pkgPath string, conf *Config, allowExtern bool) (localDir string, recursively bool, err error) {
 	return GenGoPkgPathEx(workDir, pkgPath, conf, allowExtern, 0)
 }
@@ -233,7 +230,6 @@ func remotePkgPath(pkgPath string, conf *Config, recursively bool, flags GenFlag
 }
 
 // GenGoPkgPathEx generates gop_autogen.go for a Go+ package.
-// if conf != nil && conf.Context == nil, it will be set with `gox.NewContext()`.
 func GenGoPkgPathEx(workDir, pkgPath string, conf *Config, allowExtern bool, flags GenFlags) (localDir string, recursively bool, err error) {
 	recursively = strings.HasSuffix(pkgPath, "/...")
 	if recursively {
@@ -276,8 +272,7 @@ func remotePkgPathDo(pkgPath string, doSth func(pkgDir, modDir string), onErr fu
 // -----------------------------------------------------------------------------
 
 // GenGoFiles generates gop_autogen.go for specified Go+ files.
-// if conf != nil && conf.Context == nil, it will be set with `gox.NewContext()`.
-func GenGoFiles(autogen string, files []string, conf *Config) (result []string, err error) {
+func GenGoFiles(autogen string, files []string, conf *Config) (outFiles []string, err error) {
 	if conf == nil {
 		conf = new(Config)
 	}
@@ -296,11 +291,11 @@ func GenGoFiles(autogen string, files []string, conf *Config) (result []string,
 		err = errors.NewWith(err, `LoadFiles(files, conf)`, -2, "gop.LoadFiles", files, conf)
 		return
 	}
-	result = append(result, autogen)
 	err = out.WriteFile(autogen)
 	if err != nil {
 		err = errors.NewWith(err, `out.WriteFile(autogen)`, -2, "(*gox.Package).WriteFile", out, autogen)
 	}
+	outFiles = []string{autogen}
 	return
 }
 
diff --git a/go.mod b/go.mod
index d67977e1..c0a02ec9 100644
--- a/go.mod
+++ b/go.mod
@@ -4,11 +4,11 @@ go 1.18
 
 require (
 	github.com/fsnotify/fsnotify v1.7.0
-	github.com/goplus/c2go v0.7.22
-	github.com/goplus/gox v1.14.10
-	github.com/goplus/mod v0.13.5
-	github.com/qiniu/x v1.13.5
-	golang.org/x/tools v0.17.0
+	github.com/goplus/c2go v0.7.23
+	github.com/goplus/gox v1.14.12
+	github.com/goplus/mod v0.13.7
+	github.com/qiniu/x v1.13.8
+	golang.org/x/tools v0.18.0
 )
 
 require (
@@ -16,7 +16,7 @@ require (
 	github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
 	github.com/modern-go/reflect2 v1.0.2 // indirect
 	golang.org/x/mod v0.15.0 // indirect
-	golang.org/x/sys v0.16.0 // indirect
+	golang.org/x/sys v0.17.0 // indirect
 )
 
 retract v1.1.12
diff --git a/go.sum b/go.sum
index bebcbb3f..a2b811ca 100644
--- a/go.sum
+++ b/go.sum
@@ -4,12 +4,12 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
 github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
 github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
-github.com/goplus/c2go v0.7.22 h1:TnOf6VJTvVKFhp8+zTP6POwJ894mdNGV7qKRYY1o2oY=
-github.com/goplus/c2go v0.7.22/go.mod h1:f3v+Td4JQwhToKfQK5tGvjMoMBrIR9IJjHNqzDC6NIE=
-github.com/goplus/gox v1.14.10 h1:fbcA/BzFqEOp28BQjt27UR3DkYYpT4uXObWxZXEDkbU=
-github.com/goplus/gox v1.14.10/go.mod h1:G7Hz+cAOUyJyN9pPHrpqhfQPDUtiJNmoRVTwoaQ9nw0=
-github.com/goplus/mod v0.13.5 h1:rEQ4UFmLKU1J5IK82DwC/sAKIhAMJZ2VbBrKU/k448c=
-github.com/goplus/mod v0.13.5/go.mod h1:PBGRYvD381NxJdeI+KyYHiDX9fgnDiuttzyCTG9LfHE=
+github.com/goplus/c2go v0.7.23 h1:MccDB9xBwDg9Bb9w+S9Lzbhg47ZtRFZHzqkr2OvxarM=
+github.com/goplus/c2go v0.7.23/go.mod h1:xBHI3uJ7hzQEYhw8Ql//1DHfJeuEgO5119QyoAs6JfY=
+github.com/goplus/gox v1.14.12 h1:VIrkU4T/MQ48olkTGgNk5ybF4U7YqkzIAv7By10gf9Y=
+github.com/goplus/gox v1.14.12/go.mod h1:6b6XYHmyiCevhwuEHcV/jzm7Z2FXLDBhuxgvkjceA+o=
+github.com/goplus/mod v0.13.7 h1:2tIe3cT/fmV+rOoPf896OH+AYcRu94Q0IeJU1LHVU4o=
+github.com/goplus/mod v0.13.7/go.mod h1:edZE5Qs+9mRnIjvWhyMNUkm5ayJ+dMlJ3J3YeXODNwA=
 github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
 github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
@@ -18,8 +18,8 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
 github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/qiniu/x v1.13.5 h1:YJz5i3RD/7srMLEmp7UAcYmZ9qrhOPk0EL0pGiuxGbA=
-github.com/qiniu/x v1.13.5/go.mod h1:INZ2TSWSJVWO/RuELQROERcslBwVgFG7MkTfEdaQz9E=
+github.com/qiniu/x v1.13.8 h1:xXXD1LuTw4VaqTWxFKonm0F6XlhQNE59+vgPYP3ij8Q=
+github.com/qiniu/x v1.13.8/go.mod h1:INZ2TSWSJVWO/RuELQROERcslBwVgFG7MkTfEdaQz9E=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
@@ -27,11 +27,10 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
-golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
+golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
-golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
 golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8=
 golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -40,7 +39,7 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
 golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
 golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
 golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
-golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
+golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -55,14 +54,15 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
-golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
+golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/telemetry v0.0.0-20240208230135-b75ee8823808/go.mod h1:KG1lNk5ZFNssSZLrpVb4sMXKMpGwGXOxSG3rnu2gZQQ=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
 golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
 golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
-golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
+golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
@@ -75,6 +75,6 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
 golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
 golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
-golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc=
-golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps=
+golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ=
+golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/imp.go b/imp.go
index 4b18f3bf..95531cbb 100644
--- a/imp.go
+++ b/imp.go
@@ -37,7 +37,8 @@ type Importer struct {
 	mod     *gopmod.Module
 	gop     *env.Gop
 	fset    *token.FileSet
-	flags   GenFlags
+
+	Flags GenFlags // can change this for loading Go+ modules
 }
 
 func NewImporter(mod *gopmod.Module, gop *env.Gop, fset *token.FileSet) *Importer {
@@ -48,21 +49,23 @@ func NewImporter(mod *gopmod.Module, gop *env.Gop, fset *token.FileSet) *Importe
 		mod = gopmod.Default
 	}
 	dir := ""
-	if hasModfile(mod) {
+	if mod.HasModfile() {
 		dir = mod.Root()
 	}
 	impFrom := packages.NewImporter(fset, dir)
-	return &Importer{mod: mod, gop: gop, impFrom: impFrom, fset: fset, flags: defaultFlags}
+	return &Importer{mod: mod, gop: gop, impFrom: impFrom, fset: fset, Flags: defaultFlags}
 }
 
+const (
+	gopMod = "github.com/goplus/gop"
+)
+
+// Import imports a Go/Go+ package.
 func (p *Importer) Import(pkgPath string) (pkg *types.Package, err error) {
-	const (
-		gop = "github.com/goplus/gop"
-	)
-	if strings.HasPrefix(pkgPath, gop) {
-		if suffix := pkgPath[len(gop):]; suffix == "" || suffix[0] == '/' {
+	if strings.HasPrefix(pkgPath, gopMod) {
+		if suffix := pkgPath[len(gopMod):]; suffix == "" || suffix[0] == '/' {
 			gopRoot := p.gop.Root
-			if suffix == "/cl/internal/gop-in-go/foo" {
+			if suffix == "/cl/internal/gop-in-go/foo" { // for test github.com/goplus/gop/cl
 				if err = p.genGoExtern(gopRoot+suffix, false); err != nil {
 					return
 				}
@@ -70,12 +73,12 @@ func (p *Importer) Import(pkgPath string) (pkg *types.Package, err error) {
 			return p.impFrom.ImportFrom(pkgPath, gopRoot, 0)
 		}
 	}
-	if mod := p.mod; hasModfile(mod) {
+	if isPkgInMod(pkgPath, "github.com/qiniu/x") {
+		return p.impFrom.ImportFrom(pkgPath, p.gop.Root, 0)
+	}
+	if mod := p.mod; mod.HasModfile() {
 		ret, e := mod.Lookup(pkgPath)
 		if e != nil {
-			if isPkgInMod(pkgPath, "github.com/qiniu/x") {
-				return p.impFrom.ImportFrom(pkgPath, p.gop.Root, 0)
-			}
 			return nil, e
 		}
 		switch ret.Type {
@@ -113,7 +116,7 @@ func (p *Importer) genGoExtern(dir string, isExtern bool) (err error) {
 			defer os.Chmod(dir, modReadonly)
 		}
 		gen := false
-		err = genGoIn(dir, &Config{Gop: p.gop, Importer: p, Fset: p.fset}, false, p.flags, &gen)
+		err = genGoIn(dir, &Config{Gop: p.gop, Importer: p, Fset: p.fset}, false, p.Flags, &gen)
 		if err != nil {
 			return
 		}
diff --git a/load.go b/load.go
index 214a14ee..ea58ce56 100644
--- a/load.go
+++ b/load.go
@@ -21,6 +21,7 @@ import (
 	"go/types"
 	"io/fs"
 	"os"
+	"path"
 	"strings"
 
 	"github.com/goplus/gop/ast"
@@ -122,11 +123,16 @@ type Config struct {
 
 	Filter func(fs.FileInfo) bool
 
+	// If not nil, it is used for returning result of checks Go+ dependencies.
+	// see https://pkg.go.dev/github.com/goplus/gox#File.CheckGopDeps
+	GopDeps *int
+
 	IgnoreNotatedError bool
 	DontUpdateGoMod    bool
 }
 
-func NewDefaultConf(dir string) (conf *Config, err error) {
+// NewDefaultConf creates a dfault configuration for common cases.
+func NewDefaultConf(dir string, noTestFile bool) (conf *Config, err error) {
 	mod, err := LoadMod(dir)
 	if err != nil {
 		return
@@ -134,32 +140,50 @@ func NewDefaultConf(dir string) (conf *Config, err error) {
 	gop := gopenv.Get()
 	fset := token.NewFileSet()
 	imp := NewImporter(mod, gop, fset)
-	return &Config{Gop: gop, Fset: fset, Mod: mod, Importer: imp}, nil
+	conf = &Config{Gop: gop, Fset: fset, Mod: mod, Importer: imp}
+	if noTestFile {
+		conf.Filter = FilterNoTestFiles
+	}
+	return
 }
 
+// LoadMod loads a Go+ module from a specified directory.
 func LoadMod(dir string) (mod *gopmod.Module, err error) {
 	mod, err = gopmod.Load(dir)
-	if err != nil && !NotFound(err) {
+	if err != nil && !gopmod.IsNotFound(err) {
 		err = errors.NewWith(err, `gopmod.Load(dir, 0)`, -2, "gopmod.Load", dir, 0)
 		return
 	}
-	if mod != nil {
-		err = mod.ImportClasses()
-		if err != nil {
-			err = errors.NewWith(err, `mod.RegisterClasses()`, -2, "(*gopmod.Module).RegisterClasses", mod)
-		}
-		return
+	if mod == nil {
+		mod = gopmod.Default
 	}
-	return gopmod.Default, nil
+	err = mod.ImportClasses()
+	if err != nil {
+		err = errors.NewWith(err, `mod.ImportClasses()`, -2, "(*gopmod.Module).ImportClasses", mod)
+	}
+	return
 }
 
-func hasModfile(mod *gopmod.Module) bool {
-	f := mod.File
-	return f != nil && f.Syntax != nil
+// FilterNoTestFiles filters to skip all testing files.
+func FilterNoTestFiles(fi fs.FileInfo) bool {
+	fname := fi.Name()
+	suffix := ""
+	switch path.Ext(fname) {
+	case ".gox":
+		suffix = "test.gox"
+	case ".gop":
+		suffix = "_test.gop"
+	case ".go":
+		suffix = "_test.go"
+	default:
+		return true
+	}
+	return !strings.HasSuffix(fname, suffix)
 }
 
 // -----------------------------------------------------------------------------
 
+// LoadDir loads Go+ packages from a specified directory.
 func LoadDir(dir string, conf *Config, genTestPkg bool, promptGenGo ...bool) (out, test *gox.Package, err error) {
 	if conf == nil {
 		conf = new(Config)
@@ -238,18 +262,27 @@ func LoadDir(dir string, conf *Config, genTestPkg bool, promptGenGo ...bool) (ou
 	if genTestPkg && pkgTest != nil {
 		test, err = cl.NewPackage("", pkgTest, clConf)
 	}
-	saveWithGopMod(mod, gop, out, test, conf)
+	afterLoad(mod, gop, out, test, conf)
 	return
 }
 
-func saveWithGopMod(mod *gopmod.Module, gop *env.Gop, out, test *gox.Package, conf *Config) {
-	if !conf.DontUpdateGoMod && mod.HasModfile() {
+func afterLoad(mod *gopmod.Module, gop *env.Gop, out, test *gox.Package, conf *Config) {
+	if mod.Path() == gopMod { // nothing to do for Go+ itself
+		return
+	}
+	updateMod := !conf.DontUpdateGoMod && mod.HasModfile()
+	if updateMod || conf.GopDeps != nil {
 		flags := checkGopDeps(out)
-		if test != nil {
-			flags |= checkGopDeps(test)
+		if conf.GopDeps != nil { // for `gop run`
+			*conf.GopDeps = flags
 		}
-		if flags != 0 {
-			mod.SaveWithGopMod(gop, flags)
+		if updateMod {
+			if test != nil {
+				flags |= checkGopDeps(test)
+			}
+			if flags != 0 {
+				mod.SaveWithGopMod(gop, flags)
+			}
 		}
 	}
 }
@@ -262,7 +295,7 @@ func checkGopDeps(pkg *gox.Package) (flags int) {
 }
 
 func relativeBaseOf(mod *gopmod.Module) string {
-	if hasModfile(mod) {
+	if mod.HasModfile() {
 		return mod.Root()
 	}
 	dir, _ := os.Getwd()
@@ -271,6 +304,7 @@ func relativeBaseOf(mod *gopmod.Module) string {
 
 // -----------------------------------------------------------------------------
 
+// LoadDir loads a Go+ package from specified files.
 func LoadFiles(dir string, files []string, conf *Config) (out *gox.Package, err error) {
 	if conf == nil {
 		conf = new(Config)
@@ -288,7 +322,11 @@ func LoadFiles(dir string, files []string, conf *Config) (out *gox.Package, err
 	if fset == nil {
 		fset = token.NewFileSet()
 	}
-	pkgs, err := parser.ParseFiles(fset, files, parser.ParseComments|parser.SaveAbsFile)
+	pkgs, err := parser.ParseEntries(fset, files, parser.Config{
+		ClassKind: mod.ClassKind,
+		Filter:    conf.Filter,
+		Mode:      parser.ParseComments | parser.SaveAbsFile,
+	})
 	if err != nil {
 		err = errors.NewWith(err, `parser.ParseFiles(fset, files, parser.ParseComments)`, -2, "parser.ParseFiles", fset, files, parser.ParseComments)
 		return
@@ -321,7 +359,7 @@ func LoadFiles(dir string, files []string, conf *Config) (out *gox.Package, err
 		}
 		break
 	}
-	saveWithGopMod(mod, gop, out, nil, conf)
+	afterLoad(mod, gop, out, nil, conf)
 	return
 }
 
diff --git a/parser/parser_gop.go b/parser/parser_gop.go
index 54013c62..356ad96c 100644
--- a/parser/parser_gop.go
+++ b/parser/parser_gop.go
@@ -229,7 +229,7 @@ func ParseFSEntry(fset *token.FileSet, fs FileSystem, filename string, src inter
 
 func filter(d fs.DirEntry, fn func(fs.FileInfo) bool) bool {
 	fi, err := d.Info()
-	return err != nil || fn(fi)
+	return err == nil && fn(fi)
 }
 
 func reqPkg(pkgs map[string]*ast.Package, name string) *ast.Package {
@@ -247,20 +247,22 @@ func reqPkg(pkgs map[string]*ast.Package, name string) *ast.Package {
 func defaultClassKind(fname string) (isProj bool, ok bool) {
 	ext := path.Ext(fname)
 	switch ext {
-	case ".gmx":
-		return true, true
 	case ".spx":
 		return fname == "main.spx", true
+	case ".gsh", ".gmx":
+		return true, true
 	}
 	return
 }
 
 // -----------------------------------------------------------------------------
 
+// ParseFiles parses Go+ source files and returns the corresponding packages.
 func ParseFiles(fset *token.FileSet, files []string, mode Mode) (map[string]*ast.Package, error) {
 	return ParseFSFiles(fset, fsx.Local, files, mode)
 }
 
+// ParseFSFiles parses Go+ source files and returns the corresponding packages.
 func ParseFSFiles(fset *token.FileSet, fs FileSystem, files []string, mode Mode) (map[string]*ast.Package, error) {
 	ret := map[string]*ast.Package{}
 	fabs := (mode & SaveAbsFile) != 0
@@ -286,6 +288,39 @@ func ParseFSFiles(fset *token.FileSet, fs FileSystem, files []string, mode Mode)
 	return ret, nil
 }
 
+// ParseEntries parses Go+ source files and returns the corresponding packages.
+// Compared to ParseFiles, ParseEntries detects fileKind by its filename.
+func ParseEntries(fset *token.FileSet, files []string, conf Config) (map[string]*ast.Package, error) {
+	return ParseFSEntries(fset, fsx.Local, files, conf)
+}
+
+// ParseFSEntries parses Go+ source files and returns the corresponding packages.
+// Compared to ParseFSFiles, ParseFSEntries detects fileKind by its filename.
+func ParseFSEntries(fset *token.FileSet, fs FileSystem, files []string, conf Config) (map[string]*ast.Package, error) {
+	ret := map[string]*ast.Package{}
+	fabs := (conf.Mode & SaveAbsFile) != 0
+	if fabs {
+		conf.Mode &^= SaveAbsFile
+	}
+	for _, file := range files {
+		if fabs {
+			file, _ = fs.Abs(file)
+		}
+		f, err := ParseFSEntry(fset, fs, file, nil, conf)
+		if err != nil {
+			return nil, err
+		}
+		pkgName := f.Name.Name
+		pkg, ok := ret[pkgName]
+		if !ok {
+			pkg = &ast.Package{Name: pkgName, Files: make(map[string]*ast.File)}
+			ret[pkgName] = pkg
+		}
+		pkg.Files[file] = f
+	}
+	return ret, nil
+}
+
 // -----------------------------------------------------------------------------
 
 // ParseFile parses the source code of a single Go+ source file and returns the corresponding ast.File node.
diff --git a/x/gocmd/build_install.go b/x/gocmd/build_install.go
index 7faf157a..6451d763 100644
--- a/x/gocmd/build_install.go
+++ b/x/gocmd/build_install.go
@@ -21,11 +21,11 @@ package gocmd
 type InstallConfig = Config
 
 func Install(arg string, conf *InstallConfig) (err error) {
-	return doWithArgs("install", conf, arg)
+	return doWithArgs("", "install", conf, arg)
 }
 
 func InstallFiles(files []string, conf *InstallConfig) (err error) {
-	return doWithArgs("install", conf, files...)
+	return doWithArgs("", "install", conf, files...)
 }
 
 // -----------------------------------------------------------------------------
@@ -33,11 +33,11 @@ func InstallFiles(files []string, conf *InstallConfig) (err error) {
 type BuildConfig = Config
 
 func Build(arg string, conf *BuildConfig) (err error) {
-	return doWithArgs("build", conf, arg)
+	return doWithArgs("", "build", conf, arg)
 }
 
 func BuildFiles(files []string, conf *BuildConfig) (err error) {
-	return doWithArgs("build", conf, files...)
+	return doWithArgs("", "build", conf, files...)
 }
 
 // -----------------------------------------------------------------------------
@@ -45,11 +45,11 @@ func BuildFiles(files []string, conf *BuildConfig) (err error) {
 type TestConfig = Config
 
 func Test(arg string, conf *TestConfig) (err error) {
-	return doWithArgs("test", conf, arg)
+	return doWithArgs("", "test", conf, arg)
 }
 
 func TestFiles(files []string, conf *TestConfig) (err error) {
-	return doWithArgs("test", conf, files...)
+	return doWithArgs("", "test", conf, files...)
 }
 
 // -----------------------------------------------------------------------------
diff --git a/x/gocmd/gocmd.go b/x/gocmd/gocmd.go
index 30a8b14e..ac3c720d 100644
--- a/x/gocmd/gocmd.go
+++ b/x/gocmd/gocmd.go
@@ -36,7 +36,7 @@ type Config struct {
 
 // -----------------------------------------------------------------------------
 
-func doWithArgs(op string, conf *Config, args ...string) (err error) {
+func doWithArgs(dir, op string, conf *Config, args ...string) (err error) {
 	if conf == nil {
 		conf = new(Config)
 	}
@@ -50,6 +50,7 @@ func doWithArgs(op string, conf *Config, args ...string) (err error) {
 	exargs = append(exargs, conf.Flags...)
 	exargs = append(exargs, args...)
 	cmd := exec.Command(goCmd, exargs...)
+	cmd.Dir = dir
 	run := conf.Run
 	if run == nil {
 		run = runCmd
diff --git a/x/gocmd/run.go b/x/gocmd/run.go
index 89fc3908..3b2e9138 100644
--- a/x/gocmd/run.go
+++ b/x/gocmd/run.go
@@ -18,15 +18,20 @@ package gocmd
 
 import (
 	"os"
+	"os/exec"
 	"path/filepath"
 	"strings"
+	"syscall"
 )
 
 // -----------------------------------------------------------------------------
 
 type RunConfig = Config
 
-func RunDir(dir string, args []string, conf *RunConfig) (err error) {
+// RunDir runs a Go project by specified directory.
+// If buildDir is not empty, it means split `go run` into `go build`
+// in buildDir and run the built app in current directory.
+func RunDir(buildDir, dir string, args []string, conf *RunConfig) (err error) {
 	fis, err := os.ReadDir(dir)
 	if err != nil {
 		return
@@ -39,7 +44,7 @@ func RunDir(dir string, args []string, conf *RunConfig) (err error) {
 			}
 		}
 	}
-	return RunFiles(files, args, conf)
+	return RunFiles(buildDir, files, args, conf)
 }
 
 func filterRunFname(fname string) bool {
@@ -49,9 +54,39 @@ func filterRunFname(fname string) bool {
 
 // -----------------------------------------------------------------------------
 
-func RunFiles(files []string, args []string, conf *RunConfig) (err error) {
-	args = append(files, args...)
-	return doWithArgs("run", conf, args...)
+// RunFiles runs a Go project by specified files.
+// If buildDir is not empty, it means split `go run` into `go build`
+// in buildDir and run the built app in current directory.
+func RunFiles(buildDir string, files []string, args []string, conf *RunConfig) (err error) {
+	if len(files) == 0 {
+		return syscall.ENOENT
+	}
+	if buildDir == "" {
+		args = append(files, args...)
+		return doWithArgs("", "run", conf, args...)
+	}
+
+	absFiles := make([]string, len(files))
+	for i, file := range files {
+		absFiles[i], _ = filepath.Abs(file)
+	}
+
+	f, err := os.CreateTemp("", "gobuild")
+	if err != nil {
+		return
+	}
+	tempf := f.Name()
+	f.Close()
+	os.Remove(tempf)
+	defer os.Remove(tempf)
+
+	buildArgs := append([]string{"-o", tempf}, absFiles...)
+	if err = doWithArgs(buildDir, "build", conf, buildArgs...); err != nil {
+		return
+	}
+
+	cmd := exec.Command(tempf, args...)
+	return runCmd(cmd)
 }
 
 // -----------------------------------------------------------------------------
diff --git a/x/gopprojs/proj.go b/x/gopprojs/proj.go
index 92aabbc0..e27e4d57 100644
--- a/x/gopprojs/proj.go
+++ b/x/gopprojs/proj.go
@@ -65,11 +65,8 @@ func ParseOne(args ...string) (proj Proj, next []string, err error) {
 }
 
 func isFile(fname string) bool {
-	switch filepath.Ext(fname) {
-	case ".gop", ".go":
-		return true
-	}
-	return false
+	n := len(filepath.Ext(fname))
+	return n > 1
 }
 
 func isLocal(ns string) bool {
diff --git a/x/typesutil/check.go b/x/typesutil/check.go
index 03a3a5ef..28cc2c29 100644
--- a/x/typesutil/check.go
+++ b/x/typesutil/check.go
@@ -148,7 +148,7 @@ func (p *Checker) Files(goFiles []*goast.File, gopFiles []*ast.File) (err error)
 		LookupPub:      c2go.LookupPub(mod),
 		LookupClass:    mod.LookupClass,
 		Importer:       conf.Importer,
-		Recorder:       gopRecorder{p.gopInfo},
+		Recorder:       NewRecorder(p.gopInfo),
 		NoFileLine:     true,
 		NoAutoGenMain:  true,
 		NoSkipConstant: true,
@@ -169,12 +169,14 @@ func (p *Checker) Files(goFiles []*goast.File, gopFiles []*ast.File) (err error)
 			log.Println("typesutil.Check err:", err)
 			log.SingleStack()
 		}
+		// don't return even if err != nil
 	}
 	if len(files) > 0 {
 		scope := pkgTypes.Scope()
 		objMap := DeleteObjects(scope, files)
 		checker := types.NewChecker(conf, fset, pkgTypes, p.goInfo)
 		err = checker.Files(files)
+		// TODO: how to process error?
 		CorrectTypesInfo(scope, objMap, p.gopInfo.Uses)
 	}
 	return
diff --git a/x/typesutil/gopinfo.go b/x/typesutil/gopinfo.go
index e1594d39..39f67b38 100644
--- a/x/typesutil/gopinfo.go
+++ b/x/typesutil/gopinfo.go
@@ -20,6 +20,7 @@ import (
 	"go/types"
 
 	"github.com/goplus/gop/ast"
+	"github.com/goplus/gop/cl"
 	"github.com/goplus/gox"
 	"github.com/qiniu/x/log"
 )
@@ -168,6 +169,11 @@ type gopRecorder struct {
 	*Info
 }
 
+// NewRecorder creates a new recorder for cl.NewPackage.
+func NewRecorder(info *Info) cl.Recorder {
+	return gopRecorder{info}
+}
+
 // Type maps expressions to their types, and for constant
 // expressions, also their values. Invalid expressions are
 // omitted.


Test patch:
diff --git a/cl/builtin_test.go b/cl/builtin_test.go
index c44fa00e..73a88613 100644
--- a/cl/builtin_test.go
+++ b/cl/builtin_test.go
@@ -19,6 +19,7 @@ package cl
 import (
 	"errors"
 	"go/types"
+	"log"
 	"testing"
 
 	"github.com/goplus/gop/ast"
@@ -39,6 +40,85 @@ func getGoxConf() *gox.Config {
 	return &gox.Config{Fset: fset, Importer: imp}
 }
 
+func TestCompileLambdaExpr(t *testing.T) {
+	ctx := &blockCtx{
+		pkgCtx: &pkgCtx{},
+	}
+	lhs := []*ast.Ident{ast.NewIdent("x")}
+	sig := types.NewSignatureType(nil, nil, nil, nil, nil, false)
+	e := compileLambdaExpr(ctx, &ast.LambdaExpr{Lhs: lhs}, sig)
+	if ce := e.(*gox.CodeError); ce.Msg != `too many arguments in lambda expression
+	have (x)
+	want ()` {
+		t.Fatal("compileLambdaExpr:", ce.Msg)
+	}
+}
+
+func TestCompileLambda1(t *testing.T) {
+	defer func() {
+		if e := recover(); e != nil {
+			if ce := e.(*gox.CodeError); ce.Msg != `too many arguments in lambda expression
+	have (x)
+	want ()` {
+				t.Fatal("compileLambda:", ce.Msg)
+			}
+		}
+	}()
+	ctx := &blockCtx{
+		pkgCtx: &pkgCtx{},
+	}
+	lhs := []*ast.Ident{ast.NewIdent("x")}
+	sig := types.NewSignatureType(nil, nil, nil, nil, nil, false)
+	compileLambda(ctx, &ast.LambdaExpr{Lhs: lhs}, sig)
+}
+
+func TestCompileLambda2(t *testing.T) {
+	defer func() {
+		if e := recover(); e != nil {
+			if ce := e.(*gox.CodeError); ce.Msg != `too many arguments in lambda expression
+	have (x)
+	want ()` {
+				t.Fatal("compileLambda:", ce.Msg)
+			}
+		}
+	}()
+	ctx := &blockCtx{
+		pkgCtx: &pkgCtx{},
+	}
+	lhs := []*ast.Ident{ast.NewIdent("x")}
+	sig := types.NewSignatureType(nil, nil, nil, nil, nil, false)
+	compileLambda(ctx, &ast.LambdaExpr2{Lhs: lhs}, sig)
+}
+
+func TestCompileExpr(t *testing.T) {
+	defer func() {
+		if e := recover(); e != "compileExpr failed: unknown - *ast.Ellipsis\n" {
+			t.Fatal("compileExpr:", e)
+		}
+	}()
+	compileExpr(nil, &ast.Ellipsis{})
+}
+
+func TestCompileStmt(t *testing.T) {
+	old := enableRecover
+	defer func() {
+		enableRecover = old
+		if e := recover(); e != "compileStmt failed: unknown - *ast.BadStmt\n" {
+			t.Fatal("compileStmt:", e)
+		}
+	}()
+	enableRecover = false
+	ctx := &blockCtx{}
+	compileStmt(ctx, &ast.BadStmt{})
+}
+
+func TestTryGopExec(t *testing.T) {
+	pkg := gox.NewPackage("", "foo", goxConf)
+	if tryGopExec(pkg.CB(), nil) {
+		t.Fatal("tryGopExec")
+	}
+}
+
 func TestCompileFuncAlias(t *testing.T) {
 	ctx := &blockCtx{
 		pkgCtx: &pkgCtx{
@@ -185,6 +265,12 @@ func TestNodeInterp(t *testing.T) {
 	if v := ni.Caller(&ast.Ident{}); v != "the function call" {
 		t.Fatal("TestNodeInterp:", v)
 	}
+	defer func() {
+		if e := recover(); e == nil {
+			log.Fatal("TestNodeInterp: no error")
+		}
+	}()
+	ni.Caller(&ast.CallExpr{})
 }
 
 func TestMarkAutogen(t *testing.T) {
@@ -293,24 +379,18 @@ func TestGetTypeName(t *testing.T) {
 
 func TestHandleRecover(t *testing.T) {
 	var ctx pkgCtx
-	ctx.handleRecover("hello")
+	ctx.handleRecover("hello", nil)
 	if !(len(ctx.errs) == 1 && ctx.errs[0].Error() == "hello") {
 		t.Fatal("TestHandleRecover failed:", ctx.errs)
 	}
-	defer func() {
-		if e := recover(); e == nil {
-			t.Fatal("TestHandleRecover failed: no error?")
-		}
-	}()
-	ctx.handleRecover(100)
 }
 
-func TestCanAutoCall(t *testing.T) {
-	if !isCommandWithoutArgs(
+func TestCheckCommandWithoutArgs(t *testing.T) {
+	if checkCommandWithoutArgs(
 		&ast.SelectorExpr{
 			X:   &ast.SelectorExpr{X: ast.NewIdent("foo"), Sel: ast.NewIdent("bar")},
 			Sel: ast.NewIdent("val"),
-		}) {
+		}) != clCommandWithoutArgs {
 		t.Fatal("TestCanAutoCall failed")
 	}
 }
diff --git a/cl/compile_gop_test.go b/cl/compile_gop_test.go
index 8c1acbbf..095bba2b 100644
--- a/cl/compile_gop_test.go
+++ b/cl/compile_gop_test.go
@@ -20,6 +20,27 @@ import (
 	"testing"
 )
 
+func TestMayBuiltinDelete(t *testing.T) {
+	gopClTest(t, `
+func Delete(a int) {}
+func Foo(m map[string]int) {
+	delete(m, "a")
+}
+
+delete 10
+`, `package main
+
+func Delete(a int) {
+}
+func Foo(m map[string]int) {
+	delete(m, "a")
+}
+func main() {
+	Delete(10)
+}
+`)
+}
+
 func TestVargCommand(t *testing.T) {
 	gopClTest(t, `
 type foo int
diff --git a/cl/compile_spx_test.go b/cl/compile_spx_test.go
index 32041a5f..a233f857 100644
--- a/cl/compile_spx_test.go
+++ b/cl/compile_spx_test.go
@@ -253,6 +253,42 @@ func (this *index) onInit() {
 `)
 }
 
+func TestSpxGopExec(t *testing.T) {
+	gopSpxTest(t, `
+vim "a.txt"
+vim
+ls 10
+capout => { ls }
+capout => { ls "-l" }
+`, ``, `package main
+
+import "github.com/goplus/gop/cl/internal/spx"
+
+type bar struct {
+	spx.Sprite
+	*index
+}
+type index struct {
+	*spx.MyGame
+}
+
+func (this *index) MainEntry() {
+	this.Gop_Exec("vim", "a.txt")
+	this.Gop_Exec("vim")
+	this.Ls(10)
+	this.Capout(func() {
+		this.Gop_Exec("ls")
+	})
+	this.Capout(func() {
+		this.Gop_Exec("ls", "-l")
+	})
+}
+func main() {
+	spx.Gopt_MyGame_Main(new(index))
+}
+`)
+}
+
 func TestSpxMethod(t *testing.T) {
 	gopSpxTestEx(t, `
 func onInit() {
diff --git a/cl/compile_test.go b/cl/compile_test.go
index 6c5d68d5..cf095522 100644
--- a/cl/compile_test.go
+++ b/cl/compile_test.go
@@ -122,6 +122,36 @@ func gopClTestFS(t *testing.T, conf *cl.Config, fs parser.FileSystem, pkgname, e
 	}
 }
 
+func TestTypeDoc(t *testing.T) {
+	gopClTest(t, `
+type (
+	// doc
+	A int
+)
+`, `package main
+// doc
+type A int
+`)
+}
+
+func TestUnsafe(t *testing.T) {
+	gopClTest(t, `
+import "unsafe"
+
+println unsafe.Sizeof(0)
+`, `package main
+
+import (
+	"fmt"
+	"unsafe"
+)
+
+func main() {
+	fmt.Println(unsafe.Sizeof(0))
+}
+`)
+}
+
 func Test_CastSlice_Issue1240(t *testing.T) {
 	gopClTest(t, `
 type fvec []float64
@@ -4111,9 +4141,8 @@ func (pt *Point) Test() {
 //line /foo/bar.gop:8:1
 	fmt.Println(pt.x, pt.y)
 }
+//line /foo/bar.gop:11:1
 // testPoint is test point
-//
-//line /foo/bar.gop:12:1
 func testPoint() {
 //line /foo/bar.gop:13:1
 	var pt Point
@@ -4166,9 +4195,8 @@ func (pt *Point) Test() {
 //line ../bar.gop:8:1
 	fmt.Println(pt.x, pt.y)
 }
+//line ../bar.gop:11:1
 // testPoint is test point
-//
-//line ../bar.gop:12:1
 func testPoint() {
 //line ../bar.gop:13:1
 	var pt Point
diff --git a/cl/error_msg_test.go b/cl/error_msg_test.go
index 4a4e1a58..c97acab3 100644
--- a/cl/error_msg_test.go
+++ b/cl/error_msg_test.go
@@ -72,6 +72,34 @@ func codeErrorTestAst(t *testing.T, pkgname, filename, msg, src string) {
 	}
 }
 
+func TestErrVargCommand(t *testing.T) {
+	codeErrorTest(t, `bar.gop:5:1: not enough arguments in call to Ls
+	have ()
+	want (int)`, `
+func Ls(int) {
+}
+
+ls
+`)
+	codeErrorTest(t, `bar.gop:8:1: not enough arguments in call to f.Ls
+	have ()
+	want (int)`, `
+type foo int
+
+func (f foo) Ls(int) {
+}
+
+var f foo
+f.ls
+`)
+}
+
+func TestErrUnsafe(t *testing.T) {
+	codeErrorTest(t, `bar.gop:2:9: undefined: Sizeof`, `
+println Sizeof(0)
+`)
+}
+
 func TestErrLambdaExpr(t *testing.T) {
 	codeErrorTest(t,
 		"bar.gop:7:6: too few arguments in lambda expression\n\thave ()\n\twant (int, int)", `
@@ -984,7 +1012,7 @@ a := uint128(b)
 }
 
 func TestErrCompileFunc(t *testing.T) {
-	codeErrorTest(t, `bar.gop:2:1: compile func printf("%+v\n", int32) error: unreachable`, `
+	codeErrorTest(t, "bar.gop:2:1: compile `printf(\"%+v\\n\", int32)`: unreachable", `
 printf("%+v\n", int32)
 `)
 }
diff --git a/cmd/internal/test/test.go b/cmd/internal/test/test.go
index a9d119ae..09efde54 100644
--- a/cmd/internal/test/test.go
+++ b/cmd/internal/test/test.go
@@ -69,7 +69,7 @@ func runCmd(cmd *base.Command, args []string) {
 		cl.SetDisableRecover(true)
 	}
 
-	conf, err := gop.NewDefaultConf(".")
+	conf, err := gop.NewDefaultConf(".", false)
 	if err != nil {
 		log.Panicln("gop.NewDefaultConf:", err)
 	}
diff --git a/parser/parser_test.go b/parser/parser_test.go
index b0dd4b7e..742b3646 100644
--- a/parser/parser_test.go
+++ b/parser/parser_test.go
@@ -27,12 +27,31 @@ import (
 
 // -----------------------------------------------------------------------------
 
+type fileInfo struct {
+	*fsx.FileInfo
+}
+
+func (p fileInfo) Info() (fs.FileInfo, error) {
+	return nil, fs.ErrNotExist
+}
+
 func TestFilter(t *testing.T) {
 	d := fsx.NewFileInfo("foo.go", 10)
 	if filter(d, func(fi fs.FileInfo) bool {
 		return false
 	}) {
-		t.Fatal("TestFilter failed")
+		t.Fatal("TestFilter: true?")
+	}
+	if !filter(d, func(fi fs.FileInfo) bool {
+		return true
+	}) {
+		t.Fatal("TestFilter: false?")
+	}
+	d2 := fileInfo{d}
+	if filter(d2, func(fi fs.FileInfo) bool {
+		return true
+	}) {
+		t.Fatal("TestFilter: true?")
 	}
 }
 
diff --git a/parser/parserdir_test.go b/parser/parserdir_test.go
index 3bec85ac..0831a73b 100644
--- a/parser/parserdir_test.go
+++ b/parser/parserdir_test.go
@@ -25,6 +25,7 @@ import (
 	"syscall"
 	"testing"
 
+	"github.com/goplus/gop/ast"
 	"github.com/goplus/gop/parser/fsx"
 	"github.com/goplus/gop/parser/fsx/memfs"
 	"github.com/goplus/gop/parser/parsertest"
@@ -145,7 +146,41 @@ func TestParseGoFiles(t *testing.T) {
 	}
 }
 
+func TestParseEntries(t *testing.T) {
+	doTestParseEntries(t, Config{})
+	_, err := ParseEntries(nil, []string{"/not-found/gopfile.gox"}, Config{})
+	if err == nil {
+		t.Fatal("ParseEntries: no error?")
+	}
+}
+
+func TestParseEntries_SaveAbsFile(t *testing.T) {
+	doTestParseEntries(t, Config{Mode: SaveAbsFile})
+}
+
+func doTestParseEntries(t *testing.T, confReal Config) {
+	doTestParseEntry(t, func(fset *token.FileSet, filename string, src interface{}, conf Config) (f *ast.File, err error) {
+		fs, _ := memfs.File(filename, src)
+		pkgs, err := ParseFSEntries(fset, fs, []string{filename}, confReal)
+		if err != nil {
+			return
+		}
+		for _, pkg := range pkgs {
+			for _, file := range pkg.Files {
+				f = file
+				return
+			}
+		}
+		t.Fatal("TestParseEntries: no source?")
+		return nil, syscall.ENOENT
+	})
+}
+
 func TestParseEntry(t *testing.T) {
+	doTestParseEntry(t, ParseEntry)
+}
+
+func doTestParseEntry(t *testing.T, parseEntry func(fset *token.FileSet, filename string, src interface{}, conf Config) (f *ast.File, err error)) {
 	fset := token.NewFileSet()
 	src, err := os.ReadFile("./_testdata/functype/functype.go")
 	if err != nil {
@@ -153,7 +188,7 @@ func TestParseEntry(t *testing.T) {
 	}
 	conf := Config{}
 	t.Run(".gop file", func(t *testing.T) {
-		f, err := ParseEntry(fset, "./functype.gop", src, conf)
+		f, err := parseEntry(fset, "./functype.gop", src, conf)
 		if err != nil {
 			t.Fatal("ParseEntry failed:", err)
 		}
@@ -162,7 +197,7 @@ func TestParseEntry(t *testing.T) {
 		}
 	})
 	t.Run(".gox file", func(t *testing.T) {
-		f, err := ParseEntry(fset, "./functype.gox", src, conf)
+		f, err := parseEntry(fset, "./functype.gox", src, conf)
 		if err != nil {
 			t.Fatal("ParseEntry failed:", err)
 		}
@@ -171,7 +206,7 @@ func TestParseEntry(t *testing.T) {
 		}
 	})
 	t.Run(".foo.gox file", func(t *testing.T) {
-		f, err := ParseEntry(fset, "./functype.foo.gox", src, conf)
+		f, err := parseEntry(fset, "./functype.foo.gox", src, conf)
 		if err != nil {
 			t.Fatal("ParseEntry failed:", err)
 		}
@@ -180,13 +215,13 @@ func TestParseEntry(t *testing.T) {
 		}
 	})
 	t.Run(".foo file", func(t *testing.T) {
-		_, err := ParseEntry(fset, "./functype.foo", src, conf)
+		_, err := parseEntry(fset, "./functype.foo", src, conf)
 		if err != ErrUnknownFileKind {
 			t.Fatal("ParseEntry failed:", err)
 		}
 	})
 	t.Run(".spx file", func(t *testing.T) {
-		f, err := ParseEntry(fset, "./main.spx", src, conf)
+		f, err := parseEntry(fset, "./main.spx", src, conf)
 		if err != nil {
 			t.Fatal("ParseEntry failed:", err)
 		}
diff --git a/test/classfile.go b/test/classfile.go
index 8ca2593a..ff549662 100644
--- a/test/classfile.go
+++ b/test/classfile.go
@@ -1,3 +1,19 @@
+/*
+ * Copyright (c) 2024 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 test
 
 import (
@@ -22,7 +38,7 @@ func (p *Case) initCase(t *testing.T) {
 	p.t = t
 }
 
-// T returns a *testing.T object.
+// T returns the *testing.T object .
 func (p Case) T() *testing.T { return p.t }
 
 // Run runs f as a subtest of t called name. It runs f in a separate goroutine
@@ -49,6 +65,9 @@ func (p *App) initApp(m *testing.M) {
 	p.m = m
 }
 
+// M returns the *testing.M object.
+func (p App) M() *testing.M { return p.m }
+
 // Gopt_App_TestMain is required by Go+ compiler as the entry of a Go+ testing project.
 func Gopt_App_TestMain(app interface{ initApp(m *testing.M) }, m *testing.M) {
 	app.initApp(m)