Repository: goplus/gop
Pull request #1421 (2023-09-23 10:38:17)

v1.1.7


Patch:
diff --git a/README.md b/README.md
index 257303ea..2fb322cd 100644
--- a/README.md
+++ b/README.md
@@ -16,7 +16,7 @@
 [![Interpreter](https://img.shields.io/badge/interpreter-iGo+-seagreen.svg)](https://github.com/goplus/igop)
 -->
 
-[![Build Status](https://github.com/goplus/gox/actions/workflows/go.yml/badge.svg)](https://github.com/goplus/gox/actions/workflows/go.yml)
+[![Build Status](https://github.com/goplus/gop/actions/workflows/go.yml/badge.svg)](https://github.com/goplus/gop/actions/workflows/go.yml)
 [![Go Report Card](https://goreportcard.com/badge/github.com/goplus/gop)](https://goreportcard.com/report/github.com/goplus/gop)
 [![Coverage Status](https://codecov.io/gh/goplus/gop/branch/main/graph/badge.svg)](https://codecov.io/gh/goplus/gop)
 [![GitHub release](https://img.shields.io/github/v/tag/goplus/gop.svg?label=release)](https://github.com/goplus/gop/releases)
diff --git a/VERSION b/VERSION
deleted file mode 100644
index 650b706f..00000000
--- a/VERSION
+++ /dev/null
@@ -1,1 +0,0 @@
-v1.1.6
diff --git a/ast/ast.go b/ast/ast.go
index ac758818..b14227e4 100644
--- a/ast/ast.go
+++ b/ast/ast.go
@@ -553,7 +553,7 @@ func (*ChanType) exprNode()      {}
 // Convenience functions for Idents
 
 // NewIdent creates a new Ident without position.
-// Useful for ASTs generated by code other than the Go parser.
+// Useful for ASTs generated by code other than the Go+ parser.
 func NewIdent(name string) *Ident { return &Ident{token.NoPos, name, nil} }
 
 // IsExported reports whether name starts with an upper-case letter.
@@ -1103,7 +1103,7 @@ func (*FuncDecl) declNode() {}
 
 type FileType = int16
 
-// A File node represents a Go source file.
+// A File node represents a Go+ source file.
 //
 // The Comments list contains all comments in the source file in order of
 // appearance, including the comments that are pointed to from other nodes
@@ -1150,7 +1150,7 @@ func (f *File) End() token.Pos {
 }
 
 // A Package node represents a set of source files
-// collectively building a Go package.
+// collectively building a Go+ package.
 type Package struct {
 	Name    string               // package name
 	Scope   *Scope               // package scope across all files
diff --git a/cl/classfile.go b/cl/classfile.go
index 6e0fe6c4..3e1ad51f 100644
--- a/cl/classfile.go
+++ b/cl/classfile.go
@@ -72,7 +72,7 @@ func newGmx(ctx *pkgCtx, pkg *gox.Package, file string, f *ast.File, conf *Confi
 		if idx := strings.Index(name, "."); idx > 0 {
 			name = name[:idx]
 			if name == "main" {
-				name = "_main"
+				name = gt.Class
 			}
 		}
 	}
@@ -139,7 +139,7 @@ func getStringConst(spx *gox.PkgRef, name string) string {
 	return ""
 }
 
-func getFields(ctx *blockCtx, f *ast.File) (specs []ast.Spec) {
+func getFields(f *ast.File) (specs []ast.Spec) {
 	decls := f.Decls
 	i, n := 0, len(decls)
 	for i < n {
@@ -153,7 +153,7 @@ func getFields(ctx *blockCtx, f *ast.File) (specs []ast.Spec) {
 			}
 			break
 		}
-		i++ // skip import, if any
+		i++ // skip import/const, if any
 	}
 	return
 }
diff --git a/cl/compile.go b/cl/compile.go
index b1d71e1f..4b5023ef 100644
--- a/cl/compile.go
+++ b/cl/compile.go
@@ -579,7 +579,7 @@ func preloadGopFile(p *gox.Package, ctx *blockCtx, file string, f *ast.File, con
 		}
 		syms := parent.syms
 		pos := f.Pos()
-		specs := getFields(ctx, f)
+		specs := getFields(f)
 		ld := getTypeLoader(parent, syms, pos, classType)
 		ld.typ = func() {
 			if debugLoad {
@@ -812,7 +812,7 @@ func preloadFile(p *gox.Package, ctx *blockCtx, file string, f *ast.File, genCod
 				}
 			case token.CONST:
 				pkg := ctx.pkg
-				cdecl := pkg.NewConstDecl(pkg.Types.Scope())
+				cdecl := pkg.NewConstDefs(pkg.Types.Scope())
 				for _, spec := range d.Specs {
 					vSpec := spec.(*ast.ValueSpec)
 					if debugLoad {
diff --git a/cl/expr.go b/cl/expr.go
index 5aa4d80b..9b09c8b4 100644
--- a/cl/expr.go
+++ b/cl/expr.go
@@ -98,7 +98,7 @@ func compileIdent(ctx *blockCtx, ident *ast.Ident, flags int) (obj *gox.PkgRef,
 			sig := fn.Ancestor().Type().(*types.Signature)
 			if recv := sig.Recv(); recv != nil {
 				ctx.cb.Val(recv)
-				if compileMember(ctx, ident, name, flags) == nil { // class member object
+				if compileMember(ctx, ident, name, flags&^clCommandWithoutArgs) == nil { // class member object
 					return
 				}
 				ctx.cb.InternalStack().PopN(1)
diff --git a/parser/fsx/fsys.go b/parser/fsx/fsys.go
new file mode 100644
index 00000000..02b3e301
--- /dev/null
+++ b/parser/fsx/fsys.go
@@ -0,0 +1,36 @@
+package fsx
+
+import (
+	"io/fs"
+	"os"
+	"path/filepath"
+)
+
+// -----------------------------------------------------------------------------
+
+// FileSystem represents a file system.
+type FileSystem interface {
+	ReadDir(dirname string) ([]fs.DirEntry, error)
+	ReadFile(filename string) ([]byte, error)
+	Join(elem ...string) string
+}
+
+// -----------------------------------------------------------------------------
+
+type localFS struct{}
+
+func (p localFS) ReadDir(dirname string) ([]fs.DirEntry, error) {
+	return os.ReadDir(dirname)
+}
+
+func (p localFS) ReadFile(filename string) ([]byte, error) {
+	return os.ReadFile(filename)
+}
+
+func (p localFS) Join(elem ...string) string {
+	return filepath.Join(elem...)
+}
+
+var Local FileSystem = localFS{}
+
+// -----------------------------------------------------------------------------
diff --git a/parser/fsx/memfs/fs.go b/parser/fsx/memfs/fs.go
new file mode 100644
index 00000000..12b692f1
--- /dev/null
+++ b/parser/fsx/memfs/fs.go
@@ -0,0 +1,94 @@
+/*
+ * Copyright (c) 2023 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 memfs
+
+import (
+	"bytes"
+	"io"
+	"io/fs"
+	"os"
+	"path/filepath"
+)
+
+type FileFS struct {
+	data []byte
+	info fs.DirEntry
+}
+
+type dirEntry struct {
+	fs.FileInfo
+}
+
+func (p *dirEntry) Type() fs.FileMode {
+	return p.FileInfo.Mode().Type()
+}
+
+func (p *dirEntry) Info() (fs.FileInfo, error) {
+	return p.FileInfo, nil
+}
+
+func File(filename string, src interface{}) (f *FileFS, err error) {
+	var data []byte
+	var info fs.DirEntry
+	if src != nil {
+		data, err = readSource(src)
+		if err != nil {
+			return
+		}
+		info = &memFileInfo{name: filename, size: len(data)}
+	} else {
+		fi, e := os.Stat(filename)
+		if e != nil {
+			return nil, e
+		}
+		data, err = os.ReadFile(filename)
+		if err != nil {
+			return
+		}
+		info = &dirEntry{fi}
+	}
+	return &FileFS{data: data, info: info}, nil
+}
+
+func (p *FileFS) ReadDir(dirname string) ([]fs.DirEntry, error) {
+	return []fs.DirEntry{p.info}, nil
+}
+
+func (p *FileFS) ReadFile(filename string) ([]byte, error) {
+	return p.data, nil
+}
+
+func (p *FileFS) Join(elem ...string) string {
+	return filepath.Join(elem...)
+}
+
+func readSource(src interface{}) ([]byte, error) {
+	switch s := src.(type) {
+	case string:
+		return []byte(s), nil
+	case []byte:
+		return s, nil
+	case *bytes.Buffer:
+		// is io.Reader, but src is already available in []byte form
+		if s != nil {
+			return s.Bytes(), nil
+		}
+	case io.Reader:
+		return io.ReadAll(s)
+	}
+	return nil, os.ErrInvalid
+}
diff --git a/parser/parser.go b/parser/parser.go
index 2e5a803f..7de73488 100644
--- a/parser/parser.go
+++ b/parser/parser.go
@@ -3636,7 +3636,6 @@ func (p *parser) parseDecl(sync map[token.Token]bool) ast.Decl {
 		decl, call := p.parseFuncDeclOrCall()
 		if decl != nil {
 			if p.errors.Len() != 0 {
-				p.errorExpected(pos, "declaration", 2)
 				p.advance(sync)
 			}
 			return decl
diff --git a/parser/parser_gop.go b/parser/parser_gop.go
index 3f3cac3d..372c880b 100644
--- a/parser/parser_gop.go
+++ b/parser/parser_gop.go
@@ -22,8 +22,6 @@ import (
 	"fmt"
 	"io"
 	"io/fs"
-	"io/ioutil"
-	"os"
 	"path"
 	"path/filepath"
 	"strings"
@@ -32,6 +30,7 @@ import (
 	goparser "go/parser"
 
 	"github.com/goplus/gop/ast"
+	"github.com/goplus/gop/parser/fsx"
 	"github.com/goplus/gop/token"
 )
 
@@ -51,30 +50,9 @@ func SetDebug(dbgFlags int) {
 	debugParseError = (dbgFlags & DbgFlagParseError) != 0
 }
 
-// -----------------------------------------------------------------------------
-
-// FileSystem represents a file system.
-type FileSystem interface {
-	ReadDir(dirname string) ([]fs.FileInfo, error)
-	ReadFile(filename string) ([]byte, error)
-	Join(elem ...string) string
-}
-
-type localFS struct{}
+type FileSystem = fsx.FileSystem
 
-func (p localFS) ReadDir(dirname string) ([]fs.FileInfo, error) {
-	return ioutil.ReadDir(dirname)
-}
-
-func (p localFS) ReadFile(filename string) ([]byte, error) {
-	return os.ReadFile(filename)
-}
-
-func (p localFS) Join(elem ...string) string {
-	return filepath.Join(elem...)
-}
-
-var local FileSystem = localFS{}
+// -----------------------------------------------------------------------------
 
 // Parse parses a single Go+ source file. The target specifies the Go+ source file.
 // If the file couldn't be read, a nil map and the respective error are returned.
@@ -102,7 +80,7 @@ func astFileToPkg(file *ast.File, fileName string) (pkg *ast.Package) {
 
 // ParseDir calls ParseFSDir by passing a local filesystem.
 func ParseDir(fset *token.FileSet, path string, filter func(fs.FileInfo) bool, mode Mode) (pkgs map[string]*ast.Package, first error) {
-	return ParseFSDir(fset, local, path, Config{Filter: filter, Mode: mode})
+	return ParseFSDir(fset, fsx.Local, path, Config{Filter: filter, Mode: mode})
 }
 
 type Config struct {
@@ -113,7 +91,7 @@ type Config struct {
 
 // ParseDirEx calls ParseFSDir by passing a local filesystem.
 func ParseDirEx(fset *token.FileSet, path string, conf Config) (pkgs map[string]*ast.Package, first error) {
-	return ParseFSDir(fset, local, path, conf)
+	return ParseFSDir(fset, fsx.Local, path, conf)
 }
 
 // ClassFileExt returns the classfile extension
@@ -186,7 +164,7 @@ func ParseFSDir(fset *token.FileSet, fs FileSystem, path string, conf Config) (p
 		if isClass {
 			mode |= ParseGoPlusClass
 		}
-		if !strings.HasPrefix(fname, "_") && (conf.Filter == nil || conf.Filter(d)) {
+		if !strings.HasPrefix(fname, "_") && (conf.Filter == nil || filter(conf.Filter, d)) {
 			filename := fs.Join(path, fname)
 			if useGoParser {
 				if filedata, err := fs.ReadFile(filename); err == nil {
@@ -215,6 +193,11 @@ func ParseFSDir(fset *token.FileSet, fs FileSystem, path string, conf Config) (p
 	return
 }
 
+func filter(fn func(fs.FileInfo) bool, d fs.DirEntry) bool {
+	fi, err := d.Info()
+	return err != nil || fn(fi)
+}
+
 func reqPkg(pkgs map[string]*ast.Package, name string) *ast.Package {
 	pkg, found := pkgs[name]
 	if !found {
@@ -233,7 +216,7 @@ func defaultClassKind(fname string) (isProj bool, ok bool) {
 	case ".gmx":
 		return true, true
 	case ".spx":
-		return ext == "main.spx", true
+		return fname == "main.spx", true
 	}
 	return
 }
@@ -241,7 +224,7 @@ func defaultClassKind(fname string) (isProj bool, ok bool) {
 // -----------------------------------------------------------------------------
 
 func ParseFiles(fset *token.FileSet, files []string, mode Mode) (map[string]*ast.Package, error) {
-	return ParseFSFiles(fset, local, files, mode)
+	return ParseFSFiles(fset, fsx.Local, files, mode)
 }
 
 func ParseFSFiles(fset *token.FileSet, fs FileSystem, files []string, mode Mode) (map[string]*ast.Package, error) {
@@ -266,7 +249,7 @@ func ParseFSFiles(fset *token.FileSet, fs FileSystem, files []string, mode Mode)
 
 // ParseFile parses the source code of a single Go+ source file and returns the corresponding ast.File node.
 func ParseFile(fset *token.FileSet, filename string, src interface{}, mode Mode) (f *ast.File, err error) {
-	return ParseFSFile(fset, local, filename, src, mode)
+	return ParseFSFile(fset, fsx.Local, filename, src, mode)
 }
 
 // ParseFSFile parses the source code of a single Go+ source file and returns the corresponding ast.File node.
@@ -299,7 +282,7 @@ func readSource(src interface{}) ([]byte, error) {
 			return s.Bytes(), nil
 		}
 	case io.Reader:
-		return ioutil.ReadAll(s)
+		return io.ReadAll(s)
 	}
 	return nil, errInvalidSource
 }
diff --git a/printer/nodes.go b/printer/nodes.go
index 67b2452c..0c1f2984 100644
--- a/printer/nodes.go
+++ b/printer/nodes.go
@@ -1735,11 +1735,13 @@ func (p *printer) spec(spec ast.Spec, n int, doIndent bool) {
 
 func (p *printer) genDecl(d *ast.GenDecl) {
 	p.setComment(d.Doc)
-	p.print(d.Pos(), d.Tok, blank)
+	p.setPos(d.Pos())
+	p.print(d.Tok, blank)
 
 	if d.Lparen.IsValid() || len(d.Specs) > 1 {
 		// group of parenthesized declarations
-		p.print(d.Lparen, token.LPAREN)
+		p.setPos(d.Lparen)
+		p.print(token.LPAREN)
 		if n := len(d.Specs); n > 0 {
 			p.print(indent, formfeed)
 			if n > 1 && (d.Tok == token.CONST || d.Tok == token.VAR) {
@@ -1749,7 +1751,7 @@ func (p *printer) genDecl(d *ast.GenDecl) {
 				var line int
 				for i, s := range d.Specs {
 					if i > 0 {
-						p.linebreak(p.lineFor(s.Pos()), 0, ignore, p.linesFrom(line) > 0)
+						p.linebreak(p.lineFor(s.Pos()), 1, ignore, p.linesFrom(line) > 0)
 					}
 					p.recordLine(&line)
 					p.valueSpec(s.(*ast.ValueSpec), keepType[i])
@@ -1766,7 +1768,8 @@ func (p *printer) genDecl(d *ast.GenDecl) {
 			}
 			p.print(unindent, formfeed)
 		}
-		p.print(d.Rparen, token.RPAREN)
+		p.setPos(d.Rparen)
+		p.print(token.RPAREN)
 
 	} else if len(d.Specs) > 0 {
 		// single declaration
diff --git a/printer/printer.go b/printer/printer.go
index 4040b208..2a4be02b 100644
--- a/printer/printer.go
+++ b/printer/printer.go
@@ -163,14 +163,12 @@ func (p *printer) nextComment() {
 // commentBefore reports whether the current comment group occurs
 // before the next position in the source code and printing it does
 // not introduce implicit semicolons.
-//
 func (p *printer) commentBefore(next token.Position) bool {
 	return p.commentOffset < next.Offset && (!p.impliedSemi || !p.commentNewline)
 }
 
 // commentSizeBefore returns the estimated size of the
 // comments on the same line before the next position.
-//
 func (p *printer) commentSizeBefore(next token.Position) int {
 	// save/restore current p.commentInfo (p.nextComment() modifies it)
 	defer func(info commentInfo) {
@@ -191,7 +189,6 @@ func (p *printer) commentSizeBefore(next token.Position) int {
 // token in *linePtr. It is used to compute an accurate line number for a
 // formatted construct, independent of pending (not yet emitted) whitespace
 // or comments.
-//
 func (p *printer) recordLine(linePtr *int) {
 	p.linePtr = linePtr
 }
@@ -200,7 +197,6 @@ func (p *printer) recordLine(linePtr *int) {
 // output line and the line argument, ignoring any pending (not yet
 // emitted) whitespace or comments. It is used to compute an accurate
 // size (in number of lines) for a formatted construct.
-//
 func (p *printer) linesFrom(line int) int {
 	return p.out.Line - line
 }
@@ -294,7 +290,6 @@ func (p *printer) writeByte(ch byte, n int) {
 // needed (i.e., when we don't know that s contains no tabs or line breaks)
 // avoids processing extra escape characters and reduces run time of the
 // printer benchmark by up to 10%.
-//
 func (p *printer) writeString(pos token.Position, s string, isLit bool) {
 	if p.out.Column == 1 {
 		if p.Config.Mode&SourcePos != 0 {
@@ -367,7 +362,6 @@ func (p *printer) writeString(pos token.Position, s string, isLit bool) {
 // pos is the comment position, next the position of the item
 // after all pending comments, prev is the previous comment in
 // a group of comments (or nil), and tok is the next token.
-//
 func (p *printer) writeCommentPrefix(pos, next token.Position, prev *ast.Comment, tok token.Token) {
 	if len(p.output) == 0 {
 		// the comment is the first item to be printed - don't write any whitespace
@@ -493,7 +487,6 @@ func (p *printer) writeCommentPrefix(pos, next token.Position, prev *ast.Comment
 
 // Returns true if s contains only white space
 // (only tabs and blanks can appear in the printer's context).
-//
 func isBlank(s string) bool {
 	for i := 0; i < len(s); i++ {
 		if s[i] > ' ' {
@@ -522,7 +515,6 @@ func trimRight(s string) string {
 // The prefix is computed using heuristics such that is likely that the comment
 // contents are nicely laid out after re-printing each line using the printer's
 // current indentation.
-//
 func stripCommonPrefix(lines []string) {
 	if len(lines) <= 1 {
 		return // at most one line - nothing to do
@@ -708,7 +700,6 @@ func (p *printer) writeComment(comment *ast.Comment) {
 // pending whitespace. The writeCommentSuffix result indicates if a
 // newline was written or if a formfeed was dropped from the whitespace
 // buffer.
-//
 func (p *printer) writeCommentSuffix(needsLinebreak bool) (wroteNewline, droppedFF bool) {
 	for i, ch := range p.wsbuf {
 		switch ch {
@@ -757,7 +748,6 @@ func (p *printer) containsLinebreak() bool {
 // that needs to be written before the next token). A heuristic is used to mix
 // the comments and whitespace. The intersperseComments result indicates if a
 // newline was written or if a formfeed was dropped from the whitespace buffer.
-//
 func (p *printer) intersperseComments(next token.Position, tok token.Token) (wroteNewline, droppedFF bool) {
 	var last *ast.Comment
 	for p.commentBefore(next) {
@@ -880,6 +870,12 @@ func mayCombine(prev token.Token, next byte) (b bool) {
 	return
 }
 
+func (p *printer) setPos(pos token.Pos) {
+	if pos.IsValid() {
+		p.pos = p.posFor(pos) // accurate position of next item
+	}
+}
+
 // print prints a list of "items" (roughly corresponding to syntactic
 // tokens, but also including whitespace and formatting information).
 // It is the only print function that should be called directly from
@@ -890,7 +886,6 @@ func mayCombine(prev token.Token, next byte) (b bool) {
 // taking into account the amount and structure of any pending white-
 // space for best comment placement. Then, any leftover whitespace is
 // printed, followed by the actual token.
-//
 func (p *printer) print(args ...interface{}) {
 	for _, arg := range args {
 		// information about the current arg
@@ -1033,7 +1028,6 @@ func (p *printer) print(args ...interface{}) {
 // before the position of the next token tok. The flush result indicates
 // if a newline was written or if a formfeed was dropped from the whitespace
 // buffer.
-//
 func (p *printer) flush(next token.Position, tok token.Token) (wroteNewline, droppedFF bool) {
 	if p.commentBefore(next) {
 		// if there are comments before the next item, intersperse them
@@ -1186,7 +1180,6 @@ unsupported:
 // and vtab characters into newlines and htabs (in case no tabwriter
 // is used). Text bracketed by tabwriter.Escape characters is passed
 // through unchanged.
-//
 type trimmer struct {
 	output io.Writer
 	state  int
@@ -1354,7 +1347,6 @@ func (cfg *Config) fprint(output io.Writer, fset *token.FileSet, node interface{
 
 // A CommentedNode bundles an AST node and corresponding comments.
 // It may be provided as argument to any of the Fprint functions.
-//
 type CommentedNode struct {
 	Node     interface{} // *ast.File, or ast.Expr, ast.Decl, ast.Spec, or ast.Stmt
 	Comments []*ast.CommentGroup
@@ -1364,7 +1356,6 @@ type CommentedNode struct {
 // Position information is interpreted relative to the file set fset.
 // The node type must be *ast.File, *CommentedNode, []ast.Decl, []ast.Stmt,
 // or assignment-compatible to ast.Expr, ast.Decl, ast.Spec, or ast.Stmt.
-//
 func (cfg *Config) Fprint(output io.Writer, fset *token.FileSet, node interface{}) error {
 	return cfg.fprint(output, fset, node, make(map[ast.Node]int))
 }
@@ -1373,7 +1364,6 @@ func (cfg *Config) Fprint(output io.Writer, fset *token.FileSet, node interface{
 // It calls Config.Fprint with default settings.
 // Note that gofmt uses tabs for indentation but spaces for alignment;
 // use format.Node (package go/format) for output that matches gofmt.
-//
 func Fprint(output io.Writer, fset *token.FileSet, node interface{}) error {
 	return (&Config{Tabwidth: 8}).Fprint(output, fset, node)
 }
diff --git a/x/build/build.go b/x/build/build.go
new file mode 100644
index 00000000..9373c616
--- /dev/null
+++ b/x/build/build.go
@@ -0,0 +1,217 @@
+/*
+ * Copyright (c) 2023 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 build
+
+import (
+	"bytes"
+	"fmt"
+	goast "go/ast"
+	"go/types"
+	"path"
+	"path/filepath"
+
+	"github.com/goplus/gop/ast"
+	"github.com/goplus/gop/cl"
+	"github.com/goplus/gop/parser"
+	"github.com/goplus/gop/parser/fsx/memfs"
+	"github.com/goplus/gop/token"
+	"github.com/goplus/gox"
+	"github.com/goplus/gox/packages"
+)
+
+type Class = cl.Class
+
+var (
+	projects = make(map[string]*cl.Project)
+)
+
+func RegisterClassFileType(ext string, class string, works []*Class, pkgPaths ...string) {
+	cls := &cl.Project{
+		Ext:      ext,
+		Class:    class,
+		Works:    works,
+		PkgPaths: pkgPaths,
+	}
+	if ext != "" {
+		projects[ext] = cls
+	}
+	for _, w := range works {
+		projects[w.Ext] = cls
+	}
+}
+
+func init() {
+	RegisterClassFileType(".gmx", "Game", []*Class{{Ext: ".spx", Class: "Sprite"}}, "github.com/goplus/spx", "math")
+}
+
+type Package struct {
+	Fset *token.FileSet
+	Pkg  *gox.Package
+}
+
+func (p *Package) ToSource() ([]byte, error) {
+	var buf bytes.Buffer
+	if err := gox.WriteTo(&buf, p.Pkg); err != nil {
+		return nil, err
+	}
+	return buf.Bytes(), nil
+}
+
+func (p *Package) ToAst() *goast.File {
+	return gox.ASTFile(p.Pkg)
+}
+
+func ClassKind(fname string) (isProj, ok bool) {
+	ext := path.Ext(fname)
+	switch ext {
+	case ".gmx":
+		return true, true
+	case ".spx":
+		return fname == "main.spx", true
+	default:
+		if c, ok := projects[ext]; ok {
+			for _, w := range c.Works {
+				if w.Ext == ext {
+					if ext != c.Ext || fname != "main"+ext {
+						return false, true
+					}
+					break
+				}
+			}
+			return true, true
+		}
+	}
+	return
+}
+
+type Context struct {
+	impl       types.Importer
+	fset       *token.FileSet
+	LoadConfig func(*cl.Config)
+}
+
+func Default() *Context {
+	fset := token.NewFileSet()
+	impl := packages.NewImporter(fset)
+	return NewContext(impl, fset)
+}
+
+func NewContext(impl types.Importer, fset *token.FileSet) *Context {
+	if fset == nil {
+		fset = token.NewFileSet()
+	}
+	return &Context{impl: impl, fset: fset}
+}
+
+func (c *Context) Import(path string) (*types.Package, error) {
+	return c.impl.Import(path)
+}
+
+func (c *Context) ParseDir(dir string) (*Package, error) {
+	pkgs, err := parser.ParseDirEx(c.fset, dir, parser.Config{
+		ClassKind: ClassKind,
+	})
+	if err != nil {
+		return nil, err
+	}
+	return c.loadPackage(dir, pkgs)
+}
+
+func (c *Context) ParseFSDir(fs parser.FileSystem, dir string) (*Package, error) {
+	pkgs, err := parser.ParseFSDir(c.fset, fs, dir, parser.Config{
+		ClassKind: ClassKind,
+	})
+	if err != nil {
+		return nil, err
+	}
+	return c.loadPackage(dir, pkgs)
+}
+
+func (c *Context) ParseFile(file string, src interface{}) (*Package, error) {
+	fs, err := memfs.File(file, src)
+	if err != nil {
+		return nil, err
+	}
+	return c.ParseFSDir(fs, filepath.Dir(file))
+}
+
+func (c *Context) loadPackage(srcDir string, pkgs map[string]*ast.Package) (*Package, error) {
+	mainPkg, ok := pkgs["main"]
+	if !ok {
+		for _, v := range pkgs {
+			mainPkg = v
+			break
+		}
+	}
+	conf := &cl.Config{
+		WorkingDir: srcDir, TargetDir: srcDir, Fset: c.fset}
+	conf.Importer = c
+	conf.LookupClass = func(ext string) (c *cl.Project, ok bool) {
+		c, ok = projects[ext]
+		return
+	}
+	if c.LoadConfig != nil {
+		c.LoadConfig(conf)
+	}
+	out, err := cl.NewPackage("", mainPkg, conf)
+	if err != nil {
+		return nil, err
+	}
+	return &Package{c.fset, out}, nil
+}
+
+func (ctx *Context) BuildFile(filename string, src interface{}) (data []byte, err error) {
+	defer func() {
+		r := recover()
+		if r != nil {
+			err = fmt.Errorf("compile %v failed. %v", filename, r)
+		}
+	}()
+	pkg, err := ctx.ParseFile(filename, src)
+	if err != nil {
+		return nil, err
+	}
+	return pkg.ToSource()
+}
+
+func (ctx *Context) BuildFSDir(fs parser.FileSystem, dir string) (data []byte, err error) {
+	defer func() {
+		r := recover()
+		if r != nil {
+			err = fmt.Errorf("compile %v failed. %v", dir, err)
+		}
+	}()
+	pkg, err := ctx.ParseFSDir(fs, dir)
+	if err != nil {
+		return nil, err
+	}
+	return pkg.ToSource()
+}
+
+func (ctx *Context) BuildDir(dir string) (data []byte, err error) {
+	defer func() {
+		r := recover()
+		if r != nil {
+			err = fmt.Errorf("compile %v failed. %v", dir, err)
+		}
+	}()
+	pkg, err := ctx.ParseDir(dir)
+	if err != nil {
+		return nil, err
+	}
+	return pkg.ToSource()
+}


Test patch:
diff --git a/ast/goptest/gopq.go b/ast/goptest/gopq.go
index b51182b1..c567dda2 100644
--- a/ast/goptest/gopq.go
+++ b/ast/goptest/gopq.go
@@ -20,7 +20,7 @@ import (
 	"go/token"
 
 	"github.com/goplus/gop/ast/gopq"
-	"github.com/goplus/gop/parser/parsertest"
+	"github.com/goplus/gop/parser/fsx/memfs"
 )
 
 // -----------------------------------------------------------------------------
@@ -28,7 +28,7 @@ import (
 // New creates a nodeset object that represents a Go+ dom tree.
 func New(script string) (gopq.NodeSet, error) {
 	fset := token.NewFileSet()
-	fs := parsertest.NewSingleFileFS("/foo", "bar.gop", script)
+	fs := memfs.SingleFile("/foo", "bar.gop", script)
 	return gopq.NewSourceFrom(fset, fs, "/foo", nil, 0)
 }
 
diff --git a/cl/compile_spx_test.go b/cl/compile_spx_test.go
index fc25f66f..7ceb8a59 100644
--- a/cl/compile_spx_test.go
+++ b/cl/compile_spx_test.go
@@ -24,7 +24,7 @@ import (
 
 	"github.com/goplus/gop/cl"
 	"github.com/goplus/gop/parser"
-	"github.com/goplus/gop/parser/parsertest"
+	"github.com/goplus/gop/parser/fsx/memfs"
 	"github.com/goplus/gop/scanner"
 	"github.com/goplus/mod/modfile"
 )
@@ -77,7 +77,7 @@ func gopSpxTestExConf(t *testing.T, name string, conf *cl.Config, gmx, spxcode,
 		cl.SetDisableRecover(true)
 		defer cl.SetDisableRecover(false)
 
-		fs := parsertest.NewTwoFilesFS("/foo", spxfile, spxcode, gmxfile, gmx)
+		fs := memfs.TwoFiles("/foo", spxfile, spxcode, gmxfile, gmx)
 		pkgs, err := parser.ParseFSDir(gblFset, fs, "/foo", spxParserConf())
 		if err != nil {
 			scanner.PrintError(os.Stderr, err)
@@ -101,7 +101,7 @@ func gopSpxTestExConf(t *testing.T, name string, conf *cl.Config, gmx, spxcode,
 }
 
 func gopSpxErrorTestEx(t *testing.T, msg, gmx, spxcode, gmxfile, spxfile string) {
-	fs := parsertest.NewTwoFilesFS("/foo", spxfile, spxcode, gmxfile, gmx)
+	fs := memfs.TwoFiles("/foo", spxfile, spxcode, gmxfile, gmx)
 	pkgs, err := parser.ParseFSDir(gblFset, fs, "/foo", spxParserConf())
 	if err != nil {
 		scanner.PrintError(os.Stderr, err)
diff --git a/cl/compile_test.go b/cl/compile_test.go
index 3335a989..21e2d561 100644
--- a/cl/compile_test.go
+++ b/cl/compile_test.go
@@ -26,7 +26,7 @@ import (
 	"github.com/goplus/gop"
 	"github.com/goplus/gop/cl"
 	"github.com/goplus/gop/parser"
-	"github.com/goplus/gop/parser/parsertest"
+	"github.com/goplus/gop/parser/fsx/memfs"
 	"github.com/goplus/gop/scanner"
 	"github.com/goplus/gop/token"
 	"github.com/goplus/gox"
@@ -69,17 +69,17 @@ func gopClTest(t *testing.T, gopcode, expected string) {
 }
 
 func gopClTestFile(t *testing.T, gopcode, expected string, fname string) {
-	fs := parsertest.NewSingleFileFS("/foo", fname, gopcode)
+	fs := memfs.SingleFile("/foo", fname, gopcode)
 	gopClTestFS(t, gblConf, fs, "main", expected)
 }
 
 func gopClTestEx(t *testing.T, conf *cl.Config, pkgname, gopcode, expected string) {
-	fs := parsertest.NewSingleFileFS("/foo", "bar.gop", gopcode)
+	fs := memfs.SingleFile("/foo", "bar.gop", gopcode)
 	gopClTestFS(t, conf, fs, pkgname, expected)
 }
 
 func gopMixedClTest(t *testing.T, pkgname, gocode, gopcode, expected string) {
-	fs := parsertest.NewTwoFilesFS("/foo", "a.go", gocode, "b.gop", gopcode)
+	fs := memfs.TwoFiles("/foo", "a.go", gocode, "b.gop", gopcode)
 	gopClTestFS(t, gblConf, fs, pkgname, expected)
 }
 
@@ -4515,3 +4515,45 @@ func main() {
 }
 `)
 }
+
+func TestClassFileMember(t *testing.T) {
+	gopClTestFile(t, `type Engine struct {
+}
+
+func (e *Engine) EnterPointerLock() {
+}
+
+func (e *Engine) SetEnable(b bool) {
+}
+
+
+func Engine() *Engine {
+	return &Engine{}
+}
+
+func Test() {
+	engine.setEnable true
+	engine.enterPointerLock
+}
+`, `package main
+
+type Engine struct {
+}
+
+func (e *Engine) EnterPointerLock() {
+}
+func (e *Engine) SetEnable(b bool) {
+}
+
+type Rect struct {
+}
+
+func (this *Rect) Engine() *Engine {
+	return &Engine{}
+}
+func (this *Rect) Test() {
+	this.Engine().SetEnable(true)
+	this.Engine().EnterPointerLock()
+}
+`, "Rect.gox")
+}
diff --git a/cl/error_msg_test.go b/cl/error_msg_test.go
index 28e92643..1a1f049a 100644
--- a/cl/error_msg_test.go
+++ b/cl/error_msg_test.go
@@ -25,7 +25,7 @@ import (
 
 	"github.com/goplus/gop/cl"
 	"github.com/goplus/gop/parser"
-	"github.com/goplus/gop/parser/parsertest"
+	"github.com/goplus/gop/parser/fsx/memfs"
 	"github.com/goplus/gop/scanner"
 )
 
@@ -34,7 +34,7 @@ func codeErrorTest(t *testing.T, msg, src string) {
 }
 
 func codeErrorTestEx(t *testing.T, pkgname, filename, msg, src string) {
-	fs := parsertest.NewSingleFileFS("/foo", filename, src)
+	fs := memfs.SingleFile("/foo", filename, src)
 	pkgs, err := parser.ParseFSDir(gblFset, fs, "/foo", parser.Config{})
 	if err != nil {
 		scanner.PrintError(os.Stderr, err)
diff --git a/cl/run_test.go b/cl/run_test.go
index e05b025d..7f31b1b7 100644
--- a/cl/run_test.go
+++ b/cl/run_test.go
@@ -13,7 +13,7 @@ import (
 
 	"github.com/goplus/gop/cl"
 	"github.com/goplus/gop/parser"
-	"github.com/goplus/gop/parser/parsertest"
+	"github.com/goplus/gop/parser/fsx/memfs"
 	"github.com/goplus/gop/scanner"
 )
 
@@ -83,7 +83,7 @@ func genGo(t *testing.T, conf *cl.Config, gopcode string) []byte {
 	cl.SetDisableRecover(true)
 	defer cl.SetDisableRecover(false)
 
-	fs := parsertest.NewSingleFileFS("/foo", "bar.gop", gopcode)
+	fs := memfs.SingleFile("/foo", "bar.gop", gopcode)
 	pkgs, err := parser.ParseFSDir(gblFset, fs, "/foo", parser.Config{Mode: parser.ParseComments})
 	if err != nil {
 		scanner.PrintError(os.Stderr, err)
diff --git a/cl/typeparams_test.go b/cl/typeparams_test.go
index 64adabba..10463024 100644
--- a/cl/typeparams_test.go
+++ b/cl/typeparams_test.go
@@ -11,7 +11,7 @@ import (
 
 	"github.com/goplus/gop/cl"
 	"github.com/goplus/gop/parser"
-	"github.com/goplus/gop/parser/parsertest"
+	"github.com/goplus/gop/parser/fsx/memfs"
 )
 
 func TestTypeParamsFunc(t *testing.T) {
@@ -203,7 +203,7 @@ func mixedErrorTest(t *testing.T, msg, gocode, gopcode string) {
 }
 
 func mixedErrorTestEx(t *testing.T, pkgname, msg, gocode, gopcode string) {
-	fs := parsertest.NewTwoFilesFS("/foo", "a.go", gocode, "b.gop", gopcode)
+	fs := memfs.TwoFiles("/foo", "a.go", gocode, "b.gop", gopcode)
 	pkgs, err := parser.ParseFSDir(gblFset, fs, "/foo", parser.Config{})
 	if err != nil {
 		scanner.PrintError(os.Stderr, err)
diff --git a/parser/parsertest/memfs.go b/parser/fsx/memfs/memfs.go
similarity index 69%
rename from parser/parsertest/memfs.go
rename to parser/fsx/memfs/memfs.go
index 09e7b9ef..82efae4a 100644
--- a/parser/parsertest/memfs.go
+++ b/parser/fsx/memfs/memfs.go
@@ -14,12 +14,12 @@
  * limitations under the License.
  */
 
-package parsertest
+package memfs
 
 import (
 	"io/fs"
-	"os"
 	"path"
+	"path/filepath"
 	"syscall"
 	"time"
 )
@@ -28,22 +28,27 @@ import (
 
 type memFileInfo struct {
 	name string
+	size int
 }
 
 func (p *memFileInfo) Name() string {
-	return p.name
+	return filepath.Base(p.name)
 }
 
 func (p *memFileInfo) Size() int64 {
+	return int64(p.size)
+}
+
+func (p *memFileInfo) Mode() fs.FileMode {
 	return 0
 }
 
-func (p *memFileInfo) Mode() os.FileMode {
+func (p *memFileInfo) Type() fs.FileMode {
 	return 0
 }
 
 func (p *memFileInfo) ModTime() (t time.Time) {
-	return
+	return time.Now()
 }
 
 func (p *memFileInfo) IsDir() bool {
@@ -54,22 +59,28 @@ func (p *memFileInfo) Sys() interface{} {
 	return nil
 }
 
-// MemFS represents a file system in memory.
-type MemFS struct {
+func (p *memFileInfo) Info() (fs.FileInfo, error) {
+	return p, nil
+}
+
+// -----------------------------------------------------------------------------
+
+// FS represents a file system in memory.
+type FS struct {
 	dirs  map[string][]string
 	files map[string]string
 }
 
-// NewMemFS creates a MemFS instance.
-func NewMemFS(dirs map[string][]string, files map[string]string) *MemFS {
-	return &MemFS{dirs: dirs, files: files}
+// New creates a file system instance.
+func New(dirs map[string][]string, files map[string]string) *FS {
+	return &FS{dirs: dirs, files: files}
 }
 
 // ReadDir reads the directory named by dirname and returns
 // a list of directory entries sorted by filename.
-func (p *MemFS) ReadDir(dirname string) ([]fs.FileInfo, error) {
+func (p *FS) ReadDir(dirname string) ([]fs.DirEntry, error) {
 	if items, ok := p.dirs[dirname]; ok {
-		fis := make([]fs.FileInfo, len(items))
+		fis := make([]fs.DirEntry, len(items))
 		for i, item := range items {
 			fis[i] = &memFileInfo{name: item}
 		}
@@ -82,7 +93,7 @@ func (p *MemFS) ReadDir(dirname string) ([]fs.FileInfo, error) {
 // A successful call returns err == nil, not err == EOF. Because ReadFile
 // reads the whole file, it does not treat an EOF from Read as an error
 // to be reported.
-func (p *MemFS) ReadFile(filename string) ([]byte, error) {
+func (p *FS) ReadFile(filename string) ([]byte, error) {
 	if data, ok := p.files[filename]; ok {
 		return []byte(data), nil
 	}
@@ -94,24 +105,24 @@ func (p *MemFS) ReadFile(filename string) ([]byte, error) {
 // The result is Cleaned. However, if the argument list is
 // empty or all its elements are empty, Join returns
 // an empty string.
-func (p *MemFS) Join(elem ...string) string {
+func (p *FS) Join(elem ...string) string {
 	return path.Join(elem...)
 }
 
 // -----------------------------------------------------------------------------
 
-// NewSingleFileFS creates a file system that only contains a single file.
-func NewSingleFileFS(dir string, fname string, data string) *MemFS {
-	return NewMemFS(map[string][]string{
+// SingleFile creates a file system that only contains a single file.
+func SingleFile(dir string, fname string, data string) *FS {
+	return New(map[string][]string{
 		dir: {fname},
 	}, map[string]string{
 		path.Join(dir, fname): data,
 	})
 }
 
-// NewTwoFilesFS creates a file system that contains two files.
-func NewTwoFilesFS(dir string, fname1, data1, fname2, data2 string) *MemFS {
-	return NewMemFS(map[string][]string{
+// TwoFiles creates a file system that contains two files.
+func TwoFiles(dir string, fname1, data1, fname2, data2 string) *FS {
+	return New(map[string][]string{
 		dir: {fname1, fname2},
 	}, map[string]string{
 		path.Join(dir, fname1): data1,
diff --git a/parser/parser_test.go b/parser/parser_test.go
index e1e866d5..1aed0e3f 100644
--- a/parser/parser_test.go
+++ b/parser/parser_test.go
@@ -136,6 +136,20 @@ func h() { var }
 `, `/foo/bar.gop:2:16: expected 'IDENT', found '}' (and 10 more errors)`, ``)
 }
 
+func TestErrInFunc(t *testing.T) {
+	testErrCode(t, `func test() {
+	a,
+}`, `/foo/bar.gop:2:2: expected 1 expression (and 2 more errors)`, ``)
+	testErrCode(t, `func test() {
+	a.test, => {
+	}
+}`, `/foo/bar.gop:2:10: expected operand, found '=>' (and 1 more errors)`, ``)
+	testErrCode(t, `func test() {
+		,
+	}
+}`, `/foo/bar.gop:2:3: expected statement, found ',' (and 1 more errors)`, ``)
+}
+
 // -----------------------------------------------------------------------------
 
 var testStdCode = `package bar; import "io"
diff --git a/parser/parserdir_test.go b/parser/parserdir_test.go
index bb84de9f..10fcc025 100644
--- a/parser/parserdir_test.go
+++ b/parser/parserdir_test.go
@@ -26,6 +26,7 @@ import (
 	"syscall"
 	"testing"
 
+	"github.com/goplus/gop/parser/fsx/memfs"
 	"github.com/goplus/gop/parser/parsertest"
 	"github.com/goplus/gop/scanner"
 	"github.com/goplus/gop/token"
@@ -132,7 +133,7 @@ func TestParseGoFiles(t *testing.T) {
 
 func TestGopAutoGen(t *testing.T) {
 	fset := token.NewFileSet()
-	fs := parsertest.NewSingleFileFS("/foo", "gop_autogen.go", ``)
+	fs := memfs.SingleFile("/foo", "gop_autogen.go", ``)
 	pkgs, err := ParseFSDir(fset, fs, "/foo", Config{})
 	if err != nil {
 		t.Fatal("ParseFSDir:", err)
@@ -144,7 +145,7 @@ func TestGopAutoGen(t *testing.T) {
 
 func TestGoFile(t *testing.T) {
 	fset := token.NewFileSet()
-	fs := parsertest.NewSingleFileFS("/foo", "test.go", `package foo`)
+	fs := memfs.SingleFile("/foo", "test.go", `package foo`)
 	pkgs, err := ParseFSDir(fset, fs, "/foo", Config{})
 	if err != nil {
 		t.Fatal("ParseFSDir:", err)
@@ -156,25 +157,25 @@ func TestGoFile(t *testing.T) {
 
 func TestErrParse(t *testing.T) {
 	fset := token.NewFileSet()
-	fs := parsertest.NewSingleFileFS("/foo", "test.go", `package foo bar`)
+	fs := memfs.SingleFile("/foo", "test.go", `package foo bar`)
 	_, err := ParseFSDir(fset, fs, "/foo", Config{})
 	if err == nil {
 		t.Fatal("ParseFSDir test.go: no error?")
 	}
 
-	fs = parsertest.NewSingleFileFS("/foo", "test.gop", `package foo bar`)
+	fs = memfs.SingleFile("/foo", "test.gop", `package foo bar`)
 	_, err = ParseFSDir(fset, fs, "/foo", Config{})
 	if err == nil {
 		t.Fatal("ParseFSDir test.gop: no error?")
 	}
 
-	fs = parsertest.NewMemFS(map[string][]string{"/foo": {"test.go"}}, map[string]string{})
+	fs = memfs.New(map[string][]string{"/foo": {"test.go"}}, map[string]string{})
 	_, err = ParseFSDir(fset, fs, "/foo", Config{})
 	if err != syscall.ENOENT {
 		t.Fatal("ParseFSDir:", err)
 	}
 
-	fs = parsertest.NewSingleFileFS("/foo", "test.abc.gox", `package foo`)
+	fs = memfs.SingleFile("/foo", "test.abc.gox", `package foo`)
 	_, err = ParseFSDir(fset, fs, "/foo", Config{})
 	if err == nil {
 		t.Fatal("ParseFSDir test.gop: no error?")
diff --git a/printer/bugfix_test.go b/printer/bugfix_test.go
new file mode 100644
index 00000000..6d0842f1
--- /dev/null
+++ b/printer/bugfix_test.go
@@ -0,0 +1,66 @@
+package printer_test
+
+import (
+	"bytes"
+	"testing"
+
+	goast "go/ast"
+	goprinter "go/printer"
+	gotoken "go/token"
+
+	"github.com/goplus/gop/ast"
+	"github.com/goplus/gop/printer"
+	"github.com/goplus/gop/token"
+)
+
+func goIdents(name string) []*goast.Ident {
+	return []*goast.Ident{{Name: name}}
+}
+
+func TestGoFormat(t *testing.T) {
+	const (
+		mode = goprinter.UseSpaces | goprinter.TabIndent
+	)
+	config := goprinter.Config{Mode: mode, Indent: 0, Tabwidth: 8}
+	decl := &goast.GenDecl{Tok: gotoken.VAR, Lparen: 1, Rparen: 1}
+	decl.Specs = []goast.Spec{
+		&goast.ValueSpec{Names: goIdents("foo"), Type: goast.NewIdent("int")},
+		&goast.ValueSpec{Names: goIdents("bar"), Type: goast.NewIdent("string")},
+	}
+	b := bytes.NewBuffer(nil)
+	fset := gotoken.NewFileSet()
+	config.Fprint(b, fset, decl)
+	const codeExp = `var (
+	foo int
+	bar string
+)`
+	if code := b.String(); code != codeExp {
+		t.Fatal("config.Fprint:", code, codeExp)
+	}
+}
+
+func gopIdents(name string) []*ast.Ident {
+	return []*ast.Ident{{Name: name}}
+}
+
+func TestGopFormat(t *testing.T) {
+	const (
+		mode = printer.UseSpaces | printer.TabIndent
+	)
+	config := printer.Config{Mode: mode, Indent: 0, Tabwidth: 8}
+	decl := &ast.GenDecl{Tok: token.VAR, Lparen: 1, Rparen: 1}
+	decl.Specs = []ast.Spec{
+		&ast.ValueSpec{Names: gopIdents("foo"), Type: ast.NewIdent("int")},
+		&ast.ValueSpec{Names: gopIdents("bar"), Type: ast.NewIdent("string")},
+	}
+	b := bytes.NewBuffer(nil)
+	fset := token.NewFileSet()
+	config.Fprint(b, fset, decl)
+	const codeExp = `var (
+	foo int
+	bar string
+)`
+	if code := b.String(); code != codeExp {
+		t.Fatal("config.Fprint:", code, codeExp)
+	}
+}
diff --git a/x/build/_testdata/hello/hello.expect b/x/build/_testdata/hello/hello.expect
new file mode 100644
index 00000000..e62ecea4
--- /dev/null
+++ b/x/build/_testdata/hello/hello.expect
@@ -0,0 +1,7 @@
+package main
+
+import fmt "fmt"
+
+func main() {
+	fmt.Println("Go+")
+}
diff --git a/x/build/_testdata/hello/main.gop b/x/build/_testdata/hello/main.gop
new file mode 100644
index 00000000..3cb9271c
--- /dev/null
+++ b/x/build/_testdata/hello/main.gop
@@ -0,0 +1,1 @@
+println "Go+"
diff --git a/x/build/_testdata/multi/Rect.gox b/x/build/_testdata/multi/Rect.gox
new file mode 100644
index 00000000..f403e2b3
--- /dev/null
+++ b/x/build/_testdata/multi/Rect.gox
@@ -0,0 +1,16 @@
+var (
+	BaseClass
+	Width, Height float64
+	*AggClass
+)
+
+type BaseClass struct {
+	x int
+	y int
+}
+
+type AggClass struct{}
+
+func Area() float64 {
+	return Width * Height
+}
diff --git a/x/build/_testdata/multi/main.gop b/x/build/_testdata/multi/main.gop
new file mode 100644
index 00000000..be2fce78
--- /dev/null
+++ b/x/build/_testdata/multi/main.gop
@@ -0,0 +1,2 @@
+rc := &Rect{Width: 100, Height: 200}
+println rc
diff --git a/x/build/_testdata/multi/multi.expect b/x/build/_testdata/multi/multi.expect
new file mode 100644
index 00000000..9eb31ec2
--- /dev/null
+++ b/x/build/_testdata/multi/multi.expect
@@ -0,0 +1,24 @@
+package main
+
+import fmt "fmt"
+
+type BaseClass struct {
+	x int
+	y int
+}
+type AggClass struct {
+}
+type Rect struct {
+	BaseClass
+	Width  float64
+	Height float64
+	*AggClass
+}
+
+func (this *Rect) Area() float64 {
+	return this.Width * this.Height
+}
+func main() {
+	rc := &Rect{Width: 100, Height: 200}
+	fmt.Println(rc)
+}
diff --git a/x/build/_testdata/pkg/pkg.expect b/x/build/_testdata/pkg/pkg.expect
new file mode 100644
index 00000000..e71e8cb8
--- /dev/null
+++ b/x/build/_testdata/pkg/pkg.expect
@@ -0,0 +1,7 @@
+package pkg
+
+import fmt "fmt"
+
+func Hello() {
+	fmt.Println("Go+")
+}
diff --git a/x/build/_testdata/pkg/pkg.gop b/x/build/_testdata/pkg/pkg.gop
new file mode 100644
index 00000000..fba85a65
--- /dev/null
+++ b/x/build/_testdata/pkg/pkg.gop
@@ -0,0 +1,5 @@
+package pkg
+
+func Hello() {
+	println "Go+"
+}
diff --git a/x/build/build_test.go b/x/build/build_test.go
new file mode 100644
index 00000000..72e46f14
--- /dev/null
+++ b/x/build/build_test.go
@@ -0,0 +1,522 @@
+/*
+ * Copyright (c) 2022 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 build_test
+
+import (
+	"bytes"
+	"fmt"
+	"go/printer"
+	"go/types"
+	"io/ioutil"
+	"os"
+	"path"
+	"path/filepath"
+	"strings"
+	"testing"
+
+	"github.com/goplus/gop/cl"
+	"github.com/goplus/gop/parser/fsx"
+	"github.com/goplus/gop/x/build"
+)
+
+var (
+	ctx = build.Default()
+)
+
+func init() {
+	ctx.LoadConfig = func(cfg *cl.Config) {
+		cfg.NoFileLine = true
+	}
+	build.RegisterClassFileType(".tspx", "MyGame", []*build.Class{
+		{Ext: ".tspx", Class: "Sprite"},
+	}, "github.com/goplus/gop/cl/internal/spx")
+}
+
+func gopClTest(t *testing.T, gopcode interface{}, expected string) {
+	gopClTestEx(t, "main.gop", gopcode, expected)
+}
+
+func gopClTestEx(t *testing.T, filename string, gopcode interface{}, expected string) {
+	data, err := ctx.BuildFile(filename, gopcode)
+	if err != nil {
+		t.Fatalf("build gop error: %v", err)
+	}
+	if string(data) != expected {
+		fmt.Println("build gop error:")
+		fmt.Println(string(data))
+		t.Fail()
+	}
+}
+
+func testKind(t *testing.T, name string, proj, class bool) {
+	isProj, ok := build.ClassKind(name)
+	if isProj != proj || ok != class {
+		t.Fatal("check classkind failed", name, isProj, ok)
+	}
+}
+
+func TestKind(t *testing.T) {
+	testKind(t, "Cat.gox", false, false)
+	testKind(t, "Cat.spx", false, true)
+	testKind(t, "main.spx", true, true)
+	testKind(t, "main.gmx", true, true)
+	testKind(t, "Cat.tspx", false, true)
+	testKind(t, "main.tspx", true, true)
+}
+
+func TestGop(t *testing.T) {
+	var src = `
+println "Go+"
+`
+	var expect = `package main
+
+import fmt "fmt"
+
+func main() {
+	fmt.Println("Go+")
+}
+`
+	gopClTest(t, src, expect)
+	gopClTest(t, []byte(src), expect)
+	gopClTest(t, bytes.NewBufferString(src), expect)
+	gopClTestEx(t, `./_testdata/hello/main.gop`, nil, expect)
+
+	f, err := os.Open("./_testdata/hello/main.gop")
+	if err != nil {
+		t.Fatal("open failed", err)
+	}
+	defer f.Close()
+	gopClTest(t, f, expect)
+}
+
+func TestGox(t *testing.T) {
+	gopClTestEx(t, "Rect.gox", `
+println "Go+"
+`, `package main
+
+import fmt "fmt"
+
+type Rect struct {
+}
+
+func (this *Rect) Main() {
+	fmt.Println("Go+")
+}
+func main() {
+}
+`)
+	gopClTestEx(t, "Rect.gox", `
+var (
+	Buffer
+	v int
+)
+type Buffer struct {
+	buf []byte
+}
+println "Go+"
+`, `package main
+
+import fmt "fmt"
+
+type Buffer struct {
+	buf []byte
+}
+type Rect struct {
+	Buffer
+	v int
+}
+
+func (this *Rect) Main() {
+	fmt.Println("Go+")
+}
+func main() {
+}
+`)
+	gopClTestEx(t, "Rect.gox", `
+var (
+	*Buffer
+	v int
+)
+type Buffer struct {
+	buf []byte
+}
+println "Go+"
+`, `package main
+
+import fmt "fmt"
+
+type Buffer struct {
+	buf []byte
+}
+type Rect struct {
+	*Buffer
+	v int
+}
+
+func (this *Rect) Main() {
+	fmt.Println("Go+")
+}
+func main() {
+}
+`)
+	gopClTestEx(t, "Rect.gox", `
+import "bytes"
+var (
+	*bytes.Buffer
+	v int
+)
+println "Go+"
+`, `package main
+
+import (
+	fmt "fmt"
+	bytes "bytes"
+)
+
+type Rect struct {
+	*bytes.Buffer
+	v int
+}
+
+func (this *Rect) Main() {
+	fmt.Println("Go+")
+}
+func main() {
+}
+`)
+	gopClTestEx(t, "Rect.gox", `
+import "bytes"
+var (
+	bytes.Buffer
+	v int
+)
+println "Go+"
+`, `package main
+
+import (
+	fmt "fmt"
+	bytes "bytes"
+)
+
+type Rect struct {
+	bytes.Buffer
+	v int
+}
+
+func (this *Rect) Main() {
+	fmt.Println("Go+")
+}
+func main() {
+}
+`)
+}
+
+func TestBig(t *testing.T) {
+	gopClTest(t, `
+a := 1/2r
+println a+1/2r
+`, `package main
+
+import (
+	fmt "fmt"
+	ng "github.com/goplus/gop/builtin/ng"
+	big "math/big"
+)
+
+func main() {
+	a := ng.Bigrat_Init__2(big.NewRat(1, 2))
+	fmt.Println(a.Gop_Add(ng.Bigrat_Init__2(big.NewRat(1, 2))))
+}
+`)
+}
+
+func TestIoxLines(t *testing.T) {
+	gopClTest(t, `
+import "io"
+
+var r io.Reader
+
+for line <- lines(r) {
+	println line
+}
+`, `package main
+
+import (
+	fmt "fmt"
+	iox "github.com/goplus/gop/builtin/iox"
+	io "io"
+)
+
+var r io.Reader
+
+func main() {
+	for _gop_it := iox.Lines(r).Gop_Enum(); ; {
+		var _gop_ok bool
+		line, _gop_ok := _gop_it.Next()
+		if !_gop_ok {
+			break
+		}
+		fmt.Println(line)
+	}
+}
+`)
+}
+
+func TestErrorWrap(t *testing.T) {
+	gopClTest(t, `
+import (
+    "strconv"
+)
+
+func add(x, y string) (int, error) {
+    return strconv.Atoi(x)? + strconv.Atoi(y)?, nil
+}
+
+func addSafe(x, y string) int {
+    return strconv.Atoi(x)?:0 + strconv.Atoi(y)?:0
+}
+
+println add("100", "23")!
+
+sum, err := add("10", "abc")
+println sum, err
+
+println addSafe("10", "abc")
+`, `package main
+
+import (
+	fmt "fmt"
+	strconv "strconv"
+	errors "github.com/qiniu/x/errors"
+)
+
+func add(x string, y string) (int, error) {
+	var _autoGo_1 int
+	{
+		var _gop_err error
+		_autoGo_1, _gop_err = strconv.Atoi(x)
+		if _gop_err != nil {
+			_gop_err = errors.NewFrame(_gop_err, "strconv.Atoi(x)", "main.gop", 7, "main.add")
+			return 0, _gop_err
+		}
+		goto _autoGo_2
+	_autoGo_2:
+	}
+	var _autoGo_3 int
+	{
+		var _gop_err error
+		_autoGo_3, _gop_err = strconv.Atoi(y)
+		if _gop_err != nil {
+			_gop_err = errors.NewFrame(_gop_err, "strconv.Atoi(y)", "main.gop", 7, "main.add")
+			return 0, _gop_err
+		}
+		goto _autoGo_4
+	_autoGo_4:
+	}
+	return _autoGo_1 + _autoGo_3, nil
+}
+func addSafe(x string, y string) int {
+	return func() (_gop_ret int) {
+		var _gop_err error
+		_gop_ret, _gop_err = strconv.Atoi(x)
+		if _gop_err != nil {
+			return 0
+		}
+		return
+	}() + func() (_gop_ret int) {
+		var _gop_err error
+		_gop_ret, _gop_err = strconv.Atoi(y)
+		if _gop_err != nil {
+			return 0
+		}
+		return
+	}()
+}
+func main() {
+	fmt.Println(func() (_gop_ret int) {
+		var _gop_err error
+		_gop_ret, _gop_err = add("100", "23")
+		if _gop_err != nil {
+			_gop_err = errors.NewFrame(_gop_err, "add(\"100\", \"23\")", "main.gop", 14, "main.main")
+			panic(_gop_err)
+		}
+		return
+	}())
+	sum, err := add("10", "abc")
+	fmt.Println(sum, err)
+	fmt.Println(addSafe("10", "abc"))
+}
+`)
+}
+
+func TestSpx(t *testing.T) {
+	gopClTestEx(t, "main.tspx", `println "hi"`, `package main
+
+import (
+	fmt "fmt"
+	spx "github.com/goplus/gop/cl/internal/spx"
+)
+
+type MyGame struct {
+	spx.MyGame
+}
+
+func (this *MyGame) MainEntry() {
+	fmt.Println("hi")
+}
+func main() {
+	spx.Gopt_MyGame_Main(new(MyGame))
+}
+`)
+	gopClTestEx(t, "Cat.tspx", `println "hi"`, `package main
+
+import (
+	fmt "fmt"
+	spx "github.com/goplus/gop/cl/internal/spx"
+)
+
+type Cat struct {
+	spx.Sprite
+}
+
+func (this *Cat) Main() {
+	fmt.Println("hi")
+}
+func main() {
+}
+`)
+}
+
+func testFromDir(t *testing.T, relDir string) {
+	dir, err := os.Getwd()
+	if err != nil {
+		t.Fatal("Getwd failed:", err)
+	}
+	dir = path.Join(dir, relDir)
+	fis, err := ioutil.ReadDir(dir)
+	if err != nil {
+		t.Fatal("ReadDir failed:", err)
+	}
+	for _, fi := range fis {
+		name := fi.Name()
+		if strings.HasPrefix(name, "_") {
+			continue
+		}
+		t.Run(name, func(t *testing.T) {
+			testFrom(t, name, dir+"/"+name)
+		})
+	}
+}
+
+func testFrom(t *testing.T, name, dir string) {
+	data, err := ctx.BuildDir(dir)
+	if err != nil {
+		t.Fatal("BuildDir failed:", err)
+	}
+	if chk, err := os.ReadFile(filepath.Join(dir, name+".expect")); err == nil {
+		if !bytes.Equal(data, chk) {
+			t.Fatalf("-- %v output check error --\n%v\n--\n%v", name, string(data), string(chk))
+		}
+	}
+}
+
+func TestFromTestdata(t *testing.T) {
+	testFromDir(t, "./_testdata")
+}
+
+func TestFS(t *testing.T) {
+	var expect = []byte(`package main
+
+import fmt "fmt"
+
+func main() {
+	fmt.Println("Go+")
+}
+`)
+	data, err := ctx.BuildFSDir(fsx.Local, "./_testdata/hello")
+	if err != nil {
+		t.Fatal("build fs dir failed", err)
+	}
+	if !bytes.Equal(data, expect) {
+		t.Fatal("build fs data failed", string(data))
+	}
+}
+
+func TestAst(t *testing.T) {
+	var expect = []byte(`package main
+
+import fmt "fmt"
+
+func main() {
+	fmt.Println("Go+")
+}
+`)
+	pkg, err := ctx.ParseFSDir(fsx.Local, "./_testdata/hello")
+	if err != nil {
+		t.Fatal("parser fs dir failed", err)
+	}
+	var buf bytes.Buffer
+	err = printer.Fprint(&buf, pkg.Fset, pkg.ToAst())
+	if err != nil {
+		t.Fatal("fprint ast error", err)
+	}
+	if !bytes.Equal(buf.Bytes(), expect) {
+		t.Fatal("build ast data failed", buf.String())
+	}
+}
+
+func TestError(t *testing.T) {
+	_, err := ctx.BuildFile("main.gop", "bad code")
+	if err == nil {
+		t.Fatal("BuildFile: no error?")
+	}
+	_, err = ctx.BuildDir("./testdata/nofound")
+	if err == nil {
+		t.Fatal("BuildDir: no error?")
+	}
+	_, err = ctx.BuildFSDir(fsx.Local, "./testdata/nofound")
+	if err == nil {
+		t.Fatal("BuildDir: no error?")
+	}
+	_, err = ctx.BuildFile("main.gop", "func main()")
+	if err == nil {
+		t.Fatal("BuildFile: no error?")
+	}
+	_, err = ctx.ParseFile("main.gop", 123)
+	if err == nil {
+		t.Fatal("ParseFile: no error?")
+	}
+	_, err = ctx.ParseFile("./testdata/nofound/main.gop", nil)
+	if err == nil {
+		t.Fatal("ParseFile: no error?")
+	}
+}
+
+type emptyImporter struct {
+}
+
+func (i *emptyImporter) Import(path string) (*types.Package, error) {
+	return nil, fmt.Errorf("not found %v", path)
+}
+
+func TestContext(t *testing.T) {
+	ctx := build.NewContext(&emptyImporter{}, nil)
+	_, err := ctx.BuildFile("main.gop", `println "Go+"`)
+	if err == nil {
+		t.Fatal("BuildFile: no error?")
+	}
+}