diff --git a/x/format/gopclass_test.go b/x/format/gopclass_test.go new file mode 100644 index 000000000..143461ebe --- /dev/null +++ b/x/format/gopclass_test.go @@ -0,0 +1,341 @@ +/* + * 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 format_test + +import ( + "testing" + + "github.com/goplus/xgo/x/format" +) + +func testClass(t *testing.T, name string, cfg *format.ClassConfig, src, expect string) { + t.Run(name, func(t *testing.T) { + result, err := format.XGoClassSource([]byte(src), cfg, name) + if err != nil { + t.Fatal("format.XGoClassSource failed:", err) + } + if ret := string(result); ret != expect { + t.Fatalf("%s => Expect:\n%s\n=> Got:\n%s\n== end", name, expect, ret) + } + }) +} + +func TestClassSpx(t *testing.T) { + testClass(t, "spx class", &format.ClassConfig{ + PkgPath: "github.com/goplus/spx", + ClassName: "Calf", + Overload: map[string]string{"OnMsg__1": "OnMsg"}, + }, `package main + +import ( + "github.com/goplus/spx" + "fmt" + "log" +) + +type Calf struct { + spx.Sprite + *Game + index int + info string +} +func (this *Calf) Update() { + this.index++ +} +func (this *Calf) Dump() { + log.Println(this.info) +} +func (this *Calf) Main() { + this.OnStart(func() { + this.Say("Hello Go+") + }) +//line Calf.spx:38:1 + this.OnMsg__1("tap", func() { +//line Calf.spx:39:1 + for calfPlay { + spx.Sched() +//line Calf.spx:40:1 + for !(this.KeyPressed(spx.KeySpace) || this.MousePressed()) { + spx.Sched() +//line Calf.spx:41:1 + this.Wait(0.01) + } +//line Calf.spx:43:1 + calfGravity = 0.8 +//line Calf.spx:44:1 + for +//line Calf.spx:44:1 + i := 0; i < 10; +//line Calf.spx:44:1 + i++ { + spx.Sched() +//line Calf.spx:45:1 + this.ChangeYpos(3.5) +//line Calf.spx:46:1 + this.Wait(0.03) + } +//line Calf.spx:48:1 + this.Wait(0.03) + } + }) +} +func (this *Calf) Classfname() string { + return "Calf" +} +`, `import ( + "log" +) + +var ( + index int + info string +) + +func Update() { + index++ +} + +func Dump() { + log.println info +} + +onStart => { + say "Hello Go+" +} + +onMsg "tap", => { + + for calfPlay { + + for !(keyPressed(KeySpace) || mousePressed()) { + + wait 0.01 + } + + calfGravity = 0.8 + + for i := 0; i < 10; i++ { + + changeYpos 3.5 + + wait 0.03 + } + + wait 0.03 + } +} +`) +} + +func TestClassProj(t *testing.T) { + testClass(t, "spx project", &format.ClassConfig{ + PkgPath: "github.com/goplus/spx", + ClassName: "Game", + Project: true, + }, `package main + +import "github.com/goplus/spx" +import "log" + +type Game struct { + spx.Game + MyAircraft MyAircraft + Bullet Bullet +} + +var calfPlay = false +var calfDie = false +var calfGravity = 0.0 +func (this *Game) reset() { + this.userScore = 0 + calfPlay = false + calfDie = false + calfGravity = 0.0 +} +func (this *Game) MainEntry() { + log.Println("MainEntry") +} +func (this *Game) Main() { + spx.Gopt_Game_Main(this, new(Bullet), new(MyAircraft)) +} +func main() { + new(Game).Main() +} +`, `import "log" + +var ( + MyAircraft MyAircraft + Bullet Bullet +) + +var calfPlay = false +var calfDie = false +var calfGravity = 0.0 + +func reset() { + userScore = 0 + calfPlay = false + calfDie = false + calfGravity = 0.0 +} + +log.println "MainEntry" +`) +} + +func TestClassGox(t *testing.T) { + testClass(t, "gox class", &format.ClassConfig{ + ClassName: "Rect", + Comments: true, + }, `package main + +type BaseClass struct { + x int + y int +} +type AggClass struct { +} +type Rect struct { + BaseClass + Width float64 + Height float64 + *AggClass +} + +// Area is call rect area +func (this *Rect) Area() float64 { + return this.Width * this.Height +} +`, `var ( + BaseClass + Width float64 + Height float64 + *AggClass +) + +type BaseClass struct { + x int + y int +} + +type AggClass struct { +} + +// Area is call rect area +func Area() float64 { + return Width * Height +} +`) +} + +func TestClassGopt(t *testing.T) { + testClass(t, "test class", &format.ClassConfig{ + PkgPath: "github.com/goplus/gop/cl/internal/spx", + ClassName: "Game", + Project: true, + Gopt: map[string]string{ + "Gopt_Sprite_Clone__0": "Clone", + "Gopt_Sprite_Clone__1": "Clone", + }, + Overload: map[string]string{"Broadcast__0": "Broadcast"}, + }, `package main + +import "github.com/goplus/gop/cl/internal/spx" + +type Game struct { + *spx.MyGame + Kai Kai +} +func (this *Game) onInit() { + spx.Gopt_Sprite_Clone__0(this.Kai) + this.Broadcast__0("msg1") +} +func (this *Game) MainEntry() { +} +func (this *Game) Main() { + spx.Gopt_MyGame_Main(this) +} +func main() { + new(Game).Main() +} +`, `var Kai Kai + +func onInit() { + Kai.clone + broadcast "msg1" +} + + +`) + testClass(t, "test class", &format.ClassConfig{ + PkgPath: "github.com/goplus/gop/cl/internal/spx", + ClassName: "Kai", + Gopt: map[string]string{ + "Gopt_Sprite_Clone__0": "Clone", + "Gopt_Sprite_Clone__1": "Clone", + }, + Overload: map[string]string{"Broadcast__0": "Broadcast"}, + }, `package main + +import "github.com/goplus/gop/cl/internal/spx" + +type info struct { + x int + y int +} + +type Kai struct { + spx.Sprite + *Game + a int +} + +func (this *Kai) onInit() { + this.a = 1 + spx.Gopt_Sprite_Clone__0(this) + spx.Gopt_Sprite_Clone__1(this, info{1, 2}) + spx.Gopt_Sprite_Clone__1(this, &info{1, 2}) +} +func (this *Kai) onCloned() { + this.Say("Hi") +} +func (this *Kai) Classfname() string { + return "Kai" +} +func (this *Kai) Main() { +} +`, `var a int + +type info struct { + x int + y int +} + +func onInit() { + a = 1 + clone + clone info{1, 2} + clone &info{1, 2} +} + +func onCloned() { + say "Hi" +} + + +`) +} diff --git a/x/format/gopstyle.go b/x/format/gopstyle.go index cbbd5eb4c..f8a91f077 100644 --- a/x/format/gopstyle.go +++ b/x/format/gopstyle.go @@ -83,6 +83,41 @@ func Gopstyle(file *ast.File) { XGoStyle(file) } +type ClassConfig struct { + PkgPath string // Go+ class project pkgpath, empty if normal .gox class. (optional) + ClassName string // project or class name. + Project bool // true means ClassName is project. + Comments bool // true means parse comments. + Gopt map[string]string // Gopt_ function name mapping. (optional) + Overload map[string]string // Overload function name mapping. (optional) +} + +func XGoClassSource(src []byte, cfg *ClassConfig, filename ...string) (ret []byte, err error) { + var fname string + if filename != nil { + fname = filename[0] + } + fset := token.NewFileSet() + mode := parser.AllErrors + if cfg.Comments { + mode |= parser.ParseComments + } + var f *ast.File + if f, err = parser.ParseFile(fset, fname, src, mode); err == nil { + XGoClass(f, cfg) + var buf bytes.Buffer + if err = format.Node(&buf, fset, f); err == nil { + ret = buf.Bytes() + } + } + return +} + +// XGoClass format ast.File to Go+ class +func XGoClass(file *ast.File, cfg *ClassConfig) { + formatClass(file, cfg) +} + func findFuncDecl(decls []ast.Decl, name string) (int, *ast.FuncDecl) { for i, decl := range decls { if fn, ok := decl.(*ast.FuncDecl); ok { @@ -156,8 +191,11 @@ type importCtx struct { } type formatCtx struct { - imports map[string]*importCtx - scope *types.Scope + imports map[string]*importCtx + scope *types.Scope + classCfg *ClassConfig + classPkg string //this class pkg name + funcRecv string //this class func recv } func (ctx *formatCtx) insert(name string) { @@ -223,6 +261,119 @@ func formatFile(file *ast.File) { } } +func formatClass(file *ast.File, cfg *ClassConfig) { + var funcs []*ast.FuncDecl + ctx := &formatCtx{ + imports: make(map[string]*importCtx), + scope: types.NewScope(nil, token.NoPos, token.NoPos, ""), + classCfg: cfg, + classPkg: path.Base(cfg.PkgPath), + } + if file.Name.Name == "main" { + file.NoPkgDecl = true + } + var fnEntry *ast.FuncDecl + var decls []ast.Decl + var imports []ast.Decl + var varSpecs []ast.Spec + for _, decl := range file.Decls { + switch v := decl.(type) { + case *ast.FuncDecl: + if isClassFunc(v, cfg.ClassName) { + v.IsClass = true + switch v.Name.Name { + case "MainEntry": + if cfg.Project { + fnEntry = v + file.ShadowEntry = v + continue + } + case "Main": + if !cfg.Project { + fnEntry = v + file.ShadowEntry = v + continue + } + v.Shadow = true + case "Classfname": + v.Shadow = true + } + } else if v.Name.Name == "main" && cfg.Project { + v.Shadow = true + } + case *ast.GenDecl: + switch v.Tok { + case token.IMPORT: + imports = append(imports, v) + continue + case token.TYPE: + if spec, ok := v.Specs[0].(*ast.TypeSpec); ok && spec.Name.Name == cfg.ClassName { + if st, ok := spec.Type.(*ast.StructType); ok { + for _, fs := range st.Fields.List { + if len(fs.Names) == 0 && cfg.PkgPath != "" { + continue + } + varSpecs = append(varSpecs, &ast.ValueSpec{Names: fs.Names, Type: fs.Type}) + } + continue + } + } + } + } + decls = append(decls, decl) + } + + file.Decls = imports + if len(varSpecs) != 0 { + file.Decls = append(file.Decls, &ast.GenDecl{Tok: token.VAR, Specs: varSpecs}) + } + file.Decls = append(file.Decls, decls...) + if fnEntry != nil { + file.Decls = append(file.Decls, fnEntry) + } + + for _, decl := range file.Decls { + switch v := decl.(type) { + case *ast.FuncDecl: + // delay the process, because package level vars need to be processed first. + funcs = append(funcs, v) + case *ast.GenDecl: + switch v.Tok { + case token.IMPORT: + for _, item := range v.Specs { + var spec = item.(*ast.ImportSpec) + var pkgPath = toString(spec.Path) + var name string + if spec.Name == nil { + name = path.Base(pkgPath) + } else { + name = spec.Name.Name + if name == "." || name == "_" { + continue + } + } + ctx.imports[name] = &importCtx{pkgPath: pkgPath, decl: v, spec: spec} + } + default: + formatGenDecl(ctx, v) + } + } + } + + for _, fn := range funcs { + formatFuncDecl(ctx, fn) + } + for _, imp := range ctx.imports { + if !imp.isUsed { + if len(imp.decl.Specs) == 1 { + file.Decls = deleteDecl(file.Decls, imp.decl) + } else { + imp.decl.Specs = deleteSpec(imp.decl.Specs, imp.spec) + } + } + } +} + func formatGenDecl(ctx *formatCtx, v *ast.GenDecl) { switch v.Tok { case token.VAR, token.CONST: @@ -242,7 +393,36 @@ func formatGenDecl(ctx *formatCtx, v *ast.GenDecl) { } } +func isClassFunc(v *ast.FuncDecl, className string) bool { + if v.Recv != nil && len(v.Recv.List) == 1 { + typ := v.Recv.List[0].Type + if star, ok := typ.(*ast.StarExpr); ok { + typ = star.X + } + if ident, ok := typ.(*ast.Ident); ok && ident.Name == className { + return true + } + } + return false +} + +func funcRecv(v *ast.FuncDecl) *ast.Ident { + if v.Recv != nil && len(v.Recv.List) == 1 && len(v.Recv.List[0].Names) == 1 { + return v.Recv.List[0].Names[0] + } + return nil +} + func formatFuncDecl(ctx *formatCtx, v *ast.FuncDecl) { + if ctx.classCfg != nil && isClassFunc(v, ctx.classCfg.ClassName) { + v.IsClass = true + if recv := funcRecv(v); recv != nil { + ctx.funcRecv = recv.Name + defer func() { + ctx.funcRecv = "" + }() + } + } formatFuncType(ctx, v.Type) formatBlockStmt(ctx, v.Body) } diff --git a/x/format/stmt_expr_or_type.go b/x/format/stmt_expr_or_type.go index eccebacf1..f0283aaac 100644 --- a/x/format/stmt_expr_or_type.go +++ b/x/format/stmt_expr_or_type.go @@ -166,13 +166,13 @@ func formatSliceExpr(ctx *formatCtx, v *ast.SliceExpr) { } func formatCallExpr(ctx *formatCtx, v *ast.CallExpr) { - formatExpr(ctx, v.Fun, &v.Fun) fncallStartingLowerCase(v) for i, arg := range v.Args { if fn, ok := arg.(*ast.FuncLit); ok { funcLitToLambdaExpr(fn, &v.Args[i]) } } + formatExpr(ctx, v.Fun, &v.Fun) formatExprs(ctx, v.Args) } @@ -182,6 +182,10 @@ func formatSelectorExpr(ctx *formatCtx, v *ast.SelectorExpr, ref *ast.Expr) { if _, o := ctx.scope.LookupParent(x.Name, token.NoPos); o != nil { break } + if ctx.classCfg != nil && (x.Name == ctx.funcRecv || x.Name == ctx.classPkg) { + *ref = v.Sel + break + } if imp, ok := ctx.imports[x.Name]; ok { if !fmtToBuiltin(imp, v.Sel, ref) { imp.isUsed = true @@ -202,8 +206,25 @@ func formatBlockStmt(ctx *formatCtx, stmt *ast.BlockStmt) { } } +func isClassSched(ctx *formatCtx, stmt ast.Stmt) bool { + if expr, ok := stmt.(*ast.ExprStmt); ok { + if v, ok := expr.X.(*ast.CallExpr); ok { + if sel, ok := v.Fun.(*ast.SelectorExpr); ok && sel.Sel.Name == "Sched" { + if ident, ok := sel.X.(*ast.Ident); ok && ident.Name == ctx.classPkg { + return true + } + } + } + } + return false +} + func formatStmts(ctx *formatCtx, stmts []ast.Stmt) { - for _, stmt := range stmts { + for i, stmt := range stmts { + if ctx.classCfg != nil && isClassSched(ctx, stmt) { + stmts[i] = &ast.EmptyStmt{} + continue + } formatStmt(ctx, stmt) } } @@ -262,6 +283,23 @@ func formatStmt(ctx *formatCtx, stmt ast.Stmt) { func formatExprStmt(ctx *formatCtx, v *ast.ExprStmt) { switch x := v.X.(type) { case *ast.CallExpr: + if ctx.classCfg != nil { + if sel, ok := x.Fun.(*ast.SelectorExpr); ok { + if name, ok := ctx.classCfg.Overload[sel.Sel.Name]; ok { + sel.Sel.Name = name + } else if ident, ok := sel.X.(*ast.Ident); ok && ident.Name == ctx.classPkg { + if name, ok := ctx.classCfg.Gopt[sel.Sel.Name]; ok { + if len(x.Args) > 0 { + x.Fun = &ast.SelectorExpr{ + X: x.Args[0], + Sel: ast.NewIdent(name), + } + x.Args = x.Args[1:] + } + } + } + } + } commandStyleFirst(x) } formatExpr(ctx, v.X, &v.X)