From 41dff3870a89652fb2679d40bca6fcf8ae444240 Mon Sep 17 00:00:00 2001 From: Kuroda Kayn Date: Tue, 23 Jun 2026 20:21:54 +0800 Subject: [PATCH 01/15] feat(cl): compile explicit static value declarations --- cl/compile.go | 162 ++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 136 insertions(+), 26 deletions(-) diff --git a/cl/compile.go b/cl/compile.go index 9228707b3..58cc2d4b8 100644 --- a/cl/compile.go +++ b/cl/compile.go @@ -26,6 +26,8 @@ import ( "sort" "strconv" "strings" + "unicode" + "unicode/utf8" gotoken "go/token" @@ -293,17 +295,18 @@ func (p *baseLoader) node() ast.Node { } type constNamePos struct { - name *ast.Ident - newName *ast.Ident - fn func() - specs *[]ast.Spec - ispec int - idx int + name *ast.Ident + finalName string + newName *ast.Ident + fn func() + specs *[]ast.Spec + ispec int + idx int } -func newConstNamePos(specs *[]ast.Spec, ispec, idx int, fn func()) *constNamePos { +func newConstNamePos(specs *[]ast.Spec, ispec, idx int, finalName string, fn func()) *constNamePos { name := (*specs)[ispec].(*ast.ValueSpec).Names[idx] - return &constNamePos{specs: specs, ispec: ispec, name: name, idx: idx, fn: fn} + return &constNamePos{specs: specs, ispec: ispec, name: name, finalName: finalName, idx: idx, fn: fn} } func (p *constNamePos) rename(newName string) { @@ -363,7 +366,7 @@ func (p *constNameLoader) load() { p.cdecl.New(func(cb *gogen.CodeBuilder) int { compileConstVal(cb, val) return 1 - }, 0, name.NamePos, nil, name.Name) + }, 0, name.NamePos, nil, cnp.finalName) } else if constant.Compare(val, gotoken.NEQ, oldVal) { p.ctx.handleErrorf(name.Pos(), name.End(), "enum constant %q already declared with value %v; cannot redeclare with value %v", name, oldVal, val) @@ -595,6 +598,11 @@ func (p *pkgCtx) loadSymbol(name string) bool { return false } +func (p *pkgCtx) hasSymbol(name string) bool { + _, ok := p.syms[name] + return ok +} + func (p *pkgCtx) handleRecover(e any, src ast.Node) { err := p.recoverErr(e, src) p.handleErr(err) @@ -879,6 +887,7 @@ func initXGoPkg(ctx *pkgCtx, pkg *gogen.Package, gopSyms map[string]bool) { } func loadFile(ctx *pkgCtx, f *ast.File) { + bctx := &blockCtx{pkgCtx: ctx} for _, decl := range f.Decls { switch d := decl.(type) { case *ast.GenDecl: @@ -889,8 +898,8 @@ func loadFile(ctx *pkgCtx, f *ast.File) { } case token.CONST, token.VAR: for _, spec := range d.Specs { - for _, name := range spec.(*ast.ValueSpec).Names { - ctx.loadSymbol(name.Name) + for _, name := range valueSpecNames(bctx, spec.(*ast.ValueSpec)) { + ctx.loadSymbol(name) } } } @@ -1223,7 +1232,7 @@ func preloadFile(p *gogen.Package, ctx *blockCtx, f *ast.File, goFile string, ge loadConstSpecs(ctx, c, specs, typ) for _, s := range specs { v := s.(*ast.ValueSpec) - removeNames(syms, v.Names) + removeNamedSymbols(syms, valueSpecNames(ctx, v)) } } } @@ -1231,9 +1240,10 @@ func preloadFile(p *gogen.Package, ctx *blockCtx, f *ast.File, goFile string, ge // load enum type even nobody use it ctx.inits = append(ctx.inits, loadConst) } - for i, name := range vSpec.Names { - at := newConstNamePos(&specs, ispec, i, loadConst) - initLoader(ctx, at, name.Name, loadConst, true) + names := valueSpecNames(ctx, vSpec) + for i := range vSpec.Names { + at := newConstNamePos(&specs, ispec, i, names[i], loadConst) + initLoader(ctx, at, names[i], loadConst, true) } } } @@ -1346,11 +1356,12 @@ func preloadFile(p *gogen.Package, ctx *blockCtx, f *ast.File, goFile string, ge old, _ := p.SetCurFile(goFile, true) defer p.RestoreCurFile(old) loadVars(ctx, v, d.Doc, true) - removeNames(syms, v.Names) + removeNamedSymbols(syms, valueSpecNames(ctx, v)) } } - for _, name := range vSpec.Names { - initLoader(ctx, name, name.Name, loadVar, true) + names := valueSpecNames(ctx, vSpec) + for i, name := range vSpec.Names { + initLoader(ctx, name, names[i], loadVar, true) } } default: @@ -1531,6 +1542,15 @@ func staticMethod(tname, name string) string { return "XGos" + sep + tname + sep + name } +func staticMember(tname, name string) string { + return staticMethod(tname, name) +} + +func exportStaticMemberName(name string) string { + r, size := utf8.DecodeRuneInString(name) + return string(unicode.ToUpper(r)) + name[size:] +} + func stringLit(val string) *ast.BasicLit { return &ast.BasicLit{Kind: token.STRING, Value: strconv.Quote(val)} } @@ -1792,14 +1812,16 @@ func loadConstSpecs(ctx *blockCtx, cdecl *gogen.ConstDefs, specs []ast.Spec, enu } func loadConsts(ctx *blockCtx, cdecl *gogen.ConstDefs, v *ast.ValueSpec, iotav int, typ types.Type) { - vNames := v.Names - names := makeNames(vNames) + if !validateStaticValueSpec(ctx, v) { + return + } + names := valueSpecNames(ctx, v) if v.Values == nil { if debugLoad { log.Println("==> Load const", names) } cdecl.Next(iotav, v.Pos(), names...) - defNames(ctx, vNames, nil) + defValueSpecNames(ctx, v, nil) return } isEnum := typ != nil @@ -1831,15 +1853,18 @@ func loadConsts(ctx *blockCtx, cdecl *gogen.ConstDefs, v *ast.ValueSpec, iotav i return len(v.Values) } cdecl.New(fn, iotav, v.Pos(), typ, names...) - defNames(ctx, v.Names, nil) + defValueSpecNames(ctx, v, nil) } func loadVars(ctx *blockCtx, v *ast.ValueSpec, doc *ast.CommentGroup, global bool) { + if !validateStaticValueSpec(ctx, v) { + return + } var typ types.Type if v.Type != nil { typ = toType(ctx, v.Type) } - names := makeNames(v.Names) + names := valueSpecNames(ctx, v) if debugLoad { log.Println("==> Load var", typ, names) } @@ -1852,7 +1877,7 @@ func loadVars(ctx *blockCtx, v *ast.ValueSpec, doc *ast.CommentGroup, global boo varDefs := ctx.pkg.NewVarDefs(scope).SetComments(doc) initExpr := makeInitExpr(ctx, v, typ, names) varDefs.NewAndInit(initExpr, v.Names[0].Pos(), typ, names...) - defNames(ctx, v.Names, scope) + defValueSpecNames(ctx, v, scope) } func makeInitExpr(ctx *blockCtx, v *ast.ValueSpec, typ types.Type, names []string) gogen.F { @@ -1904,6 +1929,22 @@ func defNames(ctx *blockCtx, names []*ast.Ident, scope *types.Scope) { } } +func defValueSpecNames(ctx *blockCtx, v *ast.ValueSpec, scope *types.Scope) { + if rec := ctx.recorder(); rec != nil { + if scope == nil { + scope = ctx.cb.Scope() + } + names := valueSpecNames(ctx, v) + for i, ident := range v.Names { + if ident.Name != "_" { + if o := scope.Lookup(names[i]); o != nil { + rec.Def(ident, o) + } + } + } + } +} + func makeNames(vals []*ast.Ident) []string { names := make([]string, len(vals)) for i, v := range vals { @@ -1912,9 +1953,78 @@ func makeNames(vals []*ast.Ident) []string { return names } -func removeNames(syms map[string]loader, names []*ast.Ident) { +func valueSpecNames(ctx *blockCtx, v *ast.ValueSpec) []string { + names := make([]string, len(v.Names)) + for i, name := range v.Names { + names[i] = valueSpecName(ctx, name.Name) + } + return names +} + +func valueSpecName(ctx *blockCtx, name string) string { + if tname, mname, ok := staticValueName(ctx, name); ok { + return staticMember(tname, exportStaticMemberName(mname)) + } + return name +} + +func staticValueName(ctx *blockCtx, name string) (tname, mname string, ok bool) { + tname, mname, ok = strings.Cut(name, ".") + if !ok || tname == "" || mname == "" { + return "", "", false + } + return tname, mname, true +} + +func validateStaticValueSpec(ctx *blockCtx, v *ast.ValueSpec) bool { + ok := true + for _, name := range v.Names { + tname, isStatic := explicitStaticReceiverName(name.Name) + if !isStatic { + continue + } + if lookupPackageTypeName(ctx, tname) != nil { + continue + } + if obj := ctx.pkg.Types.Scope().Lookup(tname); obj != nil { + ctx.handleErrorf(name.Pos(), name.End(), "%s is not a type", tname) + } else { + ctx.handleErrorf(name.Pos(), name.End(), "undefined: %s", tname) + } + ok = false + } + return ok +} + +func explicitStaticReceiverName(name string) (tname string, ok bool) { + if strings.HasPrefix(name, ".") { + return "", false + } + tname, mname, ok := strings.Cut(name, ".") + return tname, ok && tname != "" && mname != "" +} + +func lookupPackageTypeName(ctx *blockCtx, name string) *types.TypeName { + scope := ctx.pkg.Types.Scope() + if obj := scope.Lookup(name); obj != nil { + if t, ok := obj.(*types.TypeName); ok { + return t + } + return nil + } + if ctx.loadSymbol(name) { + if obj := scope.Lookup(name); obj != nil { + if t, ok := obj.(*types.TypeName); ok { + return t + } + } + } + return nil +} + +func removeNamedSymbols(syms map[string]loader, names []string) { for _, name := range names { - delete(syms, name.Name) + delete(syms, name) } } From 8f1714501901d0f17709a6388f1f196fe83c4939 Mon Sep 17 00:00:00 2001 From: Kuroda Kayn Date: Tue, 23 Jun 2026 20:22:14 +0800 Subject: [PATCH 02/15] feat(cl): compile classfile static values --- cl/compile.go | 92 ++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 77 insertions(+), 15 deletions(-) diff --git a/cl/compile.go b/cl/compile.go index 58cc2d4b8..2976f788d 100644 --- a/cl/compile.go +++ b/cl/compile.go @@ -463,17 +463,18 @@ func doInitMethods(ld *typeLoader) { type pkgCtx struct { *nodeInterp - nproj int // number of non-test projects - projs map[string]*classProject // project extension => project - classes map[*ast.File]*classFile - overpos map[string]token.Pos // overload => pos - fset *token.FileSet - syms map[string]loader - consts map[string]*constNameLoader - lbinames []any // names that should load before initXGoPkg (can be string/func or *ast.Ident/type) - inits []func() - tylds []*typeLoader - errs errors.List + nproj int // number of non-test projects + projs map[string]*classProject // project extension => project + classes map[*ast.File]*classFile + classTypes map[*ast.File]string // ast file => generated class type + overpos map[string]token.Pos // overload => pos + fset *token.FileSet + syms map[string]loader + consts map[string]*constNameLoader + lbinames []any // names that should load before initXGoPkg (can be string/func or *ast.Ident/type) + inits []func() + tylds []*typeLoader + errs errors.List generics map[string]bool // generic type record idents []*ast.Ident // toType ident recored @@ -711,6 +712,7 @@ func NewPackage(pkgPath string, pkg *ast.Package, conf *Config) (p *gogen.Packag nodeInterp: interp, projs: make(map[string]*classProject), classes: make(map[*ast.File]*classFile), + classTypes: make(map[*ast.File]string), overpos: make(map[string]token.Pos), syms: make(map[string]loader), generics: make(map[string]bool), @@ -888,6 +890,11 @@ func initXGoPkg(ctx *pkgCtx, pkg *gogen.Package, gopSyms map[string]bool) { func loadFile(ctx *pkgCtx, f *ast.File) { bctx := &blockCtx{pkgCtx: ctx} + if classType := ctx.classTypes[f]; classType != "" { + bctx.classRecv = &ast.FieldList{List: []*ast.Field{{ + Type: &ast.StarExpr{X: ast.NewIdent(classType)}, + }}} + } for _, decl := range f.Decls { switch d := decl.(type) { case *ast.GenDecl: @@ -981,6 +988,7 @@ func preloadXGoFile(p *gogen.Package, ctx *blockCtx, file string, f *ast.File, c if debugLoad { log.Println("==> Preload type", classType) } + parent.classTypes[f] = classType if proj != nil { ctx.lookups = make([]gogen.PkgRef, len(proj.pkgPaths)) for i, pkgPath := range proj.pkgPaths { @@ -1060,7 +1068,10 @@ func preloadXGoFile(p *gogen.Package, ctx *blockCtx, file string, f *ast.File, c var pos token.Pos var names []string var fldType types.Type - spec = v.(*ast.ValueSpec) + spec = valueSpecSubset(ctx, v.(*ast.ValueSpec), false) + if spec == nil { + continue + } if spec.Type != nil { fldType = toType(ctx, spec.Type) } @@ -1342,11 +1353,14 @@ func preloadFile(p *gogen.Package, ctx *blockCtx, f *ast.File, goFile string, ge case token.CONST: preloadConst(d.Specs, nil) case token.VAR: - if d == classDecl { // skip class fields - continue - } for _, spec := range d.Specs { vSpec := spec.(*ast.ValueSpec) + if d == classDecl { + vSpec = valueSpecSubset(ctx, vSpec, true) + if vSpec == nil { + continue + } + } if debugLoad { log.Println("==> Preload var", vSpec.Names) } @@ -1969,6 +1983,9 @@ func valueSpecName(ctx *blockCtx, name string) string { } func staticValueName(ctx *blockCtx, name string) (tname, mname string, ok bool) { + if strings.HasPrefix(name, ".") { + return classRecvName(ctx), name[1:], name != "." + } tname, mname, ok = strings.Cut(name, ".") if !ok || tname == "" || mname == "" { return "", "", false @@ -2022,6 +2039,51 @@ func lookupPackageTypeName(ctx *blockCtx, name string) *types.TypeName { return nil } +func classRecvName(ctx *blockCtx) string { + t := ctx.classRecv.List[0].Type.(*ast.StarExpr) + return t.X.(*ast.Ident).Name +} + +func valueSpecSubset(ctx *blockCtx, v *ast.ValueSpec, wantStatic bool) *ast.ValueSpec { + if len(v.Names) == 0 { + if wantStatic { + return nil + } + return v + } + idxs := make([]int, 0, len(v.Names)) + for i, name := range v.Names { + _, _, ok := staticValueName(ctx, name.Name) + if ok == wantStatic { + idxs = append(idxs, i) + } + } + if len(idxs) == 0 { + return nil + } + if len(idxs) == len(v.Names) { + return v + } + spec := *v + spec.Names = make([]*ast.Ident, len(idxs)) + for i, idx := range idxs { + spec.Names[i] = v.Names[idx] + } + spec.HasStatic = wantStatic + if len(v.Values) == len(v.Names) { + spec.Values = make([]ast.Expr, len(idxs)) + for i, idx := range idxs { + spec.Values[i] = v.Values[idx] + } + } else if len(v.Values) != 0 { + if wantStatic { + ctx.handleErrorf(v.Pos(), v.End(), "cannot split static and non-static names with a shared initializer") + } + return nil + } + return &spec +} + func removeNamedSymbols(syms map[string]loader, names []string) { for _, name := range names { delete(syms, name) From d9c050ec325be2ef19277f13b099b2feb9459e20 Mon Sep 17 00:00:00 2001 From: Kuroda Kayn Date: Tue, 23 Jun 2026 20:22:37 +0800 Subject: [PATCH 03/15] feat(cl): resolve static value member references --- cl/expr.go | 100 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/cl/expr.go b/cl/expr.go index 78d76207e..d44296497 100644 --- a/cl/expr.go +++ b/cl/expr.go @@ -117,6 +117,9 @@ func compileIdent(ctx *blockCtx, lhs int, ident *ast.Ident, flags int) (pkg goge tryMember(cb, lhs, recv, ident, flags) { return } + if tryStaticMemberIdent(ctx, lhs, ident, flags) { + return + } } // global object @@ -188,6 +191,97 @@ find: return } +func tryStaticMemberIdent(ctx *blockCtx, lhs int, ident *ast.Ident, flags int) bool { + if (flags & clIdentSelectorExpr) != 0 { + return false + } + for _, tname := range staticMemberRecvNames(ctx) { + if (flags & clIdentLHS) != 0 { + if tryStaticMemberRef(ctx, ident, nil, tname, ident.Name, ident) { + return true + } + } else if tryStaticMemberName(ctx, lhs, ident, nil, tname, ident.Name) { + return true + } + } + return false +} + +func canUseStaticMemberSelector(ctx *blockCtx, x *ast.Ident) bool { + name := x.Name + scope := ctx.pkg.Types.Scope() + if at, o := ctx.cb.Scope().LookupParent(name, token.NoPos); o != nil { + if at != scope { + return false + } + _, ok := o.(*types.TypeName) + return ok + } + return lookupPackageTypeName(ctx, name) != nil +} + +func staticMemberRecvNames(ctx *blockCtx) []string { + var names []string + if name := classRecvName(ctx); name != "" { + names = append(names, name) + } + if ctx.proj != nil { + if name := ctx.proj.getGameClass(ctx.pkgCtx); name != "" { + for _, old := range names { + if old == name { + return names + } + } + names = append(names, name) + } + } + return names +} + +func tryStaticMemberName(ctx *blockCtx, lhs int, ident, recv *ast.Ident, tname, name string) bool { + sname := staticMember(tname, exportStaticMemberName(name)) + scope := ctx.pkg.Types.Scope() + o := scope.Lookup(sname) + if o == nil && ctx.hasSymbol(sname) && ctx.loadSymbol(sname) { + o = scope.Lookup(sname) + } + if o == nil { + return false + } + ctx.cb.Val(o, ident) + if rec := ctx.recorder(); rec != nil { + recordStaticMemberUse(rec, ctx, ident, recv, tname, o) + } + return true +} + +func tryStaticMemberRef(ctx *blockCtx, ident, recv *ast.Ident, tname, name string, src ast.Node) bool { + sname := staticMember(tname, exportStaticMemberName(name)) + scope := ctx.pkg.Types.Scope() + o := scope.Lookup(sname) + if o == nil && ctx.hasSymbol(sname) && ctx.loadSymbol(sname) { + o = scope.Lookup(sname) + } + if o == nil { + return false + } + ctx.cb.VarRef(o, src) + if rec := ctx.recorder(); rec != nil { + recordStaticMemberUse(rec, ctx, ident, recv, tname, o) + } + return true +} + +func recordStaticMemberUse(rec *goxRecorder, ctx *blockCtx, ident, recv *ast.Ident, tname string, obj types.Object) { + rec.recordIdent(ident, obj) + if recv == nil { + return + } + if ro := ctx.pkg.Types.Scope().Lookup(tname); ro != nil { + rec.Use(recv, ro) + } +} + func tryMember(cb *gogen.CodeBuilder, lhs int, recv types.Object, ident *ast.Ident, flags int) bool { cb.Val(recv) chkFlag := flags @@ -509,6 +603,9 @@ func compileSliceExpr(ctx *blockCtx, v *ast.SliceExpr) { // x[i:j:k] func compileSelectorExprLHS(ctx *blockCtx, v *ast.SelectorExpr) { switch x := v.X.(type) { case *ast.Ident: + if canUseStaticMemberSelector(ctx, x) && tryStaticMemberRef(ctx, v.Sel, x, x.Name, v.Sel.Name, v) { + return + } if at, kind := compileIdent(ctx, 1, x, clIdentLHS|clIdentSelectorExpr); kind != objNormal { ctx.cb.VarRef(at.Ref(v.Sel.Name)) return @@ -625,6 +722,9 @@ func convMapToNodeSet(cb *gogen.CodeBuilder) { func compileSelectorExpr(ctx *blockCtx, lhs int, v *ast.SelectorExpr, flags int) { switch x := v.X.(type) { case *ast.Ident: + if (flags&clIdentLHS) == 0 && canUseStaticMemberSelector(ctx, x) && tryStaticMemberName(ctx, lhs, v.Sel, x, x.Name, v.Sel.Name) { + return + } if at, kind := compileIdent(ctx, 1, x, flags|clIdentCanAutoCall|clIdentSelectorExpr); kind != objNormal { if compilePkgRef(ctx, 1, at, v.Sel, flags, kind) { return From 96e5f4970bd2442b0bf8c2006fc321e82c8bf253 Mon Sep 17 00:00:00 2001 From: Kuroda Kayn Date: Tue, 23 Jun 2026 20:23:35 +0800 Subject: [PATCH 04/15] test(cl): cover static member recorder uses --- cl/static_member_recorder_test.go | 100 ++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 cl/static_member_recorder_test.go diff --git a/cl/static_member_recorder_test.go b/cl/static_member_recorder_test.go new file mode 100644 index 000000000..2c6941c9f --- /dev/null +++ b/cl/static_member_recorder_test.go @@ -0,0 +1,100 @@ +//go:build !genjs + +/* + * Copyright (c) 2026 The XGo Authors (xgo.dev). 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 cl_test + +import ( + "go/types" + "testing" + + "github.com/goplus/xgo/ast" + "github.com/goplus/xgo/cl/cltest" + "github.com/goplus/xgo/parser/fsx/memfs" + "github.com/goplus/xgo/token" +) + +type staticMemberUse struct { + ident string + obj string + line int +} + +type staticMemberRecorder struct { + fset *token.FileSet + uses []staticMemberUse +} + +func (rec *staticMemberRecorder) Type(ast.Expr, types.TypeAndValue) {} + +func (rec *staticMemberRecorder) Instantiate(*ast.Ident, types.Instance) {} + +func (rec *staticMemberRecorder) Def(*ast.Ident, types.Object) {} + +func (rec *staticMemberRecorder) Use(id *ast.Ident, obj types.Object) { + if obj == nil { + return + } + rec.uses = append(rec.uses, staticMemberUse{ + ident: id.Name, + obj: obj.Name(), + line: rec.fset.Position(id.Pos()).Line, + }) +} + +func (rec *staticMemberRecorder) Implicit(ast.Node, types.Object) {} + +func (rec *staticMemberRecorder) Select(*ast.SelectorExpr, *types.Selection) {} + +func (rec *staticMemberRecorder) Scope(ast.Node, *types.Scope) {} + +func (rec *staticMemberRecorder) hasUse(ident, obj string, line int) bool { + for _, use := range rec.uses { + if use.ident == ident && use.obj == obj && use.line == line { + return true + } + } + return false +} + +func TestStaticMemberSelectorRecorderUsesReceiver(t *testing.T) { + rec := &staticMemberRecorder{fset: cltest.Conf.Fset} + conf := *cltest.Conf + conf.Recorder = rec + + fs := memfs.SingleFile("/foo", "bar.xgo", ` +type foo int + +const foo.name = "xgo" +var foo.count int = 100 + +a := foo.name +foo.count++ +`) + cltest.DoFS(t, &conf, fs, "/foo", nil, "main", nil) + + for _, want := range []staticMemberUse{ + {ident: "foo", obj: "foo", line: 7}, + {ident: "name", obj: "XGos_foo_Name", line: 7}, + {ident: "foo", obj: "foo", line: 8}, + {ident: "count", obj: "XGos_foo_Count", line: 8}, + } { + if !rec.hasUse(want.ident, want.obj, want.line) { + t.Fatalf("missing recorder use ident=%q obj=%q line=%d in %#v", want.ident, want.obj, want.line, rec.uses) + } + } +} From 2da805b385af32b1ab591a289cd2d3af8e28dfde Mon Sep 17 00:00:00 2001 From: Kuroda Kayn Date: Tue, 23 Jun 2026 20:34:04 +0800 Subject: [PATCH 05/15] fix(cl): cache static member receivers --- cl/compile.go | 5 +++++ cl/expr.go | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/cl/compile.go b/cl/compile.go index 2976f788d..46b240934 100644 --- a/cl/compile.go +++ b/cl/compile.go @@ -510,6 +510,9 @@ type blockCtx struct { fileScope *types.Scope // available when isXGoFile rec *goxRecorder + staticMemberRecvs []string + staticMemberRecvsInited bool + fileLine bool isClass bool isXgoFile bool // is XGo file or not @@ -1557,6 +1560,8 @@ func staticMethod(tname, name string) string { } func staticMember(tname, name string) string { + // Static values share the static-method namespace so each type has one + // static member namespace for both methods and values. return staticMethod(tname, name) } diff --git a/cl/expr.go b/cl/expr.go index d44296497..20a4baedb 100644 --- a/cl/expr.go +++ b/cl/expr.go @@ -221,6 +221,10 @@ func canUseStaticMemberSelector(ctx *blockCtx, x *ast.Ident) bool { } func staticMemberRecvNames(ctx *blockCtx) []string { + if ctx.staticMemberRecvsInited { + return ctx.staticMemberRecvs + } + ctx.staticMemberRecvsInited = true var names []string if name := classRecvName(ctx); name != "" { names = append(names, name) @@ -235,6 +239,7 @@ func staticMemberRecvNames(ctx *blockCtx) []string { names = append(names, name) } } + ctx.staticMemberRecvs = names return names } From c8df54a2f02d8eb0038fcbcd29ab2053212a7c46 Mon Sep 17 00:00:00 2001 From: Kuroda Kayn Date: Tue, 23 Jun 2026 22:08:23 +0800 Subject: [PATCH 06/15] test(cl): add static member basic testxgo case --- cl/_testxgo/static-member-basic/in.xgo | 7 +++++++ cl/_testxgo/static-member-basic/out.go | 12 ++++++++++++ 2 files changed, 19 insertions(+) create mode 100644 cl/_testxgo/static-member-basic/in.xgo create mode 100644 cl/_testxgo/static-member-basic/out.go diff --git a/cl/_testxgo/static-member-basic/in.xgo b/cl/_testxgo/static-member-basic/in.xgo new file mode 100644 index 000000000..391952022 --- /dev/null +++ b/cl/_testxgo/static-member-basic/in.xgo @@ -0,0 +1,7 @@ +type foo int + +const foo.name = "xgo" +var foo.count int = 100 + +a := foo.name +foo.count++ diff --git a/cl/_testxgo/static-member-basic/out.go b/cl/_testxgo/static-member-basic/out.go new file mode 100644 index 000000000..2185775e3 --- /dev/null +++ b/cl/_testxgo/static-member-basic/out.go @@ -0,0 +1,12 @@ +package main + +type foo int + +const XGos_foo_Name = "xgo" + +var XGos_foo_Count int = 100 + +func main() { + a := XGos_foo_Name + XGos_foo_Count++ +} From b5f4a9e98e6092ab3522f4192b3407474bb7dfa6 Mon Sep 17 00:00:00 2001 From: Kuroda Kayn Date: Tue, 23 Jun 2026 22:09:17 +0800 Subject: [PATCH 07/15] test(cl): add static member forward ref testxgo case --- cl/_testxgo/static-member-forward-ref/in.xgo | 12 ++++++++++++ cl/_testxgo/static-member-forward-ref/out.go | 14 ++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 cl/_testxgo/static-member-forward-ref/in.xgo create mode 100644 cl/_testxgo/static-member-forward-ref/out.go diff --git a/cl/_testxgo/static-member-forward-ref/in.xgo b/cl/_testxgo/static-member-forward-ref/in.xgo new file mode 100644 index 000000000..31f71c921 --- /dev/null +++ b/cl/_testxgo/static-member-forward-ref/in.xgo @@ -0,0 +1,12 @@ +type foo int + +func Name() string { + return foo.name +} + +func Inc() { + foo.count++ +} + +const foo.name = "xgo" +var foo.count int = 100 diff --git a/cl/_testxgo/static-member-forward-ref/out.go b/cl/_testxgo/static-member-forward-ref/out.go new file mode 100644 index 000000000..7c20b54b4 --- /dev/null +++ b/cl/_testxgo/static-member-forward-ref/out.go @@ -0,0 +1,14 @@ +package main + +type foo int + +const XGos_foo_Name = "xgo" + +func Name() string { + return XGos_foo_Name +} +func Inc() { + XGos_foo_Count++ +} + +var XGos_foo_Count int = 100 From ad9e72c21cfa0d979a53857ffb41f16a1ee935e5 Mon Sep 17 00:00:00 2001 From: Kuroda Kayn Date: Tue, 23 Jun 2026 22:10:29 +0800 Subject: [PATCH 08/15] test(cl): add static member receiver order testxgo case --- cl/_testxgo/static-member-receiver-after/in.xgo | 7 +++++++ cl/_testxgo/static-member-receiver-after/out.go | 12 ++++++++++++ 2 files changed, 19 insertions(+) create mode 100644 cl/_testxgo/static-member-receiver-after/in.xgo create mode 100644 cl/_testxgo/static-member-receiver-after/out.go diff --git a/cl/_testxgo/static-member-receiver-after/in.xgo b/cl/_testxgo/static-member-receiver-after/in.xgo new file mode 100644 index 000000000..36b565edb --- /dev/null +++ b/cl/_testxgo/static-member-receiver-after/in.xgo @@ -0,0 +1,7 @@ +const foo.name = "xgo" +var foo.count int = 100 + +type foo int + +a := foo.name +foo.count++ diff --git a/cl/_testxgo/static-member-receiver-after/out.go b/cl/_testxgo/static-member-receiver-after/out.go new file mode 100644 index 000000000..39651a9cf --- /dev/null +++ b/cl/_testxgo/static-member-receiver-after/out.go @@ -0,0 +1,12 @@ +package main + +const XGos_foo_Name = "xgo" + +type foo int + +var XGos_foo_Count int = 100 + +func main() { + a := XGos_foo_Name + XGos_foo_Count++ +} From 5697939949b54a4080bb5ad932c15de3b2faf1f9 Mon Sep 17 00:00:00 2001 From: Kuroda Kayn Date: Tue, 23 Jun 2026 22:11:27 +0800 Subject: [PATCH 09/15] test(cl): add static member unicode testxgo case --- cl/_testxgo/static-member-unicode/in.xgo | 5 +++++ cl/_testxgo/static-member-unicode/out.go | 9 +++++++++ 2 files changed, 14 insertions(+) create mode 100644 cl/_testxgo/static-member-unicode/in.xgo create mode 100644 cl/_testxgo/static-member-unicode/out.go diff --git a/cl/_testxgo/static-member-unicode/in.xgo b/cl/_testxgo/static-member-unicode/in.xgo new file mode 100644 index 000000000..5a0e2a7c0 --- /dev/null +++ b/cl/_testxgo/static-member-unicode/in.xgo @@ -0,0 +1,5 @@ +type foo int + +const foo.π = 3 + +a := foo.π diff --git a/cl/_testxgo/static-member-unicode/out.go b/cl/_testxgo/static-member-unicode/out.go new file mode 100644 index 000000000..a6351e77b --- /dev/null +++ b/cl/_testxgo/static-member-unicode/out.go @@ -0,0 +1,9 @@ +package main + +type foo int + +const XGos_foo_Π = 3 + +func main() { + a := XGos_foo_Π +} From be1a97041c24af4a7375fd77695e02edcb4d245f Mon Sep 17 00:00:00 2001 From: Kuroda Kayn Date: Tue, 23 Jun 2026 22:12:35 +0800 Subject: [PATCH 10/15] test(cl): add static member selector shadow testxgo case --- cl/_testxgo/static-member-selector-shadow/in.xgo | 11 +++++++++++ cl/_testxgo/static-member-selector-shadow/out.go | 16 ++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 cl/_testxgo/static-member-selector-shadow/in.xgo create mode 100644 cl/_testxgo/static-member-selector-shadow/out.go diff --git a/cl/_testxgo/static-member-selector-shadow/in.xgo b/cl/_testxgo/static-member-selector-shadow/in.xgo new file mode 100644 index 000000000..d4acd6f70 --- /dev/null +++ b/cl/_testxgo/static-member-selector-shadow/in.xgo @@ -0,0 +1,11 @@ +type foo struct { + name string + count int +} + +const foo.name = "static" +var foo.count int = 100 + +foo := foo{name: "local"} +a := foo.name +foo.count++ diff --git a/cl/_testxgo/static-member-selector-shadow/out.go b/cl/_testxgo/static-member-selector-shadow/out.go new file mode 100644 index 000000000..7a6773348 --- /dev/null +++ b/cl/_testxgo/static-member-selector-shadow/out.go @@ -0,0 +1,16 @@ +package main + +type foo struct { + name string + count int +} + +const XGos_foo_Name = "static" + +var XGos_foo_Count int = 100 + +func main() { + foo := foo{name: "local"} + a := foo.name + foo.count++ +} From 6694fa04909644900103e351b28f42991021b129 Mon Sep 17 00:00:00 2001 From: Kuroda Kayn Date: Tue, 23 Jun 2026 22:17:28 +0800 Subject: [PATCH 11/15] test(cl): add classfile static member spx case --- .../static-member-classfile-basic/Rect.gox | 14 ++++++++++++++ .../static-member-classfile-basic/out.go | 17 +++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 cl/_testspx/static-member-classfile-basic/Rect.gox create mode 100644 cl/_testspx/static-member-classfile-basic/out.go diff --git a/cl/_testspx/static-member-classfile-basic/Rect.gox b/cl/_testspx/static-member-classfile-basic/Rect.gox new file mode 100644 index 000000000..33223a5aa --- /dev/null +++ b/cl/_testspx/static-member-classfile-basic/Rect.gox @@ -0,0 +1,14 @@ +const .name = "rect" +var ( + .count int = 100 + Rect.total int = 200 +) + +func Get() string { + return name +} + +func Inc() { + count++ + Rect.total++ +} diff --git a/cl/_testspx/static-member-classfile-basic/out.go b/cl/_testspx/static-member-classfile-basic/out.go new file mode 100644 index 000000000..38d5569b7 --- /dev/null +++ b/cl/_testspx/static-member-classfile-basic/out.go @@ -0,0 +1,17 @@ +package main + +const XGos_Rect_Name = "rect" + +type Rect struct { +} + +var XGos_Rect_Count int = 100 +var XGos_Rect_Total int = 200 + +func (this *Rect) Get() string { + return XGos_Rect_Name +} +func (this *Rect) Inc() { + XGos_Rect_Count++ + XGos_Rect_Total++ +} From 0a6f72c91cea9cbd703b6562d01d8b78de79fd35 Mon Sep 17 00:00:00 2001 From: Kuroda Kayn Date: Tue, 23 Jun 2026 22:18:45 +0800 Subject: [PATCH 12/15] test(cl): add classfile static before fields spx case --- .../Rect.gox | 8 ++++++++ .../static-member-classfile-before-fields/out.go | 15 +++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 cl/_testspx/static-member-classfile-before-fields/Rect.gox create mode 100644 cl/_testspx/static-member-classfile-before-fields/out.go diff --git a/cl/_testspx/static-member-classfile-before-fields/Rect.gox b/cl/_testspx/static-member-classfile-before-fields/Rect.gox new file mode 100644 index 000000000..a7cbe12fb --- /dev/null +++ b/cl/_testspx/static-member-classfile-before-fields/Rect.gox @@ -0,0 +1,8 @@ +var ( + .count int = 100 + width int = 10 +) + +func Get() int { + return width +} diff --git a/cl/_testspx/static-member-classfile-before-fields/out.go b/cl/_testspx/static-member-classfile-before-fields/out.go new file mode 100644 index 000000000..5d8b434c6 --- /dev/null +++ b/cl/_testspx/static-member-classfile-before-fields/out.go @@ -0,0 +1,15 @@ +package main + +type Rect struct { + width int +} + +var XGos_Rect_Count int = 100 + +func (this *Rect) Get() int { + return this.width +} +func (this *Rect) XGo_Init() *Rect { + this.width = 10 + return this +} From c007925df5b8edd9b7f6da6eee13c28446cb422e Mon Sep 17 00:00:00 2001 From: Kuroda Kayn Date: Tue, 23 Jun 2026 22:21:43 +0800 Subject: [PATCH 13/15] test(cl): add classfile mixed static field spx case --- .../static-member-classfile-mixed-field/Rect.gox | 7 +++++++ .../static-member-classfile-mixed-field/out.go | 15 +++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 cl/_testspx/static-member-classfile-mixed-field/Rect.gox create mode 100644 cl/_testspx/static-member-classfile-mixed-field/out.go diff --git a/cl/_testspx/static-member-classfile-mixed-field/Rect.gox b/cl/_testspx/static-member-classfile-mixed-field/Rect.gox new file mode 100644 index 000000000..65335fb04 --- /dev/null +++ b/cl/_testspx/static-member-classfile-mixed-field/Rect.gox @@ -0,0 +1,7 @@ +var ( + Rect.total, width int = 200, 10 +) + +func Get() int { + return width + Rect.total +} diff --git a/cl/_testspx/static-member-classfile-mixed-field/out.go b/cl/_testspx/static-member-classfile-mixed-field/out.go new file mode 100644 index 000000000..62a5a643c --- /dev/null +++ b/cl/_testspx/static-member-classfile-mixed-field/out.go @@ -0,0 +1,15 @@ +package main + +type Rect struct { + width int +} + +var XGos_Rect_Total int = 200 + +func (this *Rect) Get() int { + return this.width + XGos_Rect_Total +} +func (this *Rect) XGo_Init() *Rect { + this.width = 10 + return this +} From bb55e566fbf87ef5d5bcfebc7ac86d6abca99cd1 Mon Sep 17 00:00:00 2001 From: Kuroda Kayn Date: Tue, 23 Jun 2026 22:51:40 +0800 Subject: [PATCH 14/15] test(cl): cover static member errors --- cl/error_msg_test.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/cl/error_msg_test.go b/cl/error_msg_test.go index 3749aead2..597b2162a 100644 --- a/cl/error_msg_test.go +++ b/cl/error_msg_test.go @@ -800,6 +800,16 @@ func TestErrNoEntrypoint(t *testing.T) { codeErrorTest(t, `bar.xgo:1:9: undefined: abc`, `println abc +`) + codeErrorTest(t, `bar.xgo:1:7: undefined: Missing`, `const Missing.name = "xgo"`) + codeErrorTest(t, `bar.xgo:1:5: undefined: Missing`, `var Missing.count int`) + codeErrorTest(t, `bar.xgo:3:6: undefined: Missing`, `type foo int + +a := Missing.name +`) + codeErrorTest(t, `bar.xgo:3:7: value is not a type`, `const value = 1 + +const value.name = "xgo" `) codeErrorTestEx(t, "bar", "bar.xgo", `bar.xgo:2:9: undefined: abc`, @@ -1001,6 +1011,12 @@ var ( ) type A struct{} println "hello" +`) + codeErrorTestEx(t, "main", "Rect.gox", + `Rect.gox:3:2: cannot split static and non-static names with a shared initializer`, ` +var ( + .count, width int = 100 +) `) } From 2fac8bc170f230969f0e8326d42faa4b6091922c Mon Sep 17 00:00:00 2001 From: Kuroda Kayn Date: Tue, 23 Jun 2026 22:51:45 +0800 Subject: [PATCH 15/15] test(cl): cover classfile static member recorder --- cl/static_member_recorder_test.go | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/cl/static_member_recorder_test.go b/cl/static_member_recorder_test.go index 2c6941c9f..92c5c3ad3 100644 --- a/cl/static_member_recorder_test.go +++ b/cl/static_member_recorder_test.go @@ -98,3 +98,25 @@ foo.count++ } } } + +func TestClassfileStaticMemberRecorderUsesBareMember(t *testing.T) { + rec := &staticMemberRecorder{fset: cltest.Conf.Fset} + conf := *cltest.Conf + conf.Recorder = rec + + fs := memfs.SingleFile("/foo", "Rect.gox", ` +const .name = "rect" + +func Get() string { + return name +} +`) + cltest.DoFS(t, &conf, fs, "/foo", nil, "main", nil) + + if rec.hasUse("Rect", "Rect", 4) { + t.Fatalf("bare static member should not record explicit receiver use: %#v", rec.uses) + } + if !rec.hasUse("name", "XGos_Rect_Name", 5) { + t.Fatalf("missing static member use in %#v", rec.uses) + } +}