From 449a1984164ad24242f7c545a04b015748d5f08f Mon Sep 17 00:00:00 2001 From: Kuroda Kayn Date: Sun, 5 Jul 2026 01:16:36 +0800 Subject: [PATCH 1/3] feat(parser): parse static value names --- parser/parser.go | 196 ++++++++++++++++++++++++----------------------- 1 file changed, 101 insertions(+), 95 deletions(-) diff --git a/parser/parser.go b/parser/parser.go index aeb90bb3f..388ec28dd 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -92,8 +92,6 @@ type parser struct { syncPos token.Pos // last synchronization position syncCnt int // number of parser.advance calls without progress - classVarDecl token.Pos // first top-level var declaration in a classfile - // Non-syntactic parser control exprLev int // < 0: in control clause, >= 0: in expression inRHS bool // if set, the parser is parsing a rhs expression @@ -709,55 +707,47 @@ func (p *parser) parseIdent() *ast.Ident { return &ast.Ident{NamePos: pos, Name: name} } -func (p *parser) parseIdentEx() (*ast.Ident, bool) { - var posErr token.Pos - var ret string - if p.inClassFile() && p.tok == token.PERIOD { - posDot := p.pos +func (p *parser) parseValueName(allowDot bool) (*ast.Ident, bool) { + if allowDot && p.tok == token.PERIOD { + dot := p.pos p.next() name := p.parseIdent() - ret = "." + name.Name - if posDot+1 == name.NamePos { - return &ast.Ident{NamePos: posDot, Name: ret}, true - } - posErr = name.NamePos - } else { - id := p.parseIdent() - if p.tok != token.PERIOD { - return id, false - } - posDot := p.pos - p.next() - name := p.parseIdent() - ret = id.Name + "." + name.Name - if id.End() == posDot { - if posDot+1 == name.NamePos { - return &ast.Ident{NamePos: id.NamePos, Name: ret}, true - } - posErr = name.NamePos - } else { - posErr = posDot - } + p.checkStaticValueNameSpacing(token.NoPos, dot, name.Pos()) + name.NamePos = dot + name.Name = "." + name.Name + return name, true } - const msg = "whitespace is not allowed in static member name" - p.error(posErr, msg) - return &ast.Ident{NamePos: posErr, Name: ret}, true + name := p.parseIdent() + if p.tok != token.PERIOD { + return name, false + } + dot := p.pos + p.next() + sel := p.parseIdent() + p.checkStaticValueNameSpacing(name.End(), dot, sel.Pos()) + name.Name += "." + sel.Name + return name, true } -func (p *parser) parseIdentList() (list []*ast.Ident, hasStatic bool) { - if p.trace { - defer un(trace(p, "IdentList")) +func (p *parser) checkStaticValueNameSpacing(leftEnd, dot, right token.Pos) { + const msg = "whitespace is not allowed in static value name" + if leftEnd.IsValid() && leftEnd != dot { + p.error(dot, msg) + } + if right != dot+1 { + p.error(right, msg) } +} - idFirst, hasStatic := p.parseIdentEx() - list = append(list, idFirst) +func (p *parser) parseValueNameList(allowDot bool) (list []*ast.Ident, hasStatic bool) { + name, static := p.parseValueName(allowDot) + list = append(list, name) + hasStatic = static for p.tok == token.COMMA { p.next() - idNext, isStatic := p.parseIdentEx() - if isStatic { - hasStatic = true - } - list = append(list, idNext) + name, static = p.parseValueName(allowDot) + list = append(list, name) + hasStatic = hasStatic || static } return } @@ -3991,69 +3981,92 @@ func (p *parser) inClassFile() bool { return p.mode&ParseXGoClass != 0 } -func typeFromIdentEx(id *ast.Ident, isStatic bool) ast.Expr { - if isStatic { - pos := strings.IndexByte(id.Name, '.') - return &ast.SelectorExpr{ - X: &ast.Ident{NamePos: id.NamePos, Name: id.Name[:pos]}, - Sel: &ast.Ident{NamePos: id.NamePos + token.Pos(pos+1), Name: id.Name[pos+1:]}, - } - } - return id -} - -func (p *parser) parseValueSpec(doc *ast.CommentGroup, keyword token.Token, iota int) ast.Spec { - if p.trace { - defer un(trace(p, keyword.String()+"Spec")) +func (p *parser) parseClassValueSpec() (idents []*ast.Ident, typ ast.Expr, tag *ast.BasicLit, values []ast.Expr, hasStatic bool) { + var starPos token.Pos + if p.tok == token.MUL { + starPos = p.pos + p.next() } - - pos := p.pos - var idents []*ast.Ident - var hasStatic bool - var typ ast.Expr - var tag *ast.BasicLit - var values []ast.Expr - if p.inClassFile() && p.topScope == p.pkgScope && keyword == token.VAR { - var idFirst *ast.Ident - var starPos token.Pos - if p.tok == token.MUL { - starPos = p.pos + if p.tok == token.PERIOD && starPos == token.NoPos { + idents, hasStatic = p.parseValueNameList(true) + typ = p.tryType() + if p.tok == token.ASSIGN { p.next() + values = p.parseRHSList() } - idFirst, hasStatic = p.parseIdentEx() - if starPos != token.NoPos { - typ = &ast.StarExpr{ - Star: starPos, - X: typeFromIdentEx(idFirst, hasStatic), + } else { + ident := p.parseIdent() + if p.tok == token.PERIOD { + dot := p.pos + p.next() + selOK := p.tok == token.IDENT + sel := p.parseIdent() + if selOK && starPos == token.NoPos && + p.tok != token.SEMICOLON && p.tok != token.STRING && p.tok != token.RPAREN { + p.checkStaticValueNameSpacing(ident.End(), dot, sel.Pos()) + ident.Name += "." + sel.Name + idents = append(idents, ident) + hasStatic = true + for p.tok == token.COMMA { + p.next() + name, static := p.parseValueName(true) + idents = append(idents, name) + hasStatic = hasStatic || static + } + typ = p.tryType() + if p.tok == token.ASSIGN { + p.next() + values = p.parseRHSList() + } + } else { + typ = &ast.SelectorExpr{X: ident, Sel: sel} + if starPos != token.NoPos { + typ = &ast.StarExpr{Star: starPos, X: typ} + } } - hasStatic = false + } else if starPos != token.NoPos { + typ = &ast.StarExpr{Star: starPos, X: ident} } else { - idents = append(idents, idFirst) + idents = append(idents, ident) for p.tok == token.COMMA { p.next() - idNext, isStatic := p.parseIdentEx() - if isStatic { - hasStatic = true - } - idents = append(idents, idNext) + name, static := p.parseValueName(true) + idents = append(idents, name) + hasStatic = hasStatic || static } typ = p.tryType() if p.tok == token.ASSIGN { p.next() values = p.parseRHSList() } else if len(idents) == 1 && typ == nil { - typ = typeFromIdentEx(idFirst, hasStatic) - hasStatic = false + typ = ident idents = nil } } - if p.tok == token.STRING { - tag = &ast.BasicLit{ValuePos: p.pos, Kind: p.tok, Value: p.lit} - p.next() - } - p.expect(token.SEMICOLON) + } + if p.tok == token.STRING { + tag = &ast.BasicLit{ValuePos: p.pos, Kind: p.tok, Value: p.lit} + p.next() + } + p.expect(token.SEMICOLON) + return +} + +func (p *parser) parseValueSpec(doc *ast.CommentGroup, keyword token.Token, iota int) ast.Spec { + if p.trace { + defer un(trace(p, keyword.String()+"Spec")) + } + + pos := p.pos + var idents []*ast.Ident + var typ ast.Expr + var tag *ast.BasicLit + var values []ast.Expr + var hasStatic bool + if p.inClassFile() && p.topScope == p.pkgScope && keyword == token.VAR { + idents, typ, tag, values, hasStatic = p.parseClassValueSpec() } else { - idents, hasStatic = p.parseIdentList() + idents, hasStatic = p.parseValueNameList(p.inClassFile()) typ = p.tryType() // always permit optional initialization for more tolerant parsing if p.tok == token.ASSIGN { @@ -4160,13 +4173,6 @@ func (p *parser) parseGenDecl(keyword token.Token, f parseSpecFunction) *ast.Gen } doc := p.leadComment pos := p.expect(keyword) - if keyword == token.VAR && p.inClassFile() && p.topScope == p.pkgScope { - if p.classVarDecl.IsValid() { - p.error(pos, "multiple top-level var declarations in classfile") - } else { - p.classVarDecl = pos - } - } var lparen, rparen token.Pos var list []ast.Spec if p.tok == token.LPAREN { From d91ede51114abd918b90f5e4fa56b591522c8e84 Mon Sep 17 00:00:00 2001 From: Kuroda Kayn Date: Sun, 5 Jul 2026 01:16:47 +0800 Subject: [PATCH 2/3] test(parser): cover static value specs --- parser/parser_test.go | 89 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 79 insertions(+), 10 deletions(-) diff --git a/parser/parser_test.go b/parser/parser_test.go index a7373a258..16b0899d4 100644 --- a/parser/parser_test.go +++ b/parser/parser_test.go @@ -18,6 +18,7 @@ package parser import ( "io/fs" + "strings" "testing" "github.com/goplus/xgo/ast" @@ -64,6 +65,80 @@ func TestAssert(t *testing.T) { assert(false, "panic msg") } +func TestStaticValueSpecs(t *testing.T) { + fset := token.NewFileSet() + f, err := ParseEntry(fset, "/foo/Rect.gox", `const global = 1 +const .kind, Rect.name = "shape", "rect" +var ( + .count int = 1 + Rect.total, .extra int = 2, 3 + width int +) +`, Config{Mode: ParseXGoClass | AllErrors}) + if err != nil { + t.Fatal("ParseEntry:", err) + } + for i, want := range []struct { + names []string + hasStatic bool + }{ + {[]string{"global"}, false}, + {[]string{".kind", "Rect.name"}, true}, + {[]string{".count"}, true}, + {[]string{"Rect.total", ".extra"}, true}, + {[]string{"width"}, false}, + } { + decl := f.Decls[2] + specIndex := i - 2 + if i < 2 { + decl = f.Decls[i] + specIndex = 0 + } + spec := decl.(*ast.GenDecl).Specs[specIndex].(*ast.ValueSpec) + if spec.HasStatic != want.hasStatic || len(spec.Names) != len(want.names) { + t.Fatalf("spec %d = %#v", i, spec) + } + for j, name := range want.names { + if spec.Names[j].Name != name { + t.Fatalf("spec %d name %d = %q, want %q", i, j, spec.Names[j].Name, name) + } + } + } + if got := f.ClassFieldsDecl(); got != f.Decls[2] { + t.Fatalf("ClassFieldsDecl = %p, want %p", got, f.Decls[2]) + } +} + +func TestStaticValueSpecWhitespaceErrors(t *testing.T) { + for _, test := range []struct { + name string + code string + }{ + {"before dot", "var (\nRect .count int\n)\n"}, + {"after shorthand dot", "var (\n. count int\n)\n"}, + {"after receiver dot", "var (\nRect. count int\n)\n"}, + } { + t.Run(test.name, func(t *testing.T) { + fset := token.NewFileSet() + _, err := ParseEntry(fset, "/foo/Rect.gox", test.code, Config{ + Mode: ParseXGoClass | AllErrors, + }) + if err == nil || !strings.Contains(err.Error(), "whitespace is not allowed in static value name") { + t.Fatalf("ParseEntry error = %v", err) + } + }) + } + t.Run("qualified embedded type", func(t *testing.T) { + fset := token.NewFileSet() + _, err := ParseEntry(fset, "/foo/Rect.gox", "var (\npkg . Type\n)\n", Config{ + Mode: ParseXGoClass | AllErrors, + }) + if err != nil { + t.Fatalf("ParseEntry error = %v", err) + } + }) +} + func panicMsg(e any) string { switch v := e.(type) { case string: @@ -241,12 +316,6 @@ func TestErrInFunc(t *testing.T) { // ----------------------------------------------------------------------------- func TestClassErrCode(t *testing.T) { - testClassErrCode(t, `var ( - x int -) - -var y int -`, `/foo/bar.gox:5:1: multiple top-level var declarations in classfile`, ``) testClassErrCode(t, `var ( A,B v int @@ -256,7 +325,7 @@ var y int A.*B v int ) -`, `/foo/bar.gox:2:4: expected 'IDENT', found '*'`, ``) +`, `/foo/bar.gox:2:4: expected 'IDENT', found '*' (and 2 more errors)`, ``) testClassErrCode(t, `var ( []A v int @@ -279,7 +348,7 @@ func TestErrStaticMember(t *testing.T) { testClassErrCode(t, `var ( . B int ) -`, `/foo/bar.gox:2:4: whitespace is not allowed in static member name`, ``) +`, `/foo/bar.gox:2:4: whitespace is not allowed in static value name`, ``) testErrCode(t, `var ( .B int ) @@ -287,11 +356,11 @@ func TestErrStaticMember(t *testing.T) { testErrCode(t, `var ( A. B int ) -`, `/foo/bar.xgo:2:5: whitespace is not allowed in static member name`, ``) +`, `/foo/bar.xgo:2:5: whitespace is not allowed in static value name`, ``) testErrCode(t, `var ( A .B int ) -`, `/foo/bar.xgo:2:4: whitespace is not allowed in static member name`, ``) +`, `/foo/bar.xgo:2:4: whitespace is not allowed in static value name`, ``) } func TestErrGlobal(t *testing.T) { From 30c1143557ed248f5fd7639b3eaaeee1ee475002 Mon Sep 17 00:00:00 2001 From: Kuroda Kayn Date: Sun, 5 Jul 2026 01:16:47 +0800 Subject: [PATCH 3/3] test(parser): add static value fixtures --- parser/_testdata/staticvalues/Rect.gox | 8 ++ parser/_testdata/staticvalues/parser.expect | 107 ++++++++++++++++++++ parser/_testdata/staticvalues/values.xgo | 5 + 3 files changed, 120 insertions(+) create mode 100644 parser/_testdata/staticvalues/Rect.gox create mode 100644 parser/_testdata/staticvalues/parser.expect create mode 100644 parser/_testdata/staticvalues/values.xgo diff --git a/parser/_testdata/staticvalues/Rect.gox b/parser/_testdata/staticvalues/Rect.gox new file mode 100644 index 000000000..e3aa8b086 --- /dev/null +++ b/parser/_testdata/staticvalues/Rect.gox @@ -0,0 +1,8 @@ +const global = 1 +const .kind, Rect.name = "shape", "rect" + +var ( + .count int = 1 + Rect.total, .extra int = 2, 3 + width int +) diff --git a/parser/_testdata/staticvalues/parser.expect b/parser/_testdata/staticvalues/parser.expect new file mode 100644 index 000000000..19543b384 --- /dev/null +++ b/parser/_testdata/staticvalues/parser.expect @@ -0,0 +1,107 @@ +package main + +file Rect.gox +ast.GenDecl: + Tok: const + Specs: + ast.ValueSpec: + Names: + ast.Ident: + Name: global + Values: + ast.BasicLit: + Kind: INT + Value: 1 +ast.GenDecl: + Tok: const + Specs: + ast.ValueSpec: + Names: + ast.Ident: + Name: .kind + ast.Ident: + Name: Rect.name + Values: + ast.BasicLit: + Kind: STRING + Value: "shape" + ast.BasicLit: + Kind: STRING + Value: "rect" +ast.GenDecl: + Tok: var + Specs: + ast.ValueSpec: + Names: + ast.Ident: + Name: .count + Type: + ast.Ident: + Name: int + Values: + ast.BasicLit: + Kind: INT + Value: 1 + ast.ValueSpec: + Names: + ast.Ident: + Name: Rect.total + ast.Ident: + Name: .extra + Type: + ast.Ident: + Name: int + Values: + ast.BasicLit: + Kind: INT + Value: 2 + ast.BasicLit: + Kind: INT + Value: 3 + ast.ValueSpec: + Names: + ast.Ident: + Name: width + Type: + ast.Ident: + Name: int + +file values.xgo +ast.GenDecl: + Tok: type + Specs: + ast.TypeSpec: + Name: + ast.Ident: + Name: Rect + Type: + ast.Ident: + Name: int +ast.GenDecl: + Tok: const + Specs: + ast.ValueSpec: + Names: + ast.Ident: + Name: Rect.kind + ast.Ident: + Name: global + Values: + ast.BasicLit: + Kind: STRING + Value: "shape" + ast.BasicLit: + Kind: STRING + Value: "global" +ast.GenDecl: + Tok: var + Specs: + ast.ValueSpec: + Names: + ast.Ident: + Name: Rect.count + ast.Ident: + Name: other + Type: + ast.Ident: + Name: int diff --git a/parser/_testdata/staticvalues/values.xgo b/parser/_testdata/staticvalues/values.xgo new file mode 100644 index 000000000..c0dd99393 --- /dev/null +++ b/parser/_testdata/staticvalues/values.xgo @@ -0,0 +1,5 @@ +type Rect int + +const Rect.kind, global = "shape", "global" + +var Rect.count, other int