From 327f75bb45ecbcc9dc286fb8c1ad46fa7dafe30e Mon Sep 17 00:00:00 2001 From: CharlesKWON Date: Sat, 18 Apr 2026 16:17:32 +0900 Subject: [PATCH] =?UTF-8?q?fix(parser):=20DATA=20x,=20y,=20z=20registers?= =?UTF-8?q?=20every=20name=20=E2=80=94=20not=20just=20the=20first?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Harbour's `DATA name1, name2, name3` (and `VAR`, `CLASSDATA`) should declare every listed field. Five's parseDataDecl instead returned a single DataDecl for the first name and silently dropped the rest — the comma branch just consumed the identifier without producing a new decl. Surfaced by the OPERATOR overloading test (/tmp/test_operator.prg originally had `DATA x, y` for a Vec2 class) where later `::y` access panicked with "unknown method y". Change the signature to `[]*ast.DataDecl` and rewrite the loop so each comma closes the current decl and starts a fresh one. AS / INIT / qualifier runs still attach to the most recent name, so: DATA x, y, z → three decls, no init DATA x INIT 10, y, z INIT 0 → init attaches to preceding name DATA cName AS CHARACTER → typed single decl All seven class-body call sites flatten the slice into `members`. Verified with /tmp/test_multidata.prg (`DATA x, y, z` + mixed `DATA label INIT "origin", count INIT 0`) and the OPERATOR test which now passes with the original `DATA x, y` form restored. FiveSql2 43/43, Harbour compat 56/56, Go test ALL PASS. Co-Authored-By: Claude Opus 4.7 (1M context) --- compiler/parser/parser.go | 58 +++++++++++++++++++++++++++------------ 1 file changed, 41 insertions(+), 17 deletions(-) diff --git a/compiler/parser/parser.go b/compiler/parser/parser.go index b35f205..1c30cb5 100644 --- a/compiler/parser/parser.go +++ b/compiler/parser/parser.go @@ -543,7 +543,7 @@ func (p *Parser) parseClassDecl() *ast.ClassDecl { for !p.atAny(token.ENDCLASS, token.END, token.EOF) { switch p.current.Kind { case token.DATA: - members = append(members, p.parseDataDecl()) + for _, dd := range p.parseDataDecl() { members = append(members, dd) } case token.METHOD: members = append(members, p.parseClassMethodDecl()) case token.ACCESS: @@ -554,13 +554,13 @@ func (p *Parser) parseClassDecl() *ast.ClassDecl { // CLASS VAR / CLASS METHOD / CLASS DATA inside class body p.advance() // skip CLASS if p.current.Kind == token.DATA { - members = append(members, p.parseDataDecl()) + for _, dd := range p.parseDataDecl() { members = append(members, dd) } } else if p.current.Kind == token.METHOD { members = append(members, p.parseClassMethodDecl()) } else if p.current.Kind == token.IDENT && p.currentUpper() == "VAR" { p.tokens[p.pos].Kind = token.DATA p.current = p.tokens[p.pos] - members = append(members, p.parseDataDecl()) + for _, dd := range p.parseDataDecl() { members = append(members, dd) } } else { p.skipToEndOfLine() } @@ -588,7 +588,7 @@ func (p *Parser) parseClassDecl() *ast.ClassDecl { // Rewrite as DATA token p.tokens[p.pos].Kind = token.DATA p.current = p.tokens[p.pos] - members = append(members, p.parseDataDecl()) + for _, dd := range p.parseDataDecl() { members = append(members, dd) } } else if (upper == "PROTECTED" || upper == "EXPORTED" || upper == "HIDDEN" || upper == "VISIBLE" || upper == "EXPORT" || upper == "SYNC") && p.peekAt(1) == token.COLON { @@ -600,16 +600,16 @@ func (p *Parser) parseClassDecl() *ast.ClassDecl { // CLASSDATA / CLASSVAR — class-level variable (treat as DATA) p.tokens[p.pos].Kind = token.DATA p.current = p.tokens[p.pos] - members = append(members, p.parseDataDecl()) + for _, dd := range p.parseDataDecl() { members = append(members, dd) } } else if upper == "CLASS" { // CLASS VAR — class-level variable p.advance() // skip CLASS if p.current.Kind == token.IDENT && p.currentUpper() == "VAR" { p.tokens[p.pos].Kind = token.DATA p.current = p.tokens[p.pos] - members = append(members, p.parseDataDecl()) + for _, dd := range p.parseDataDecl() { members = append(members, dd) } } else if p.current.Kind == token.DATA { - members = append(members, p.parseDataDecl()) + for _, dd := range p.parseDataDecl() { members = append(members, dd) } } else if p.current.Kind == token.METHOD { members = append(members, p.parseClassMethodDecl()) } else { @@ -662,25 +662,46 @@ func (p *Parser) parseClassDecl() *ast.ClassDecl { } } -func (p *Parser) parseDataDecl() *ast.DataDecl { +// parseDataDecl parses `DATA name [AS type] [INIT expr] [qualifiers]`, +// with optional comma-separated additional names each carrying their +// own AS / INIT / qualifier run — Harbour syntax: +// +// DATA x, y, z -- three decls, no init +// DATA x INIT 10, y, z INIT 0 -- init attaches to preceding name +// DATA cName AS CHARACTER INIT "" -- typed single decl +// +// Returns the full list; callers flatten it into the class member +// slice. Previously only the first name survived — the loop skipped +// extra names after a comma without creating decls for them. +func (p *Parser) parseDataDecl() []*ast.DataDecl { dataPos := p.expect(token.DATA).Pos - name := p.expectMethodName().Literal // allow keywords as data names - var init ast.Expr - var asType string + current := &ast.DataDecl{ + DataPos: dataPos, + Name: p.expectMethodName().Literal, + } + out := []*ast.DataDecl{} - // Parse AS, INIT, commas, and qualifiers in any order for p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF { if p.match(token.AS) { if p.current.Kind == token.IDENT || p.current.Literal != "" { - p.advance() // type name + current.AsType = p.current.Literal + p.advance() } continue } if p.match(token.COMMA) { - // VAR One, Two, Three — skip additional names + out = append(out, current) if p.current.Kind == token.IDENT || p.current.Literal != "" { - p.advance() // skip additional name + current = &ast.DataDecl{ + DataPos: p.current.Pos, + Name: p.current.Literal, + } + p.advance() + } else { + // Trailing comma or missing name — bail cleanly. + current = nil + break } continue } @@ -688,7 +709,7 @@ func (p *Parser) parseDataDecl() *ast.DataDecl { upper := p.currentUpper() if upper == "INIT" { p.advance() - init = p.parseExpr() + current.Init = p.parseExpr() continue } // Skip visibility/attribute qualifiers @@ -705,9 +726,12 @@ func (p *Parser) parseDataDecl() *ast.DataDecl { } break } + if current != nil { + out = append(out, current) + } p.expectEndOfStmt() - return &ast.DataDecl{DataPos: dataPos, Name: name, Init: init, AsType: asType} + return out } // expectMethodName: method names can be keywords (end, home, left, right, etc.)