fix(parser): DATA x, y, z registers every name — not just the first

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) <noreply@anthropic.com>
This commit is contained in:
2026-04-18 16:17:32 +09:00
parent e089c81bcd
commit 327f75bb45

View File

@@ -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.)