Repository: goplus/gop
Pull request #1651 (2024-01-20 17:41:14)

classfile: support multiple projects


Patch:
diff --git a/cl/classfile.go b/cl/classfile.go
index a9031c23..085181ea 100644
--- a/cl/classfile.go
+++ b/cl/classfile.go
@@ -20,6 +20,7 @@ import (
 	goast "go/ast"
 	"go/constant"
 	"go/types"
+	"log"
 	"path/filepath"
 	"strings"
 
@@ -31,10 +32,17 @@ import (
 
 // -----------------------------------------------------------------------------
 
-type gmxSettings struct {
-	gameClass  string
-	game       gox.Ref
-	sprite     map[string]gox.Ref
+type gmxClass struct {
+	tname string // class type
+	ext   string
+	proj  *gmxProject
+}
+
+type gmxProject struct {
+	gameClass  string             // <gmtype>.gmx
+	game       gox.Ref            // Game
+	sprite     map[string]gox.Ref // .spx => Sprite
+	sptypes    []string           // <sptype>.spx
 	scheds     []string
 	schedStmts []goast.Stmt // nil or len(scheds) == 2 (delayload)
 	pkgImps    []*gox.PkgRef
@@ -43,7 +51,7 @@ type gmxSettings struct {
 	gameIsPtr  bool
 }
 
-func (p *gmxSettings) getScheds(cb *gox.CodeBuilder) []goast.Stmt {
+func (p *gmxProject) getScheds(cb *gox.CodeBuilder) []goast.Stmt {
 	if p == nil || !p.hasScheds {
 		return nil
 	}
@@ -69,36 +77,49 @@ func ClassNameAndExt(file string) (name, ext string) {
 	return
 }
 
-func newGmx(ctx *pkgCtx, pkg *gox.Package, file string, f *ast.File, conf *Config) *gmxSettings {
+func loadClass(ctx *pkgCtx, pkg *gox.Package, file string, f *ast.File, conf *Config) *gmxProject {
 	tname, ext := ClassNameAndExt(file)
 	gt, ok := conf.LookupClass(ext)
 	if !ok {
 		panic("TODO: class not found")
 	}
-	var name string
-	if f.IsProj {
-		name = tname
-		if name == "main" {
-			name = gt.Class
+	p, ok := ctx.projs[gt.Ext]
+	if !ok {
+		pkgPaths := gt.PkgPaths
+		p = &gmxProject{pkgPaths: pkgPaths}
+		ctx.projs[gt.Ext] = p
+
+		p.pkgImps = make([]*gox.PkgRef, len(pkgPaths))
+		for i, pkgPath := range pkgPaths {
+			p.pkgImps[i] = pkg.Import(pkgPath)
+		}
+		spx := p.pkgImps[0]
+		if gt.Class != "" {
+			p.game, p.gameIsPtr = spxRef(spx, gt.Class)
+		}
+		p.sprite = make(map[string]types.Object)
+		for _, v := range gt.Works {
+			obj, _ := spxRef(spx, v.Class)
+			p.sprite[v.Ext] = obj
+		}
+		if x := getStringConst(spx, "Gop_sched"); x != "" {
+			p.scheds, p.hasScheds = strings.SplitN(x, ",", 2), true
 		}
 	}
-	pkgPaths := gt.PkgPaths
-	p := &gmxSettings{gameClass: name, pkgPaths: pkgPaths}
-	p.pkgImps = make([]*gox.PkgRef, len(pkgPaths))
-	for i, pkgPath := range pkgPaths {
-		p.pkgImps[i] = pkg.Import(pkgPath)
-	}
-	spx := p.pkgImps[0]
-	if gt.Class != "" {
-		p.game, p.gameIsPtr = spxRef(spx, gt.Class)
-	}
-	p.sprite = make(map[string]types.Object)
-	for _, v := range gt.Works {
-		obj, _ := spxRef(spx, v.Class)
-		p.sprite[v.Ext] = obj
+	if f.IsProj {
+		if p.gameClass != "" {
+			panic("TODO: multiple project files found")
+		}
+		if tname == "main" {
+			tname = gt.Class
+		}
+		p.gameClass = tname
+	} else {
+		p.sptypes = append(p.sptypes, tname)
 	}
-	if x := getStringConst(spx, "Gop_sched"); x != "" {
-		p.scheds, p.hasScheds = strings.SplitN(x, ",", 2), true
+	ctx.classes[f] = gmxClass{tname, ext, p}
+	if debugLoad {
+		log.Println("==> InitClass", tname, "isProj:", f.IsProj)
 	}
 	return p
 }
@@ -151,8 +172,8 @@ func getFields(f *ast.File) []ast.Spec {
 }
 
 func setBodyHandler(ctx *blockCtx) {
-	if ctx.isClass { // in a Go+ class file
-		if scheds := ctx.getScheds(ctx.cb); scheds != nil {
+	if proj := ctx.proj; proj != nil { // in a Go+ class file
+		if scheds := proj.getScheds(ctx.cb); scheds != nil {
 			ctx.cb.SetBodyHandler(func(body *goast.BlockStmt, kind int) {
 				idx := 0
 				if len(body.List) == 0 {
@@ -165,13 +186,20 @@ func setBodyHandler(ctx *blockCtx) {
 }
 
 func gmxMainFunc(p *gox.Package, ctx *pkgCtx) bool {
-	if o := p.Types.Scope().Lookup(ctx.gameClass); o != nil && hasMethod(o, "MainEntry") {
-		// new(Game).Main()
-		p.NewFunc(nil, "main", nil, nil, false).BodyStart(p).
-			Val(p.Builtin().Ref("new")).Val(o).Call(1).
-			MemberVal("Main").Call(0).EndStmt().
-			End()
-		return true
+	if len(ctx.projs) == 1 { // only one project file
+		var proj *gmxProject
+		for _, v := range ctx.projs {
+			proj = v
+			break
+		}
+		if o := p.Types.Scope().Lookup(proj.gameClass); o != nil && hasMethod(o, "MainEntry") {
+			// new(Game).Main()
+			p.NewFunc(nil, "main", nil, nil, false).BodyStart(p).
+				Val(p.Builtin().Ref("new")).Val(o).Call(1).
+				MemberVal("Main").Call(0).EndStmt().
+				End()
+			return true
+		}
 	}
 	return false
 }
diff --git a/cl/compile.go b/cl/compile.go
index 567b0f14..4bd21ead 100644
--- a/cl/compile.go
+++ b/cl/compile.go
@@ -336,13 +336,14 @@ func doInitMethods(ld *typeLoader) {
 
 type pkgCtx struct {
 	*nodeInterp
-	*gmxSettings
-	fset  *token.FileSet
-	cpkgs *cpackages.Importer
-	syms  map[string]loader
-	inits []func()
-	tylds []*typeLoader
-	errs  errors.List
+	projs   map[string]*gmxProject // .gmx => project
+	classes map[*ast.File]gmxClass
+	fset    *token.FileSet
+	cpkgs   *cpackages.Importer
+	syms    map[string]loader
+	inits   []func()
+	tylds   []*typeLoader
+	errs    errors.List
 
 	generics map[string]bool // generic type record
 	idents   []*ast.Ident    // toType ident recored
@@ -356,6 +357,7 @@ type pkgImp struct {
 
 type blockCtx struct {
 	*pkgCtx
+	proj       *gmxProject
 	pkg        *gox.Package
 	cb         *gox.CodeBuilder
 	imports    map[string]pkgImp
@@ -470,8 +472,12 @@ func NewPackage(pkgPath string, pkg *ast.Package, conf *Config) (p *gox.Package,
 		fset: fset, files: files, relBaseDir: relBaseDir,
 	}
 	ctx := &pkgCtx{
-		fset: fset,
-		syms: make(map[string]loader), nodeInterp: interp, generics: make(map[string]bool),
+		fset:       fset,
+		nodeInterp: interp,
+		projs:      make(map[string]*gmxProject),
+		classes:    make(map[*ast.File]gmxClass),
+		syms:       make(map[string]loader),
+		generics:   make(map[string]bool),
 	}
 	confGox := &gox.Config{
 		Types:           conf.Types,
@@ -509,18 +515,11 @@ func NewPackage(pkgPath string, pkg *ast.Package, conf *Config) (p *gox.Package,
 	ctx.cpkgs = cpackages.NewImporter(&cpackages.Config{
 		Pkg: p, LookupPub: conf.LookupPub,
 	})
+
 	for file, gmx := range files {
-		if gmx.IsProj {
-			ctx.gmxSettings = newGmx(ctx, p, file, gmx, conf)
-			break
-		}
-	}
-	if ctx.gmxSettings == nil {
-		for file, gmx := range files {
-			if gmx.IsClass && !gmx.IsNormalGox {
-				ctx.gmxSettings = newGmx(ctx, p, file, gmx, conf)
-				break
-			}
+		log.Println("==> File", file, "isClass:", gmx.IsClass, "normalGox:", gmx.IsNormalGox)
+		if gmx.IsClass && !gmx.IsNormalGox {
+			loadClass(ctx, p, file, gmx, conf)
 		}
 	}
 
@@ -538,7 +537,7 @@ func NewPackage(pkgPath string, pkg *ast.Package, conf *Config) (p *gox.Package,
 		preloadGopFile(p, ctx, fpath, f, conf)
 	}
 
-	gopSyms := make(map[string]bool)
+	gopSyms := make(map[string]bool) // TODO: remove this map
 	for name := range ctx.syms {
 		gopSyms[name] = true
 	}
@@ -579,14 +578,14 @@ func NewPackage(pkgPath string, pkg *ast.Package, conf *Config) (p *gox.Package,
 	for _, f := range sfiles {
 		if f.IsProj {
 			loadFile(ctx, f.File)
-			if genMain { // generate main func if need
-				genMain = !gmxMainFunc(p, ctx)
-			}
-			break
 		}
 	}
+	if genMain { // generate main func if need
+		genMain = !gmxMainFunc(p, ctx)
+	}
+
 	for _, f := range sfiles {
-		if !f.IsProj { // only one .gmx file
+		if !f.IsProj {
 			loadFile(ctx, f.File)
 		}
 	}
@@ -708,25 +707,27 @@ func genGoFile(file string, gopFile bool) string {
 }
 
 func preloadGopFile(p *gox.Package, ctx *blockCtx, file string, f *ast.File, conf *Config) {
-	var parent = ctx.pkgCtx
+	var proj *gmxProject
 	var classType string
 	var baseTypeName string
 	var baseType types.Type
 	var spxClass bool
-	switch {
-	case f.IsProj:
-		classType = parent.gameClass
-		o := parent.game
-		baseTypeName, baseType = o.Name(), o.Type()
-		if parent.gameIsPtr {
-			baseType = types.NewPointer(baseType)
-		}
-	case f.IsClass:
-		var classExt string
-		classType, classExt = ClassNameAndExt(file)
-		if parent.gmxSettings != nil {
-			o, ok := parent.sprite[classExt]
-			if ok {
+	var parent = ctx.pkgCtx
+	if f.IsClass {
+		if f.IsNormalGox {
+			classType, _ = ClassNameAndExt(file)
+		} else {
+			c := parent.classes[f]
+			classType, proj = c.tname, c.proj
+			ctx.proj = proj
+			if f.IsProj {
+				o := proj.game
+				baseTypeName, baseType = o.Name(), o.Type()
+				if proj.gameIsPtr {
+					baseType = types.NewPointer(baseType)
+				}
+			} else {
+				o := proj.sprite[c.ext]
 				baseTypeName, baseType, spxClass = o.Name(), o.Type(), true
 			}
 		}
@@ -735,9 +736,9 @@ func preloadGopFile(p *gox.Package, ctx *blockCtx, file string, f *ast.File, con
 		if debugLoad {
 			log.Println("==> Preload type", classType)
 		}
-		if parent.gmxSettings != nil {
-			ctx.lookups = make([]*gox.PkgRef, len(parent.pkgPaths))
-			for i, pkgPath := range parent.pkgPaths {
+		if proj != nil {
+			ctx.lookups = make([]*gox.PkgRef, len(proj.pkgPaths))
+			for i, pkgPath := range proj.pkgPaths {
 				ctx.lookups[i] = p.Import(pkgPath)
 			}
 		}
@@ -758,13 +759,13 @@ func preloadGopFile(p *gox.Package, ctx *blockCtx, file string, f *ast.File, con
 				var flds []*types.Var
 				var tags []string
 				chk := newCheckRedecl()
-				if len(baseTypeName) != 0 {
+				if baseTypeName != "" {
 					flds = append(flds, types.NewField(pos, pkg, baseTypeName, baseType, true))
 					tags = append(tags, "")
 					chk.chkRedecl(ctx, baseTypeName, pos)
 				}
-				if spxClass && parent.gmxSettings != nil && parent.gameClass != "" {
-					typ := toType(ctx, &ast.StarExpr{X: &ast.Ident{Name: parent.gameClass}})
+				if spxClass && proj.gameClass != "" {
+					typ := toType(ctx, &ast.StarExpr{X: &ast.Ident{Name: proj.gameClass}})
 					name := getTypeName(typ)
 					if !chk.chkRedecl(ctx, name, pos) {
 						fld := types.NewField(pos, pkg, name, typ, true)


Test patch:
diff --git a/cl/builtin_test.go b/cl/builtin_test.go
index 5a9cda8c..b1b3298e 100644
--- a/cl/builtin_test.go
+++ b/cl/builtin_test.go
@@ -53,6 +53,26 @@ func TestErrStringLit(t *testing.T) {
 	})
 }
 
+func TestNodeInterp(t *testing.T) {
+	ni := &nodeInterp{}
+	if v := ni.Caller(&ast.Ident{}); v != "the function call" {
+		t.Fatal("TestNodeInterp:", v)
+	}
+}
+
+func TestMarkAutogen(t *testing.T) {
+	old := noMarkAutogen
+	noMarkAutogen = false
+
+	NewPackage("", &ast.Package{Files: map[string]*ast.File{
+		"main.t2gmx": {IsProj: true},
+	}}, &Config{
+		LookupClass: lookupClassErr,
+	})
+
+	noMarkAutogen = old
+}
+
 func TestClassNameAndExt(t *testing.T) {
 	name, ext := ClassNameAndExt("/foo/bar.abc_yap.gox")
 	if name != "bar" || ext != "_yap.gox" {
@@ -216,20 +236,25 @@ func isError(e interface{}, msg string) bool {
 	return false
 }
 
-func TestGmxSettings(t *testing.T) {
+func TestGmxProject(t *testing.T) {
 	pkg := gox.NewPackage("", "foo", goxConf)
-	gmx := newGmx(nil, pkg, "main.t2gmx", &ast.File{IsProj: true}, &Config{
+	ctx := &pkgCtx{
+		projs:   make(map[string]*gmxProject),
+		classes: make(map[*ast.File]gmxClass),
+	}
+	gmx := loadClass(ctx, pkg, "main.t2gmx", &ast.File{IsProj: true}, &Config{
 		LookupClass: lookupClass,
 	})
 	scheds := gmx.getScheds(pkg.CB())
 	if len(scheds) != 2 || scheds[0] == nil || scheds[0] != scheds[1] {
-		t.Fatal("TestGmxSettings failed")
+		t.Fatal("TestGmxProject failed")
 	}
 	gmx.hasScheds = false
 	if gmx.getScheds(nil) != nil {
-		t.Fatal("TestGmxSettings failed: hasScheds?")
+		t.Fatal("TestGmxProject failed: hasScheds?")
 	}
-	_, err := NewPackage("", &ast.Package{Files: map[string]*ast.File{
+
+	/* _, err := NewPackage("", &ast.Package{Files: map[string]*ast.File{
 		"main.t2gmx": {
 			IsProj: true,
 		},
@@ -238,16 +263,28 @@ func TestGmxSettings(t *testing.T) {
 	})
 	if e := err.Error(); e != `github.com/goplus/gop/cl/internal/libc.Game not found` {
 		t.Fatal("newGmx:", e)
-	}
+	} */
 
-	defer func() {
-		if e := recover(); e == nil {
-			t.Fatal("TestGmxSettings failed: no error?")
-		}
+	func() {
+		defer func() {
+			if e := recover(); e != "TODO: class not found" {
+				t.Fatal("TestGmxProject failed:", e)
+			}
+		}()
+		loadClass(nil, pkg, "main.abcx", &ast.File{IsProj: true}, &Config{
+			LookupClass: lookupClass,
+		})
+	}()
+	func() {
+		defer func() {
+			if e := recover(); e != "TODO: multiple project files found" {
+				t.Fatal("TestGmxProject failed:", e)
+			}
+		}()
+		loadClass(ctx, pkg, "main.t2gmx", &ast.File{IsProj: true}, &Config{
+			LookupClass: lookupClass,
+		})
 	}()
-	newGmx(nil, pkg, "main.abcx", &ast.File{IsProj: true}, &Config{
-		LookupClass: lookupClass,
-	})
 }
 
 func TestSpxLookup(t *testing.T) {
diff --git a/cl/compile_spx_test.go b/cl/compile_spx_test.go
index 314fdf2e..652e523c 100644
--- a/cl/compile_spx_test.go
+++ b/cl/compile_spx_test.go
@@ -19,7 +19,6 @@ package cl_test
 import (
 	"bytes"
 	"os"
-	"path"
 	"testing"
 
 	"github.com/goplus/gop/cl"
@@ -54,7 +53,7 @@ func lookupClass(ext string) (c *modfile.Project, ok bool) {
 func spxParserConf() parser.Config {
 	return parser.Config{
 		ClassKind: func(fname string) (isProj bool, ok bool) {
-			ext := path.Ext(fname)
+			ext := modfile.ClassExt(fname)
 			c, ok := lookupClass(ext)
 			if ok {
 				isProj = (c.Ext == ext)
@@ -433,7 +432,9 @@ type Kai struct {
 func (this *Kai) onMsg(msg string) {
 }
 `, "Game.t2gmx", "Kai.t2spx2")
+}
 
+func TestSpx3(t *testing.T) {
 	gopSpxTestEx(t, `
 println("Hi, Sprite")
 `, `