Files
five/compiler/parser/parser.go
CharlesKWON 000500e034 fix(pp,parser,gengo): pre-release blocker round (Wave 1)
Six audit-driven blockers landed together because they're tangled:

  * MENU TO removed from std.ch — the rule expanded to a call to a
    nonexistent __MenuTo() RTL symbol, so any user code with `MENU
    TO choice` compiled clean and panicked at runtime. Behavior
    pre-this-round was a parser silent no-op, which is at least
    consistent. Restore that until @ PROMPT (the companion command)
    actually lands.

  * COUNT now requires `TO <var>`. The earlier `[TO <v>]` optional
    bracket was a Harbour-pattern transcription error: the result
    template references `<v>` unconditionally, so a bare `COUNT`
    expanded to ungrammatical ` := 0 ; dbEval(...)` and the
    PRG parser rejected it. Match Harbour's std.ch which makes TO
    mandatory.

  * UPDATE FROM ... REPLACE now requires `FROM`/`ON`/`REPLACE` all
    three. Same root cause as COUNT: the result template uses
    `<key>`, `<f1>`, `<x1>` unconditionally; missing any of them
    produced broken syntax. Tightened to fail loudly rather than
    silently mis-expand.

  * CLOSE <unknown_alias> no longer closes the *current* workarea.
    SelectByAlias was a silent no-op when the alias was missing,
    leaving WASaveAndSelectAlias to evaluate the inner DbCloseArea()
    against the originally-selected WA — a real data-loss footgun.
    SelectByAlias now returns bool; WASaveAndSelectAlias switches to
    the no-area sentinel (0) on miss so the inner expression's
    Current() returns nil and short-circuits.

  * SUM <x1>, <xN> TO <v1>, <vN> — multi-pair form supported.
    Required two pieces:

       1. matchSegment's regular-marker stop-boundary now combines
          outerTail literals AND the segment's repeat boundary so
          `[, <xN>]` doesn't let `<xN>` swallow past the next ','.

       2. **Five parser miscompiled comma-separated expressions in
          code blocks.** `{|| e1, e2, e3 }` kept only the last expr
          and threw away earlier ones at *AST level*, so all their
          side effects vanished. New SeqExpr AST node + emitter
          (emit each, pop intermediate results) + folding/walk
          updates fix the underlying bug, which also unbreaks any
          other block that relied on comma sequencing.

  * pp.go's `;` continuation joiner now strips exactly one trailing
    `;` per iteration, preserving Harbour's `;;` convention (literal
    `;` followed by a continuation marker). Without this the SUM
    rule's chained `<v1> :=[ <vN> :=] 0 ; ; dbEval(...)` collapsed
    to a missing statement separator.

  * parseExprStmt's xBase fallback switch is back in sync with
    parseIdentStmt — COPY/SORT/COUNT/SUM/AVERAGE/TOTAL/UPDATE/JOIN/
    DISPLAY/LIST removed (std.ch handles all of them now). Leaving
    them in the fallback masked typos as silent no-ops.

Gates green:
  go test ./...      : PASS
  FiveSql2 SQL:1999  : 43/43
  Harbour compat     : 56/56

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 07:45:20 +09:00

2483 lines
67 KiB
Go

// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
// All rights reserved.
// Recursive descent parser for the Five language (Harbour-compatible).
//
// Design references:
// - Harbour: harbour.y (Bison grammar, /mnt/d/harbour-core/src/compiler/harbour.y)
// Key: operator precedence is mostly right-associative, = is both assign and compare
// - tsgo: internal/parser/ — recursive descent with precedence climbing
// - Pratt parser for expression parsing (precedence climbing)
//
// Harbour-specific rules enforced:
// - LOCAL/STATIC/FIELD declarations must be at function top (before executable code)
// - ? and ?? are QOut/QQOut shorthand
// - (expr)->field is dynamic alias access
// - &variable is macro expansion
package parser
import (
"five/compiler/ast"
"five/compiler/lexer"
"five/compiler/token"
"fmt"
"strings"
)
// Parser parses Five/Harbour source into an AST.
type Parser struct {
tokens []token.Token
pos int
current token.Token
errors []Error
file string
GoDumps []string // inline Go code blocks from PP
// lastClassName tracks the most recent `CLASS X` / `CREATE CLASS X`
// declaration at file scope. Standalone `METHOD foo(...)` decls
// without an explicit `CLASS Y` clause default to this name —
// matches classic Clipper/Harbour where method bodies follow the
// class declaration and bind implicitly.
lastClassName string
}
// Error represents a parse error with position.
type Error struct {
Pos token.Position
Message string
}
func (e Error) Error() string {
return fmt.Sprintf("%s: %s", e.Pos, e.Message)
}
// Parse parses a source file and returns the AST.
func Parse(filename, source string) (*ast.File, []Error) {
return ParseWithGoDumps(filename, source, nil)
}
// ParseWithGoDumps parses with inline Go code blocks from PP.
func ParseWithGoDumps(filename, source string, goDumps []string) (*ast.File, []Error) {
tokens := lexer.Tokenize(filename, source)
p := &Parser{
tokens: tokens,
pos: 0,
file: filename,
GoDumps: goDumps,
}
if len(tokens) > 0 {
p.current = tokens[0]
}
file := p.parseFile()
return file, p.errors
}
// --- Token navigation ---
func (p *Parser) peek() token.Kind {
return p.current.Kind
}
// currentUpper returns the uppercased literal of the current token.
// Uses strings.EqualFold for comparisons where possible to avoid allocation.
func (p *Parser) currentUpper() string {
return strings.ToUpper(p.current.Literal)
}
// peekAt returns the token kind at offset from current position.
// peekAt(0) = current, peekAt(1) = next, etc. Returns EOF if out of range.
func (p *Parser) peekAt(offset int) token.Kind {
idx := p.pos + offset
if idx >= 0 && idx < len(p.tokens) {
return p.tokens[idx].Kind
}
return token.EOF
}
// peekLitAt returns the token literal at offset from current position.
func (p *Parser) peekLitAt(offset int) string {
idx := p.pos + offset
if idx >= 0 && idx < len(p.tokens) {
return p.tokens[idx].Literal
}
return ""
}
// peekTokenAt returns the full token at offset. Safe for out-of-range.
func (p *Parser) peekTokenAt(offset int) token.Token {
idx := p.pos + offset
if idx >= 0 && idx < len(p.tokens) {
return p.tokens[idx]
}
return token.Token{Kind: token.EOF}
}
func (p *Parser) advance() token.Token {
tok := p.current
p.pos++
if p.pos < len(p.tokens) {
p.current = p.tokens[p.pos]
} else {
p.current = token.Token{Kind: token.EOF}
}
return tok
}
func (p *Parser) match(kind token.Kind) bool {
if p.current.Kind == kind {
p.advance()
return true
}
return false
}
func (p *Parser) expect(kind token.Kind) token.Token {
if p.current.Kind == kind {
return p.advance()
}
p.error(fmt.Sprintf("expected %v, got %v %q", kind, p.current.Kind, p.current.Literal))
// Error recovery: return a synthetic token and try to continue
return token.Token{Kind: kind, Pos: p.current.Pos}
}
func (p *Parser) at(kind token.Kind) bool {
return p.current.Kind == kind
}
func (p *Parser) atAny(kinds ...token.Kind) bool {
for _, k := range kinds {
if p.current.Kind == k {
return true
}
}
return false
}
// --- Error handling & recovery ---
func (p *Parser) error(msg string) {
// Avoid duplicate errors at same position
if len(p.errors) > 0 {
last := p.errors[len(p.errors)-1]
if last.Pos == p.current.Pos {
return
}
}
p.errors = append(p.errors, Error{
Pos: p.current.Pos,
Message: msg,
})
}
// syncStmt skips tokens until a statement boundary is found.
// Called after errors to prevent cascading parse failures.
func (p *Parser) syncStmt() {
for p.current.Kind != token.EOF {
// Newline = statement boundary in Harbour
if p.current.Kind == token.NEWLINE {
p.advance()
return
}
// Statement-starting keywords = safe to resume
switch p.current.Kind {
case token.FUNCTION_KW, token.PROCEDURE, token.CLASS, token.METHOD,
token.LOCAL, token.STATIC, token.PRIVATE, token.PUBLIC,
token.IF, token.FOR, token.DO, token.WHILE, token.SWITCH,
token.BEGIN, token.RETURN, token.QMARK, token.QQMARK:
return
}
p.advance()
}
}
// skipNewlines consumes consecutive NEWLINE tokens.
func (p *Parser) skipNewlines() {
for p.current.Kind == token.NEWLINE {
p.advance()
}
}
// expectEndOfStmt expects a NEWLINE or EOF (statement terminator).
func (p *Parser) expectEndOfStmt() {
if p.current.Kind == token.NEWLINE {
p.advance()
return
}
if p.current.Kind == token.EOF {
return
}
// Don't error for some keywords that start new statements
// (Harbour allows missing newline before certain keywords)
}
// --- File parsing ---
func (p *Parser) parseFile() *ast.File {
file := &ast.File{Name: p.file}
p.skipNewlines()
for p.current.Kind != token.EOF {
switch p.current.Kind {
case token.FUNCTION_KW, token.PROCEDURE:
file.Decls = append(file.Decls, p.parseFuncDecl())
case token.EXIT:
// EXIT PROCEDURE/FUNCTION at top level
p.advance()
if p.current.Kind == token.PROCEDURE || p.current.Kind == token.FUNCTION_KW {
file.Decls = append(file.Decls, p.parseFuncDecl())
} else {
p.skipToEndOfLine()
}
case token.CLASS:
file.Decls = append(file.Decls, p.parseClassDecl())
case token.METHOD:
file.Decls = append(file.Decls, p.parseMethodDecl())
case token.STATIC:
// STATIC FUNCTION/PROCEDURE → function declaration with static scope
if p.peekAt(1) == token.FUNCTION_KW || p.peekAt(1) == token.PROCEDURE {
p.advance() // skip STATIC
file.Decls = append(file.Decls, p.parseFuncDecl())
} else {
// Top-level STATIC declaration (module-level variable)
file.Decls = append(file.Decls, p.parseVarDecl())
}
case token.IDENT:
upper := p.currentUpper()
if upper == "THREAD" && p.peekAt(1) == token.STATIC {
// THREAD STATIC → treat as STATIC
p.advance() // skip THREAD
file.Decls = append(file.Decls, p.parseVarDecl())
} else if upper == "CREATE" && p.peekAt(1) == token.CLASS {
// CREATE CLASS → treat as CLASS
p.advance() // skip CREATE
file.Decls = append(file.Decls, p.parseClassDecl())
} else if upper == "EXTERNAL" || upper == "EXTERN" || upper == "REQUEST" || upper == "ANNOUNCE" || upper == "DYNAMIC" || upper == "DECLARE" {
p.skipToEndOfLine()
} else if upper == "INIT" || upper == "EXIT" {
// INIT PROCEDURE/FUNCTION or EXIT PROCEDURE/FUNCTION
p.advance() // skip INIT/EXIT
if p.current.Kind == token.PROCEDURE || p.current.Kind == token.FUNCTION_KW {
file.Decls = append(file.Decls, p.parseFuncDecl())
} else {
p.skipToEndOfLine()
}
} else if upper == "SET" {
// Top-level SET commands (SET PROCEDURE TO, etc.)
p.skipToEndOfLine()
} else if upper == "FIVE_GODUMP__" {
// Inline Go code block from #pragma BEGINDUMP
file.Decls = append(file.Decls, p.parseGoDump())
} else {
// Unknown IDENT at top level — PP residual or bare expression, skip
p.skipToEndOfLine()
}
case token.MEMVAR, token.DECLARE:
// File-level MEMVAR/DECLARE declaration — skip
p.skipToEndOfLine()
case token.IMPORT:
file.Imports = append(file.Imports, p.parseImport())
case token.PP_INCLUDE, token.PP_DEFINE, token.PP_IFDEF, token.PP_IFNDEF,
token.PP_ELSE, token.PP_ENDIF, token.PP_PRAGMA, token.PP_UNDEF,
token.PP_COMMAND, token.PP_TRANSLATE:
p.skipToEndOfLine() // skip preprocessor for now
default:
// Bare expression or PP residual at top level — skip to EOL
// This handles: PP #command residuals, bare function calls, etc.
p.skipToEndOfLine()
}
p.skipNewlines()
}
return file
}
func (p *Parser) skipToEndOfLine() {
for p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
p.advance()
}
if p.current.Kind == token.NEWLINE {
p.advance()
}
}
// consumeFileExtension handles filename.ext (e.g. test.dbf, test.idx)
// where the dot+extension was parsed as separate tokens after parseExpr.
func (p *Parser) consumeFileExtension(fileExpr ast.Expr) {
for p.current.Kind == token.DOT &&
(p.peekAt(1) == token.IDENT || p.peekAt(1) == token.INT) {
p.advance() // skip .
ext := p.advance() // skip extension
if ident, ok := fileExpr.(*ast.IdentExpr); ok {
ident.Name = ident.Name + "." + ext.Literal
}
}
}
// parseGoDump parses FIVE_GODUMP__ <index> marker from PP.
func (p *Parser) parseGoDump() *ast.GoDumpDecl {
pos := p.advance().Pos // consume FIVE_GODUMP__
idx := 0
if p.current.Kind == token.INT {
fmt.Sscanf(p.current.Literal, "%d", &idx)
p.advance()
}
p.expectEndOfStmt()
code := ""
if idx >= 0 && idx < len(p.GoDumps) {
code = p.GoDumps[idx]
}
return &ast.GoDumpDecl{DumpPos: pos, Code: code}
}
// --- IMPORT ---
func (p *Parser) parseImport() *ast.ImportDecl {
pos := p.expect(token.IMPORT).Pos
alias := ""
// IMPORT _ "pkg" — blank import
// IMPORT alias "pkg" — aliased import
if p.current.Kind == token.IDENT && p.peekAt(1) == token.STRING {
alias = p.advance().Literal
}
pathTok := p.expect(token.STRING)
p.expectEndOfStmt()
return &ast.ImportDecl{ImportPos: pos, Alias: alias, Path: pathTok.Literal}
}
// --- FUNCTION / PROCEDURE ---
func (p *Parser) parseFuncDecl() *ast.FuncDecl {
tok := p.advance() // FUNCTION or PROCEDURE
isProc := tok.Kind == token.PROCEDURE
// Allow keywords as function names (GOTO, END, etc.)
nameTok := p.expectMethodName()
// Parameters
var params []*ast.ParamDecl
if p.match(token.LPAREN) {
params = p.parseParamList()
p.expect(token.RPAREN)
}
// Skip CLASS className qualifier (PROCEDURE name CLASS className)
if p.current.Kind == token.CLASS {
p.advance() // skip CLASS
p.expectMethodName() // skip className
}
p.expectEndOfStmt()
// Declarations (LOCAL, STATIC, FIELD — must come first)
var decls []ast.Decl
p.skipNewlines()
for p.atAny(token.LOCAL, token.STATIC, token.FIELD, token.MEMVAR, token.PARAMETERS) {
decls = append(decls, p.parseVarDecl())
p.skipNewlines()
}
// Body (executable statements until RETURN or end)
body := p.parseStmtBlock(token.RETURN, token.FUNCTION_KW, token.PROCEDURE, token.CLASS, token.METHOD, token.EOF)
// Consume RETURN if present
var endPos token.Position
if p.current.Kind == token.RETURN {
retStmt := p.parseReturn()
body = append(body, retStmt)
endPos = retStmt.Pos()
} else {
endPos = p.current.Pos
}
return &ast.FuncDecl{
FuncPos: tok.Pos,
Name: nameTok.Literal,
IsProc: isProc,
Params: params,
Decls: decls,
Body: body,
EndPos: endPos,
}
}
func (p *Parser) parseParamList() []*ast.ParamDecl {
var params []*ast.ParamDecl
if p.at(token.RPAREN) {
return params
}
// Variadic: FUNCTION Foo(...) — skip dots, use PCount() at runtime
if p.at(token.DOT) {
for p.at(token.DOT) {
p.advance()
}
return params
}
for {
byRef := p.match(token.AT)
// Handle trailing ... after named params: FUNCTION Foo(a, b, ...)
if p.at(token.DOT) {
for p.at(token.DOT) {
p.advance()
}
break
}
name := p.expectMethodName() // allow keywords as param names (e.g. data, type)
var asType string
if p.match(token.AS) {
asType = p.expectMethodName().Literal
}
params = append(params, &ast.ParamDecl{
NamePos: name.Pos,
Name: name.Literal,
ByRef: byRef,
AsType: asType,
})
if !p.match(token.COMMA) {
break
}
}
return params
}
// --- Variable declarations ---
func (p *Parser) parseVarDecl() *ast.VarDecl {
tok := p.advance() // LOCAL, STATIC, FIELD, MEMVAR
var scope ast.VarScope
switch tok.Kind {
case token.LOCAL:
scope = ast.ScopeLocal
case token.STATIC:
scope = ast.ScopeStatic
case token.FIELD:
scope = ast.ScopeField
case token.MEMVAR:
// MEMVAR declares field aliases, treated like FIELD
scope = ast.ScopeField
default:
scope = ast.ScopeLocal
}
var vars []*ast.VarInit
for {
// Allow keywords as variable names (data, field, etc.)
name := p.expectMethodName()
var init ast.Expr
var asType string
// LOCAL a[10] or LOCAL a[5,3] — array declaration
if p.at(token.LBRACKET) {
p.advance() // skip [
sizeExpr := p.parseExpr()
// Multi-dim: a[5,3]
for p.match(token.COMMA) {
p.parseExpr() // consume additional dimensions (simplified)
}
p.expect(token.RBRACKET)
// Generate init as Array(size)
init = &ast.CallExpr{
Func: &ast.IdentExpr{Name: "Array"},
Args: []ast.Expr{sizeExpr},
}
}
// AS type (may come before or after :=)
if p.match(token.AS) {
// Skip type: AS STRING, AS NUMERIC, AS CLASS ClassName, AS ARRAY OF type
for p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF &&
p.current.Kind != token.ASSIGN && p.current.Kind != token.COMMA {
p.advance()
}
}
if p.match(token.ASSIGN) { // :=
init = p.parseExpr()
}
// AS type after := (alternative order)
if p.match(token.AS) {
asType = p.expectMethodName().Literal
// Skip complex AS: AS CLASS ClassName, AS ARRAY OF...
for p.current.Kind == token.IDENT {
upper := p.currentUpper()
if upper == "OF" || upper == "CLASS" {
p.advance()
if p.current.Kind == token.IDENT || p.current.Literal != "" {
p.advance()
}
} else {
break
}
}
}
vars = append(vars, &ast.VarInit{
NamePos: name.Pos,
Name: name.Literal,
Init: init,
AsType: asType,
})
if !p.match(token.COMMA) {
break
}
}
p.expectEndOfStmt()
return &ast.VarDecl{DeclPos: tok.Pos, Scope: scope, Vars: vars}
}
// --- CLASS ---
func (p *Parser) parseClassDecl() *ast.ClassDecl {
classPos := p.expect(token.CLASS).Pos
name := p.expect(token.IDENT).Literal
p.lastClassName = name
var parent string
if p.match(token.INHERIT) {
p.match(token.FROM) // optional FROM
parent = p.expectMethodName().Literal
}
// Alternative: FROM without INHERIT
if parent == "" && p.match(token.FROM) {
parent = p.expectMethodName().Literal
}
// Multiple inheritance: FROM class1, class2, class3 — skip extra parents
for p.match(token.COMMA) {
p.expectMethodName() // skip additional parent class name
}
p.expectEndOfStmt()
var members []ast.Decl
p.skipNewlines()
for !p.atAny(token.ENDCLASS, token.END, token.EOF) {
switch p.current.Kind {
case token.DATA:
for _, dd := range p.parseDataDecl() { members = append(members, dd) }
case token.METHOD:
members = append(members, p.parseClassMethodDecl())
case token.ACCESS:
members = append(members, p.parseAccessDecl())
case token.ASSIGN_KW:
members = append(members, p.parseAssignDecl())
case token.CLASS:
// CLASS VAR / CLASS METHOD / CLASS DATA inside class body
p.advance() // skip CLASS
if p.current.Kind == token.DATA {
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]
for _, dd := range p.parseDataDecl() { members = append(members, dd) }
} else {
p.skipToEndOfLine()
}
case token.OPERATOR_KW:
// OPERATOR "<sym>" ARG <name> INLINE <expr>
if md := p.parseOperatorDecl(); md != nil {
members = append(members, md)
}
case token.INLINE_KW, token.ON, token.DESTRUCTOR:
// Stray INLINE, ON ERROR, DESTRUCTOR — skip to EOL
p.skipToEndOfLine()
p.skipNewlines()
continue
case token.IDENT:
upper := p.currentUpper()
// FRIEND FUNCTION/CLASS — skip declaration
if upper == "FRIEND" {
p.advance() // skip FRIEND
p.skipToEndOfLine()
p.skipNewlines()
continue
}
// VAR = DATA synonym
if upper == "VAR" {
// Rewrite as DATA token
p.tokens[p.pos].Kind = token.DATA
p.current = p.tokens[p.pos]
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 {
// Scope qualifier — skip it (Five doesn't enforce visibility)
p.advance() // skip keyword
p.advance() // skip :
p.skipNewlines()
} else if upper == "CLASSDATA" || upper == "CLASSVAR" {
// CLASSDATA / CLASSVAR — class-level variable (treat as DATA)
p.tokens[p.pos].Kind = token.DATA
p.current = p.tokens[p.pos]
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]
for _, dd := range p.parseDataDecl() { members = append(members, dd) }
} else if p.current.Kind == token.DATA {
for _, dd := range p.parseDataDecl() { members = append(members, dd) }
} else if p.current.Kind == token.METHOD {
members = append(members, p.parseClassMethodDecl())
} else {
p.skipToEndOfLine()
}
} else if upper == "MESSAGE" {
// MESSAGE <name> [(params)] INLINE <expr>
// Harbour sugar for an inline method. Emit as MethodDecl so
// emitClassDecl routes it through AddMethod and the inline
// body emitter generates the HB_<CLASS>_<NAME> function.
members = append(members, p.parseMessageDecl())
} else if upper == "OPERATOR" {
// OPERATOR "<sym>" ARG <name> INLINE <expr>
if md := p.parseOperatorDecl(); md != nil {
members = append(members, md)
}
} else if upper == "ON" || upper == "DESTRUCTOR" ||
upper == "DELEGATE" || upper == "ERROR" ||
upper == "VIRTUAL" || upper == "DEFERRED" {
// ON ERROR, OPERATOR "+" ARG, DESTRUCTOR, DELEGATE — skip to EOL
p.skipToEndOfLine()
p.skipNewlines()
continue
} else {
p.error(fmt.Sprintf("unexpected in CLASS body: %v %q", p.current.Kind, p.current.Literal))
p.advance()
}
default:
p.error(fmt.Sprintf("unexpected in CLASS body: %v", p.current.Kind))
p.advance()
}
p.skipNewlines()
}
endPos := p.current.Pos
if p.match(token.ENDCLASS) {
// ok
} else if p.match(token.END) {
// END CLASS — skip optional CLASS keyword
p.match(token.CLASS)
}
p.expectEndOfStmt()
return &ast.ClassDecl{
ClassPos: classPos,
Name: name,
ParentName: parent,
Members: members,
EndPos: endPos,
}
}
// 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
current := &ast.DataDecl{
DataPos: dataPos,
Name: p.expectMethodName().Literal,
}
out := []*ast.DataDecl{}
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 != "" {
current.AsType = p.current.Literal
p.advance()
}
continue
}
if p.match(token.COMMA) {
out = append(out, current)
if p.current.Kind == token.IDENT || p.current.Literal != "" {
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
}
if p.current.Kind == token.IDENT {
upper := p.currentUpper()
if upper == "INIT" {
p.advance()
current.Init = p.parseExpr()
continue
}
// Skip visibility/attribute qualifiers
if upper == "READONLY" || upper == "EXPORTED" || upper == "PROTECTED" ||
upper == "HIDDEN" || upper == "SYNC" || upper == "USUAL" ||
upper == "PROPERTY" || upper == "PERSISTENT" || upper == "SHARED" {
p.advance()
continue
}
}
if p.current.Kind == token.INLINE_KW {
p.skipToEndOfLine()
break
}
break
}
if current != nil {
out = append(out, current)
}
p.expectEndOfStmt()
return out
}
// expectMethodName: method names can be keywords (end, home, left, right, etc.)
func (p *Parser) expectMethodName() token.Token {
if p.current.Kind == token.IDENT {
return p.advance()
}
// Allow keywords as method names
if p.current.Literal != "" {
return p.advance()
}
return p.expect(token.IDENT)
}
func (p *Parser) parseClassMethodDecl() *ast.MethodDecl {
methodPos := p.expect(token.METHOD).Pos
// Skip optional FUNCTION/PROCEDURE qualifier
if p.current.Kind == token.FUNCTION_KW || p.current.Kind == token.PROCEDURE {
p.advance()
}
name := p.expectMethodName().Literal
var params []*ast.ParamDecl
if p.match(token.LPAREN) {
params = p.parseParamList()
p.expect(token.RPAREN)
}
// Check for SETGET keyword
isSetGet := false
if p.current.Kind == token.IDENT && p.currentUpper() == "SETGET" {
p.advance()
isSetGet = true
}
// Skip trailing qualifiers: OPERATOR, VIRTUAL, CONSTRUCTOR, etc.
if p.current.Kind == token.IDENT {
upper := p.currentUpper()
if upper == "OPERATOR" || upper == "VIRTUAL" || upper == "DEFERRED" {
p.skipToEndOfLine()
}
}
if p.current.Kind == token.OPERATOR_KW || p.current.Kind == token.CONSTRUCTOR {
p.skipToEndOfLine()
}
// INLINE expression — parse as the method body instead of skipping.
// Harbour semantics: `METHOD X() INLINE expr` is sugar for a method
// whose body is `RETURN expr`.
isInline := false
var inlineBody ast.Expr
if p.current.Kind == token.INLINE_KW ||
(p.current.Kind == token.IDENT && p.currentUpper() == "INLINE") {
p.advance() // consume INLINE
isInline = true
inlineBody = p.parseExpr()
}
p.expectEndOfStmt()
return &ast.MethodDecl{
MethodPos: methodPos,
Name: name,
Params: params,
IsSetGet: isSetGet,
IsInline: isInline,
InlineBody: inlineBody,
EndPos: methodPos,
}
}
// operatorNameIndex maps a Harbour operator symbol to its slot in
// hbrt.ClassDef.Operators. Values mirror hbrt.Op* constants
// (hbrt/class.go:50-73).
//
// `=` and `==` both route through VM Thread.Equal(), which dispatches
// on OpEqual (8) — Five doesn't distinguish them at the VM level, so
// `==` maps to the same slot. `!=`, `<>`, `#` are all OpNotEqual (10).
var operatorNameIndex = map[string]int{
"+": 0, "-": 1, "*": 2, "/": 3, "%": 4, "^": 5,
"++": 6, "--": 7,
"=": 8, "==": 8, "!=": 10, "<>": 10, "#": 10,
"<": 11, "<=": 12, ">": 13, ">=": 14,
":=": 15, "$": 16,
}
// parseOperatorDecl parses:
// OPERATOR "<sym>" [ARG <name>] INLINE <expr>
//
// The ARG binds the RHS operand to a local when the operator is
// dispatched via the VM's binary op. INLINE <expr> is the body.
// Harbour also allows `OPERATOR "<sym>" ... METHOD foo CLASS X`
// (non-inline) but that form is rare; we only support INLINE for
// now and skip the line otherwise.
func (p *Parser) parseOperatorDecl() *ast.MethodDecl {
opPos := p.current.Pos
p.advance() // OPERATOR
if p.current.Kind != token.STRING {
p.skipToEndOfLine()
return nil
}
symbol := p.current.Literal
p.advance()
opIdx, ok := operatorNameIndex[symbol]
if !ok {
p.skipToEndOfLine()
return nil
}
var params []*ast.ParamDecl
if p.current.Kind == token.IDENT && p.currentUpper() == "ARG" {
p.advance()
argName := p.expectMethodName().Literal
params = []*ast.ParamDecl{{Name: argName}}
}
if !(p.current.Kind == token.INLINE_KW ||
(p.current.Kind == token.IDENT && p.currentUpper() == "INLINE")) {
// Non-inline operator — body arrives as a separate METHOD decl.
// Unsupported for now; skip.
p.skipToEndOfLine()
return nil
}
p.advance() // INLINE
body := p.parseExpr()
p.expectEndOfStmt()
// Synthesise a method named __OP_<idx> so the vtable doesn't collide
// with user-declared methods. emitClassDecl sees OperatorOp >= 0 and
// routes registration through AddOperator instead of AddMethod.
return &ast.MethodDecl{
MethodPos: opPos,
Name: fmt.Sprintf("__OP_%d", opIdx),
Params: params,
IsInline: true,
InlineBody: body,
IsOperator: true,
OperatorOp: opIdx,
EndPos: opPos,
}
}
// parseMessageDecl parses `MESSAGE <name> [(params)] INLINE <expr>` in
// a CLASS body and returns a MethodDecl. Harbour semantics: a MESSAGE
// handler is invoked like a method and behaves identically — the form
// exists mostly for readability of "this ident is really a message
// handler, not a regular method". Without INLINE the handler is
// declaration-only (the real body arrives as a separate
// `METHOD <name>() CLASS X` implementation).
func (p *Parser) parseMessageDecl() *ast.MethodDecl {
msgPos := p.current.Pos
p.advance() // MESSAGE
name := p.expectMethodName().Literal
var params []*ast.ParamDecl
if p.match(token.LPAREN) {
params = p.parseParamList()
p.expect(token.RPAREN)
}
isInline := false
var inlineBody ast.Expr
if p.current.Kind == token.INLINE_KW ||
(p.current.Kind == token.IDENT && p.currentUpper() == "INLINE") {
p.advance()
isInline = true
inlineBody = p.parseExpr()
}
p.expectEndOfStmt()
return &ast.MethodDecl{
MethodPos: msgPos,
Name: name,
Params: params,
IsInline: isInline,
InlineBody: inlineBody,
EndPos: msgPos,
}
}
// skipClassInline skips INLINE keyword and the rest of the line.
// Used by ACCESS/ASSIGN decls where inline body handling hasn't been
// wired up yet (falls back to pre-INLINE-capture behaviour).
func (p *Parser) skipClassInline() {
if p.current.Kind == token.INLINE_KW ||
(p.current.Kind == token.IDENT && p.currentUpper() == "INLINE") {
p.skipToEndOfLine()
}
}
// ACCESS name METHOD getterName
func (p *Parser) parseAccessDecl() *ast.MethodDecl {
pos := p.expect(token.ACCESS).Pos
propName := p.expectMethodName().Literal
// Skip optional (params)
if p.match(token.LPAREN) {
for !p.atAny(token.RPAREN, token.EOF) {
p.advance()
}
p.match(token.RPAREN)
}
methodName := propName
if p.match(token.METHOD) {
methodName = p.expectMethodName().Literal
if p.match(token.LPAREN) {
for !p.atAny(token.RPAREN, token.EOF) {
p.advance()
}
p.match(token.RPAREN)
}
}
// Skip INLINE + rest of line
p.skipClassInline()
p.expectEndOfStmt()
return &ast.MethodDecl{
MethodPos: pos,
Name: methodName,
IsAccess: true,
AccessName: propName,
EndPos: pos,
}
}
// ASSIGN name METHOD setterName
func (p *Parser) parseAssignDecl() *ast.MethodDecl {
pos := p.expect(token.ASSIGN_KW).Pos
propName := p.expectMethodName().Literal
// Skip optional (params)
if p.match(token.LPAREN) {
for !p.atAny(token.RPAREN, token.EOF) {
p.advance()
}
p.match(token.RPAREN)
}
methodName := "_" + propName
if p.match(token.METHOD) {
methodName = p.expectMethodName().Literal
if p.match(token.LPAREN) {
for !p.atAny(token.RPAREN, token.EOF) {
p.advance()
}
p.match(token.RPAREN)
}
}
// Skip INLINE + rest of line
p.skipClassInline()
p.expectEndOfStmt()
return &ast.MethodDecl{
MethodPos: pos,
Name: methodName,
IsAssign: true,
AccessName: propName,
EndPos: pos,
}
}
// parseMethodDecl parses standalone: METHOD [FUNCTION|PROCEDURE] name(...) CLASS classname
func (p *Parser) parseMethodDecl() *ast.MethodDecl {
methodPos := p.expect(token.METHOD).Pos
// Skip optional FUNCTION/PROCEDURE qualifier
if p.current.Kind == token.FUNCTION_KW || p.current.Kind == token.PROCEDURE {
p.advance()
}
name := p.expectMethodName().Literal
var params []*ast.ParamDecl
if p.match(token.LPAREN) {
params = p.parseParamList()
p.expect(token.RPAREN)
}
var className string
if p.match(token.CLASS) {
className = p.expect(token.IDENT).Literal
} else if p.lastClassName != "" {
// Implicit binding to the most recent `CREATE CLASS` / `CLASS`
// declaration — classic Clipper/Harbour form. Real code uses
// `CREATE CLASS CLSX ... ENDCLASS` followed by a bare
// `METHOD TestX() ... RETURN Self` without restating the class.
className = p.lastClassName
}
p.expectEndOfStmt()
// Declarations
var decls []ast.Decl
p.skipNewlines()
for p.atAny(token.LOCAL, token.STATIC) {
decls = append(decls, p.parseVarDecl())
p.skipNewlines()
}
// Body
body := p.parseStmtBlock(token.RETURN, token.FUNCTION_KW, token.PROCEDURE, token.CLASS, token.METHOD, token.EOF)
var endPos token.Position
if p.current.Kind == token.RETURN {
retStmt := p.parseReturn()
body = append(body, retStmt)
endPos = retStmt.Pos()
} else {
endPos = p.current.Pos
}
return &ast.MethodDecl{
MethodPos: methodPos,
Name: name,
ClassName: className,
Params: params,
Decls: decls,
Body: body,
EndPos: endPos,
}
}
// --- Statement parsing ---
// isIdentSuffix reports whether the given token could follow an
// identifier, signalling that a keyword on the prior slot is being
// used as a variable name (array index, method call, assignment,
// etc.). Used by parseStmtBlock to avoid prematurely ending a
// statement block when a keyword identifier appears.
func isIdentSuffix(k token.Kind) bool {
switch k {
case token.LBRACKET, token.ASSIGN, token.PLUSEQ, token.MINUSEQ,
token.STAREQ, token.SLASHEQ, token.PERCENTEQ, token.POWEREQ,
token.INC, token.DEC, token.COLON, token.DOT:
return true
}
return false
}
// parseStmtBlock parses statements until one of the stop tokens.
func (p *Parser) parseStmtBlock(stopTokens ...token.Kind) []ast.Stmt {
var stmts []ast.Stmt
for {
p.skipNewlines()
if p.current.Kind == token.EOF {
break
}
for _, stop := range stopTokens {
if p.current.Kind == stop {
// Don't stop at RETURN if it's used as variable: return := ...
if stop == token.RETURN &&
p.peekAt(1) == token.ASSIGN {
break // continue parsing as statement
}
// Don't stop at a keyword-used-as-identifier. Harbour
// allows keywords like CASE/DO as variable names, so
// `case[idx]`, `case := 1`, `case:method()` are
// expression statements, not new block arms. Peek the
// next token for identifier-ish suffixes.
if isIdentSuffix(p.peekAt(1)) {
break // treat current token as identifier, parse as stmt
}
return stmts
}
}
stmt := p.parseStmt()
if stmt != nil {
stmts = append(stmts, stmt)
}
}
return stmts
}
func (p *Parser) parseStmt() ast.Stmt {
// Registry lookup — O(1) dispatch
if fn := p.lookupStmtParser(); fn != nil {
return fn(p)
}
// IDENT-based commands (xBase multi-word: COPY, SORT, etc.)
if p.current.Kind == token.IDENT {
return p.parseIdentStmt()
}
// Multi-assign: a, b := expr
if p.looksLikeMultiAssign() {
return p.parseMultiAssign()
}
// Default: expression statement
return p.parseExprStmt()
}
// parseIdentStmt handles IDENT-based commands (xBase multi-word: COPY, SORT, etc.)
func (p *Parser) parseIdentStmt() ast.Stmt {
upper := p.currentUpper()
// WITH TIMEOUT → timeout context
if upper == "WITH" && p.peekAt(1) == token.TIMEOUT_KW {
return p.parseWithTimeout()
}
// xBase commands that consume entire line. These are silent no-ops
// for now — they have no RTL backend, so std.ch deliberately omits
// rules for them. ERASE / RENAME / LOCATE / CONTINUE / COMMIT /
// CLOSE / REINDEX / PACK / ZAP / UNLOCK / KEYBOARD / RUN are now
// rewritten by compiler/pp/std.ch into function calls before the
// parser sees them.
switch upper {
case "LABEL", "REPORT", "ACCEPT", "INPUT",
"RELEASE", "SAVE", "RESTORE",
"DIR", "STORE", "NOTE", "TEXT", "ENDTEXT",
"WITH", "CLEAR":
p.advance()
for p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
p.advance()
}
p.expectEndOfStmt()
return &ast.ExprStmt{X: &ast.LiteralExpr{Kind: token.NIL_LIT, Value: "NIL"}}
case "FIVE_GODUMP__":
// GoDump is a Decl, wrap as ExprStmt for statement context
p.advance() // consume FIVE_GODUMP__
idx := 0
if p.current.Kind == token.INT {
fmt.Sscanf(p.current.Literal, "%d", &idx)
p.advance()
}
p.expectEndOfStmt()
// Store as nil statement — gengo handles GoDumpDecl at file level
return &ast.ExprStmt{X: &ast.LiteralExpr{Kind: token.NIL_LIT, Value: "NIL"}}
}
// Multi-assign check: a, b := expr
if p.looksLikeMultiAssign() {
return p.parseMultiAssign()
}
// Default: expression statement (function call, assignment, etc.)
return p.parseExprStmt()
}
func (p *Parser) parseExprStmt() ast.Stmt {
// READ [SAVE] [MSG AT ...] [MSG COLOR ...] — special case
if p.current.Kind == token.IDENT && p.currentUpper() == "READ" {
pos := p.advance().Pos
save := false
if p.current.Kind == token.IDENT && p.currentUpper() == "SAVE" {
save = true
p.advance()
}
// Skip optional clauses: MSG AT row,col,col2 / MSG COLOR "..." etc.
if p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
p.skipToEndOfLine()
}
p.expectEndOfStmt()
return &ast.ReadCmd{ReadPos: pos, Save: save}
}
// TRY / CATCH [oErr] / END — Harbour extension, maps to BEGIN SEQUENCE / RECOVER
if p.current.Kind == token.IDENT && p.currentUpper() == "TRY" {
return p.parseTryCatch()
}
// xBase commands that consume entire line — duplicate of the switch
// in parseIdentStmt(). The keyword set is kept in sync; std.ch covers
// ERASE/RENAME/LOCATE/CONTINUE/COMMIT/CLOSE/REINDEX/PACK/ZAP/UNLOCK/
// KEYBOARD/RUN, so they're absent here.
if p.current.Kind == token.IDENT {
// WITH TIMEOUT n / body / ENDWITH
if p.currentUpper() == "WITH" &&
p.peekAt(1) == token.TIMEOUT_KW {
return p.parseWithTimeout()
}
// Keep this list IN SYNC with parseIdentStmt's switch above.
// COPY/SORT/COUNT/SUM/AVERAGE/TOTAL/UPDATE/JOIN/DISPLAY/LIST
// are no longer here — std.ch rewrites them to function calls
// before the parser sees them. Leaving them in the fallback
// would silently no-op a typo'd version (e.g. `COPYY TO ...`)
// against the user's expectation.
switch p.currentUpper() {
case "LABEL", "REPORT", "ACCEPT", "INPUT",
"RELEASE", "SAVE", "RESTORE",
"DIR", "STORE", "NOTE", "TEXT", "ENDTEXT",
"WITH", "CLEAR":
// Consume entire line — these are complex multi-word commands
p.advance()
for p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
p.advance()
}
p.expectEndOfStmt()
return &ast.ExprStmt{X: &ast.LiteralExpr{Kind: token.NIL_LIT, Value: "NIL"}}
}
}
expr := p.parseExpr()
// ch <- value (channel send)
if p.at(token.ARROW_LEFT) {
pos := p.advance().Pos
val := p.parseExpr()
p.expectEndOfStmt()
return &ast.ChanSendStmt{ChanPos: pos, Chan: expr, Value: val}
}
p.expectEndOfStmt()
return &ast.ExprStmt{X: expr}
}
// --- Control flow ---
func (p *Parser) parseIf() *ast.IfStmt {
ifPos := p.expect(token.IF).Pos
cond := p.parseExpr()
p.expectEndOfStmt()
body := p.parseStmtBlock(token.ELSEIF, token.ELSE, token.ENDIF, token.END)
var elseIfs []*ast.ElseIfClause
for p.current.Kind == token.ELSEIF {
eiPos := p.advance().Pos
eiCond := p.parseExpr()
p.expectEndOfStmt()
eiBody := p.parseStmtBlock(token.ELSEIF, token.ELSE, token.ENDIF, token.END)
elseIfs = append(elseIfs, &ast.ElseIfClause{
ElseIfPos: eiPos,
Cond: eiCond,
Body: eiBody,
})
}
var elseBody []ast.Stmt
if p.match(token.ELSE) {
p.expectEndOfStmt()
elseBody = p.parseStmtBlock(token.ENDIF, token.END)
}
endPos := p.current.Pos
if !p.match(token.ENDIF) {
p.match(token.END) // alternative
}
p.expectEndOfStmt()
return &ast.IfStmt{
IfPos: ifPos,
Cond: cond,
Body: body,
ElseIfs: elseIfs,
ElseBody: elseBody,
EndPos: endPos,
}
}
// looksLikeIIF checks if IF( starts an IIF-style call: IF(cond, true, false)
// by scanning for commas inside the parenthesized expression.
func (p *Parser) looksLikeIIF() bool {
// Start after IF token; expect ( at p.pos+1
if p.pos+1 >= len(p.tokens) || p.peekAt(1) != token.LPAREN {
return false
}
depth := 0
commas := 0
for i := p.pos + 2; i < len(p.tokens); i++ { // start INSIDE the parens
switch p.tokens[i].Kind {
case token.LPAREN, token.LBRACE, token.LBRACKET:
depth++
case token.RPAREN:
if depth == 0 {
return commas >= 2
}
depth--
case token.RBRACE, token.RBRACKET:
if depth > 0 {
depth--
}
case token.COMMA:
if depth == 0 {
commas++
}
case token.NEWLINE, token.EOF:
return false
}
}
return false
}
// parseDoProc: DO funcname [WITH arg1, arg2, ...]
func (p *Parser) parseDoProc() ast.Stmt {
p.advance() // skip DO
funcName := p.expectMethodName().Literal
var args []ast.Expr
if p.current.Kind == token.WITH {
p.advance() // skip WITH
for {
args = append(args, p.parseExpr())
if !p.match(token.COMMA) {
break
}
}
}
p.expectEndOfStmt()
return &ast.ExprStmt{X: &ast.CallExpr{
Func: &ast.IdentExpr{Name: funcName},
Args: args,
}}
}
func (p *Parser) parseDoWhile() *ast.DoWhileStmt {
var doPos token.Position
if p.current.Kind == token.DO {
doPos = p.advance().Pos
p.expect(token.WHILE)
} else {
// Bare WHILE (Clipper compatibility)
doPos = p.expect(token.WHILE).Pos
}
cond := p.parseExpr()
p.expectEndOfStmt()
body := p.parseStmtBlock(token.ENDDO, token.END)
endPos := p.current.Pos
if !p.match(token.ENDDO) {
p.match(token.END)
}
p.expectEndOfStmt()
return &ast.DoWhileStmt{DoPos: doPos, Cond: cond, Body: body, EndPos: endPos}
}
func (p *Parser) parseFor() ast.Stmt {
forPos := p.expect(token.FOR).Pos
// FOR EACH var IN collection
if p.match(token.EACH) {
return p.parseForEach(forPos)
}
// FOR var := start TO end [STEP step]
// Variable can be aliased: M->TEST or simple: i
varTok := p.expectMethodName()
varName := varTok.Literal
// Handle M->varname
if p.at(token.ARROW) {
p.advance() // skip ->
fieldTok := p.expectMethodName()
varName = fieldTok.Literal
}
p.expect(token.ASSIGN) // :=
start := p.parseExpr()
p.expect(token.TO)
toExpr := p.parseExpr()
var step ast.Expr
if p.match(token.STEP) {
step = p.parseExpr()
}
p.expectEndOfStmt()
body := p.parseStmtBlock(token.NEXT, token.END)
nextPos := p.current.Pos
if !p.match(token.NEXT) {
p.match(token.END)
}
// Skip optional counter variable after NEXT (e.g. NEXT nVar)
if p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
p.skipToEndOfLine()
}
p.expectEndOfStmt()
return &ast.ForStmt{
ForPos: forPos, Var: varName, Start: start, To: toExpr,
Step: step, Body: body, NextPos: nextPos,
}
}
func (p *Parser) parseForEach(forPos token.Position) *ast.ForEachStmt {
varName := p.expect(token.IDENT).Literal
// Multi-variable FOR EACH: FOR EACH a, b, c IN x, y, z
// Skip extra variables — use only first var and first collection
var extraVars []string
for p.match(token.COMMA) {
extraVars = append(extraVars, p.expect(token.IDENT).Literal)
}
p.expect(token.IN)
collection := p.parseExpr()
// Skip extra collections
for p.match(token.COMMA) {
p.parseExpr() // consume and discard
}
descend := false
if p.current.Kind == token.DESCENDING {
p.advance()
descend = true
}
p.expectEndOfStmt()
body := p.parseStmtBlock(token.NEXT, token.END)
nextPos := p.current.Pos
if !p.match(token.NEXT) {
p.match(token.END)
}
// Skip optional counter variable after NEXT
if p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
p.skipToEndOfLine()
}
p.expectEndOfStmt()
return &ast.ForEachStmt{
ForPos: forPos, Var: varName, Collection: collection,
Descend: descend, Body: body, NextPos: nextPos,
}
}
// parseDoCase: DO CASE / CASE cond / OTHERWISE / ENDCASE
// Harbour: equivalent to IF/ELSEIF/ELSE chain
func (p *Parser) parseDoCase() *ast.IfStmt {
doPos := p.expect(token.DO).Pos
p.expect(token.CASE) // consume CASE after DO
p.expectEndOfStmt()
p.skipNewlines()
// First CASE
if p.current.Kind != token.CASE {
p.error("expected CASE after DO CASE")
return &ast.IfStmt{IfPos: doPos, EndPos: doPos}
}
p.advance() // consume CASE
cond := p.parseExpr()
p.expectEndOfStmt()
body := p.parseStmtBlock(token.CASE, token.OTHERWISE, token.ENDCASE, token.END)
// Build as IfStmt with ElseIfs
var elseIfs []*ast.ElseIfClause
for p.current.Kind == token.CASE {
eiPos := p.advance().Pos
eiCond := p.parseExpr()
p.expectEndOfStmt()
eiBody := p.parseStmtBlock(token.CASE, token.OTHERWISE, token.ENDCASE, token.END)
elseIfs = append(elseIfs, &ast.ElseIfClause{
ElseIfPos: eiPos,
Cond: eiCond,
Body: eiBody,
})
}
var elseBody []ast.Stmt
if p.match(token.OTHERWISE) {
p.expectEndOfStmt()
elseBody = p.parseStmtBlock(token.ENDCASE, token.END)
}
endPos := p.current.Pos
if !p.match(token.ENDCASE) {
p.match(token.END)
}
p.expectEndOfStmt()
return &ast.IfStmt{
IfPos: doPos,
Cond: cond,
Body: body,
ElseIfs: elseIfs,
ElseBody: elseBody,
EndPos: endPos,
}
}
func (p *Parser) parseSwitch() *ast.SwitchStmt {
switchPos := p.expect(token.SWITCH).Pos
expr := p.parseExpr()
p.expectEndOfStmt()
var cases []*ast.CaseClause
var otherwise []ast.Stmt
p.skipNewlines()
for p.current.Kind == token.CASE {
casePos := p.advance().Pos
val := p.parseExpr()
p.expectEndOfStmt()
caseBody := p.parseStmtBlock(token.CASE, token.OTHERWISE, token.ENDSWITCH, token.ENDCASE, token.END)
cases = append(cases, &ast.CaseClause{CasePos: casePos, Value: val, Body: caseBody})
}
if p.match(token.OTHERWISE) {
p.expectEndOfStmt()
otherwise = p.parseStmtBlock(token.ENDSWITCH, token.ENDCASE, token.END)
}
endPos := p.current.Pos
if !p.match(token.ENDSWITCH) {
if !p.match(token.ENDCASE) {
p.match(token.END)
}
}
p.expectEndOfStmt()
return &ast.SwitchStmt{SwitchPos: switchPos, Expr: expr, Cases: cases, Otherwise: otherwise, EndPos: endPos}
}
func (p *Parser) parseBeginSequence() *ast.SeqStmt {
beginPos := p.expect(token.BEGIN).Pos
p.expect(token.SEQUENCE)
p.expectEndOfStmt()
body := p.parseStmtBlock(token.RECOVER, token.END)
var recoverVar string
var recoverBody []ast.Stmt
if p.match(token.RECOVER) {
if p.match(token.USING) {
recoverVar = p.expect(token.IDENT).Literal
}
p.expectEndOfStmt()
recoverBody = p.parseStmtBlock(token.END)
}
endPos := p.current.Pos
p.match(token.END)
p.match(token.SEQUENCE) // optional: END SEQUENCE
p.expectEndOfStmt()
return &ast.SeqStmt{
BeginPos: beginPos, Body: body,
RecoverVar: recoverVar, RecoverBody: recoverBody,
EndPos: endPos,
}
}
// parseTryCatch: TRY ... CATCH [oErr] ... END — maps to SeqStmt
func (p *Parser) parseTryCatch() *ast.SeqStmt {
beginPos := p.advance().Pos // consume TRY
p.expectEndOfStmt()
// Parse body until CATCH or END
var body []ast.Stmt
for !p.atAny(token.EOF) {
if p.current.Kind == token.IDENT && p.currentUpper() == "CATCH" {
break
}
if p.current.Kind == token.END {
break
}
body = append(body, p.parseStmt())
p.skipNewlines()
}
var recoverVar string
var recoverBody []ast.Stmt
if p.current.Kind == token.IDENT && p.currentUpper() == "CATCH" {
p.advance() // consume CATCH
if p.current.Kind == token.IDENT && p.current.Kind != token.NEWLINE {
recoverVar = p.advance().Literal
}
p.expectEndOfStmt()
recoverBody = p.parseStmtBlock(token.END)
}
endPos := p.current.Pos
p.match(token.END)
if p.current.Kind == token.IDENT && p.currentUpper() == "TRY" {
p.advance() // END TRY
}
p.expectEndOfStmt()
return &ast.SeqStmt{
BeginPos: beginPos, Body: body,
RecoverVar: recoverVar, RecoverBody: recoverBody,
EndPos: endPos,
}
}
func (p *Parser) parseReturn() *ast.ReturnStmt {
pos := p.expect(token.RETURN).Pos
var val ast.Expr
var vals []ast.Expr
if p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
val = p.parseExpr()
// Multi-return: RETURN a, b, c
if p.match(token.COMMA) {
vals = append(vals, val)
vals = append(vals, p.parseExpr())
for p.match(token.COMMA) {
vals = append(vals, p.parseExpr())
}
val = vals[0] // keep first for backward compat
}
}
p.expectEndOfStmt()
return &ast.ReturnStmt{ReturnPos: pos, Value: val, Values: vals}
}
func (p *Parser) parseQOut(isQQ bool) *ast.QOutStmt {
pos := p.advance().Pos // consume ? or ??
var exprs []ast.Expr
if p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
exprs = append(exprs, p.parseExpr())
for p.match(token.COMMA) {
exprs = append(exprs, p.parseExpr())
}
}
p.expectEndOfStmt()
return &ast.QOutStmt{QPos: pos, IsQQ: isQQ, Exprs: exprs}
}
func (p *Parser) parsePrivatePublic(scope ast.VarScope) ast.Stmt {
tok := p.advance()
var vars []*ast.VarInit
for {
// Handle &macro in PRIVATE/PUBLIC list
if p.at(token.AMPERSAND) {
macroExpr := p.parseMacro()
var init ast.Expr
if p.match(token.ASSIGN) {
init = p.parseExpr()
}
name := "macro"
if me, ok := macroExpr.(*ast.MacroExpr); ok {
if id, ok2 := me.Expr.(*ast.IdentExpr); ok2 {
name = id.Name
}
}
vars = append(vars, &ast.VarInit{NamePos: macroExpr.Pos(), Name: name, Init: init})
if !p.match(token.COMMA) {
break
}
continue
}
name := p.expectMethodName() // allow keywords as var names (MEMVAR, etc.)
var init ast.Expr
// Skip AS type declaration
if p.match(token.AS) {
for p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF &&
p.current.Kind != token.ASSIGN && p.current.Kind != token.COMMA {
p.advance()
}
}
if p.match(token.ASSIGN) {
init = p.parseExpr()
}
vars = append(vars, &ast.VarInit{NamePos: name.Pos, Name: name.Literal, Init: init})
if !p.match(token.COMMA) {
break
}
}
p.expectEndOfStmt()
return &ast.VarDecl{DeclPos: tok.Pos, Scope: scope, Vars: vars}
}
// --- xBase commands ---
func (p *Parser) parseUse() *ast.UseCmd {
pos := p.expect(token.USE).Pos
var file ast.Expr
var via, alias string
var aliasExprNode ast.Expr
var shared, readOnly bool
// USE without args = close
if p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
// `USE &cFile` / `USE &(expr)` — macro expression yields the
// filename at runtime. Harbour uses this heavily for data-
// driven apps (USE &cTable INDEX &cIndex ...).
if p.at(token.AMPERSAND) {
file = p.parseMacro()
} else if p.at(token.IDENT) {
// Bare ident as filename: USE myfile / USE myfile.dbf / USE myfile NEW
// In Harbour, USE <name> treats name as a filename string, not a variable.
name := p.advance().Literal
if p.at(token.DOT) && (p.peekAt(1) == token.IDENT || p.peekAt(1) == token.INT) {
p.advance() // skip DOT
ext := p.advance().Literal
name = name + "." + ext
}
file = &ast.LiteralExpr{ValuePos: pos, Kind: token.STRING, Value: name}
} else {
file = p.parseExpr()
p.consumeFileExtension(file)
}
}
// Parse optional clauses: VIA, ALIAS, EXCLUSIVE, SHARED, NEW, READONLY
for p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
if p.current.Kind == token.IDENT {
upper := p.currentUpper()
if upper == "VIA" {
p.advance()
if p.at(token.STRING) {
via = p.current.Literal
p.advance()
} else {
via = p.expectMethodName().Literal
}
continue
}
if upper == "ALIAS" {
p.advance()
if p.at(token.AMPERSAND) {
// `ALIAS &cAlias` / `&cAlias.1` — compute the alias
// name at runtime via macro evaluation.
aliasExprNode = p.parseMacro()
} else if p.at(token.LPAREN) {
// ALIAS ( expr ) — parenthesized alias expression (runtime)
p.advance() // skip (
aliasExpr := p.parseExpr()
p.expect(token.RPAREN)
if lit, ok := aliasExpr.(*ast.LiteralExpr); ok && lit.Kind == token.STRING {
alias = lit.Value // constant string — store directly
} else {
aliasExprNode = aliasExpr // dynamic — evaluate at runtime
}
} else {
alias = p.expectMethodName().Literal
}
continue
}
if upper == "SHARED" {
shared = true
p.advance()
continue
}
if upper == "READONLY" {
readOnly = true
p.advance()
continue
}
if upper == "EXCLUSIVE" || upper == "NEW" || upper == "ADDITIVE" {
p.advance()
continue
}
if upper == "INDEX" {
// INDEX file1[, file2, ...] — skip to EOL
p.skipToEndOfLine()
break
}
}
if p.current.Kind == token.ALIAS {
p.advance()
if p.at(token.AMPERSAND) {
aliasExprNode = p.parseMacro()
} else if p.at(token.LPAREN) {
// ALIAS ( expr ) — parenthesized alias expression
p.advance()
ae := p.parseExpr()
p.expect(token.RPAREN)
if lit, ok := ae.(*ast.LiteralExpr); ok && lit.Kind == token.STRING {
alias = lit.Value
} else {
aliasExprNode = ae
}
} else {
alias = p.expectMethodName().Literal
}
continue
}
if p.current.Kind == token.INDEX {
// INDEX file1, file2, ... — skip to EOL
p.skipToEndOfLine()
break
}
break
}
p.expectEndOfStmt()
return &ast.UseCmd{UsePos: pos, File: file, Via: via, Alias: alias, AliasExpr: aliasExprNode, Shared: shared, ReadOnly: readOnly}
}
func (p *Parser) parseSelect() *ast.SelectCmd {
pos := p.expect(token.SELECT).Pos
// Classic Clipper/Harbour semantics: `SELECT <alias>` treats a bare
// identifier as a literal alias name (string), not as an expression.
// Wrap in parens to force expression evaluation — e.g. `SELECT (n)`
// where n is a local holding an area number or alias name.
//
// Without this, unresolved identifiers fell back to PushMemvar(name)
// which returned NIL, and _wa.Select("") quietly allocated a fresh
// empty workarea, stranding the caller's real data in the previous
// slot. Visible symptom: `SELECT ALTSRC` inside SqlAlterAddColumn
// picked up a phantom area and the row-copy loop saw EOF from the
// first iteration (no rows migrated).
var area ast.Expr
if p.current.Kind == token.IDENT {
// Peek: only treat bare IDENT as literal alias when it's the
// entire argument (next token ends the statement). `SELECT x:y`
// or `SELECT f()` must parse as expressions so the dispatch
// below still routes through parseExpr.
next := p.peekAt(1)
if next == token.NEWLINE || next == token.SEMICOLON || next == token.EOF {
tok := p.advance()
area = &ast.LiteralExpr{ValuePos: tok.Pos, Kind: token.STRING, Value: tok.Literal}
}
}
if area == nil {
area = p.parseExpr()
}
p.expectEndOfStmt()
return &ast.SelectCmd{SelectPos: pos, Area: area}
}
func (p *Parser) parseGo() *ast.GoCmd {
pos := p.advance().Pos // GO or GOTO
var dir string
var recNo ast.Expr
switch p.current.Kind {
case token.TOP:
dir = "TOP"
p.advance()
case token.BOTTOM:
dir = "BOTTOM"
p.advance()
default:
recNo = p.parseExpr()
}
p.expectEndOfStmt()
return &ast.GoCmd{GoPos: pos, Direction: dir, RecNo: recNo}
}
func (p *Parser) parseSkip() *ast.SkipCmd {
pos := p.expect(token.SKIP_KW).Pos
var count ast.Expr
if p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
count = p.parseExpr()
}
p.expectEndOfStmt()
return &ast.SkipCmd{SkipPos: pos, Count: count}
}
func (p *Parser) parseSeek() *ast.SeekCmd {
pos := p.expect(token.SEEK).Pos
key := p.parseExpr()
softSeek := false
if p.current.Kind == token.SOFTSEEK {
p.advance()
softSeek = true
}
p.expectEndOfStmt()
return &ast.SeekCmd{SeekPos: pos, Key: key, SoftSeek: softSeek}
}
func (p *Parser) parseReplace() *ast.ReplaceCmd {
pos := p.expect(token.REPLACE).Pos
var fields []ast.ReplaceField
for {
field := p.parseExpr()
p.expect(token.WITH)
value := p.parseExpr()
fields = append(fields, ast.ReplaceField{Field: field, Value: value})
if !p.match(token.COMMA) {
break
}
}
p.expectEndOfStmt()
return &ast.ReplaceCmd{ReplacePos: pos, Fields: fields}
}
func (p *Parser) parseAppend() *ast.AppendCmd {
pos := p.expect(token.APPEND).Pos
if p.match(token.FROM) {
// APPEND FROM filename [DELIMITED|SDF|VIA ...] — skip to EOL
p.skipToEndOfLine()
p.expectEndOfStmt()
return &ast.AppendCmd{AppendPos: pos}
}
p.expect(token.BLANK)
p.expectEndOfStmt()
return &ast.AppendCmd{AppendPos: pos}
}
func (p *Parser) parseIndex() *ast.IndexCmd {
pos := p.expect(token.INDEX).Pos
p.expect(token.ON)
keyExpr := p.parseExpr()
// INDEX ON expr [TAG tagname] TO file [FOR cond] [UNIQUE] [DESCENDING]
var fileExpr ast.Expr
var tagName string
if p.match(token.TO) {
fileExpr = p.parseExpr()
p.consumeFileExtension(fileExpr)
} else if p.current.Kind == token.IDENT && p.currentUpper() == "TAG" {
p.advance() // skip TAG
tagName = p.expectMethodName().Literal // capture tag name
if p.match(token.TO) {
fileExpr = p.parseExpr()
} else {
// TAG without TO: use tag name as file name
fileExpr = &ast.IdentExpr{NamePos: p.current.Pos, Name: tagName}
}
} else {
fileExpr = p.parseExpr() // fallback
}
var forCond ast.Expr
unique := false
descending := false
for {
if p.match(token.FOR) {
forCond = p.parseExpr()
} else if p.match(token.UNIQUE) {
unique = true
} else if p.match(token.DESCENDING) {
descending = true
} else {
break
}
}
p.expectEndOfStmt()
return &ast.IndexCmd{
IndexPos: pos, KeyExpr: keyExpr, File: fileExpr,
ForCond: forCond, TagName: tagName, Unique: unique, Descending: descending,
}
}
func (p *Parser) parseSet() *ast.SetCmd {
pos := p.expect(token.SET).Pos
// Accept any token as SET keyword (COLOR, KEY, ORDER, FILTER, etc. may be keyword tokens)
setting := p.expectMethodName().Literal
var expr ast.Expr
var extra string
// SET commands: consume everything until end of line.
// Boolean toggles: SET DELETED ON/OFF, SET EXACT ON/OFF, etc.
// Value settings: SET FILTER TO expr, SET ORDER TO n, SET DATE TO fmt
upperSetting := strings.ToUpper(setting)
// Check for ON/OFF boolean toggle
booleanSets := map[string]bool{
"DELETED": true, "EXACT": true, "SOFTSEEK": true, "EXCLUSIVE": true,
"FIXED": true, "CANCEL": true, "BELL": true, "CONFIRM": true,
"INSERT": true, "ESCAPE": true, "WRAP": true, "INTENSITY": true,
"SCOREBOARD": true, "CONSOLE": true, "ALTERNATE": true, "PRINTER": true,
}
if booleanSets[upperSetting] {
if p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
extra = strings.ToUpper(p.expectMethodName().Literal)
}
} else if p.match(token.TO) {
if upperSetting == "FILTER" || upperSetting == "RELATION" || upperSetting == "ORDER" || upperSetting == "INDEX" {
if p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
expr = p.parseExpr()
// SET INDEX TO a, b, c — collect comma-separated file names
// into a single string literal "a,b,c" for gengo to split.
if upperSetting == "INDEX" {
getName := func(e ast.Expr) string {
switch v := e.(type) {
case *ast.IdentExpr:
return v.Name
case *ast.LiteralExpr:
return v.Value
default:
return ""
}
}
combined := getName(expr)
for p.current.Kind == token.COMMA {
p.advance()
if p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
next := p.parseExpr()
combined += "," + getName(next)
}
}
if strings.Contains(combined, ",") {
expr = &ast.LiteralExpr{
Value: combined,
Kind: token.STRING,
ValuePos: expr.Pos(),
}
}
}
}
if p.current.Kind == token.INTO {
p.advance()
extra = p.expectMethodName().Literal
}
} else if upperSetting == "DATE" || upperSetting == "DECIMALS" || upperSetting == "EPOCH" {
if p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
expr = p.parseExpr()
}
}
}
// Consume remaining tokens (for COLOR TO, KEY TO, etc.)
for p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
p.advance()
}
p.expectEndOfStmt()
return &ast.SetCmd{SetPos: pos, Setting: setting, Expr: expr, Extra: extra}
}
// parseAtCmd parses @ row, col SAY/GET/PROMPT commands.
func (p *Parser) parseAtCmd() ast.Stmt {
pos := p.advance().Pos // consume @
row := p.parseExpr()
p.expect(token.COMMA)
col := p.parseExpr()
// Determine sub-command: SAY, GET, PROMPT
if p.current.Kind == token.IDENT {
switch p.currentUpper() {
case "SAY":
return p.parseAtSay(pos, row, col)
case "GET":
return p.parseAtGet(pos, row, col)
case "PROMPT":
return p.parseAtPrompt(pos, row, col)
case "CLEAR":
// @ row, col CLEAR [TO row2, col2] — clear region
p.advance() // skip CLEAR
if p.match(token.TO) {
p.parseExpr() // row2
p.expect(token.COMMA)
p.parseExpr() // col2
}
p.expectEndOfStmt()
return &ast.ExprStmt{X: &ast.CallExpr{
Func: &ast.IdentExpr{Name: "SetPos"},
Args: []ast.Expr{row, col},
}}
}
}
// @ row, col TO row2, col2 [DOUBLE] — box drawing
if p.match(token.TO) {
row2 := p.parseExpr()
p.expect(token.COMMA)
col2 := p.parseExpr()
// Skip optional DOUBLE keyword or other modifiers
for p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
p.advance()
}
p.expectEndOfStmt()
return &ast.ExprStmt{X: &ast.CallExpr{
Func: &ast.IdentExpr{Name: "DispBox"},
Args: []ast.Expr{row, col, row2, col2},
}}
}
// Bare @ row, col — just position cursor
for p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
p.advance() // skip any remaining tokens
}
p.expectEndOfStmt()
return &ast.ExprStmt{X: &ast.CallExpr{
Func: &ast.IdentExpr{Name: "SetPos"},
Args: []ast.Expr{row, col},
}}
}
func (p *Parser) parseAtSay(pos token.Position, row, col ast.Expr) ast.Stmt {
p.advance() // consume SAY
sayExpr := p.parseExpr()
// Check for GET after SAY
if p.current.Kind == token.IDENT && p.currentUpper() == "GET" {
return p.parseAtSayGet(pos, row, col, sayExpr)
}
// PICTURE clause
var pic ast.Expr
if p.current.Kind == token.IDENT && p.currentUpper() == "PICTURE" {
p.advance()
pic = p.parseExpr()
}
p.expectEndOfStmt()
return &ast.AtSayCmd{AtPos: pos, Row: row, Col: col, SayExpr: sayExpr, Picture: pic}
}
func (p *Parser) parseAtGet(pos token.Position, row, col ast.Expr) *ast.AtGetCmd {
p.advance() // consume GET
varExpr := p.parseExpr()
varName := ""
if ident, ok := varExpr.(*ast.IdentExpr); ok {
varName = ident.Name
}
var pic, valid, when ast.Expr
for p.current.Kind == token.IDENT {
switch p.currentUpper() {
case "PICTURE":
p.advance()
pic = p.parseExpr()
case "VALID":
p.advance()
valid = p.parseExpr()
case "WHEN":
p.advance()
when = p.parseExpr()
case "RANGE":
// RANGE low, high — skip both values
p.advance()
p.parseExpr() // low
p.expect(token.COMMA)
p.parseExpr() // high
case "COLOR", "COLOUR", "MESSAGE", "SEND", "GUISEND",
"CAPTION", "CARGO", "COLORSPEC":
// Skip keyword + value
p.advance()
p.parseExpr()
default:
goto done
}
}
done:
p.expectEndOfStmt()
return &ast.AtGetCmd{AtPos: pos, Row: row, Col: col, Var: varExpr, VarName: varName, Picture: pic, Valid: valid, When: when}
}
func (p *Parser) parseAtSayGet(pos token.Position, row, col ast.Expr, sayExpr ast.Expr) *ast.AtSayGetCmd {
p.advance() // consume GET
varExpr := p.parseExpr()
varName := ""
if ident, ok := varExpr.(*ast.IdentExpr); ok {
varName = ident.Name
}
var pic, valid, when ast.Expr
for p.current.Kind == token.IDENT {
switch p.currentUpper() {
case "PICTURE":
p.advance()
pic = p.parseExpr()
case "VALID":
p.advance()
valid = p.parseExpr()
case "WHEN":
p.advance()
when = p.parseExpr()
case "RANGE":
p.advance()
p.parseExpr()
p.expect(token.COMMA)
p.parseExpr()
case "COLOR", "COLOUR", "MESSAGE", "SEND", "GUISEND",
"CAPTION", "CARGO", "COLORSPEC":
p.advance()
p.parseExpr()
default:
goto done
}
}
done:
p.expectEndOfStmt()
return &ast.AtSayGetCmd{AtPos: pos, Row: row, Col: col, SayExpr: sayExpr, Var: varExpr, VarName: varName, Picture: pic, Valid: valid, When: when}
}
func (p *Parser) parseAtPrompt(pos token.Position, row, col ast.Expr) ast.Stmt {
p.advance() // consume PROMPT
prompt := p.parseExpr()
var msg ast.Expr
if p.current.Kind == token.IDENT && p.currentUpper() == "MESSAGE" {
p.advance()
msg = p.parseExpr()
}
p.expectEndOfStmt()
// Emit as: __AtPrompt(row, col, prompt [, msg])
args := []ast.Expr{row, col, prompt}
if msg != nil {
args = append(args, msg)
}
return &ast.ExprStmt{X: &ast.CallExpr{
Func: &ast.IdentExpr{Name: "__AtPrompt"},
Args: args,
}}
}
// === Five Go Extension Parsers ===
// looksLikeMultiAssign checks strictly: IDENT , IDENT [, IDENT...] :=
// Each token between start and := must be IDENT or COMMA only (no [ ( : etc.)
func (p *Parser) looksLikeMultiAssign() bool {
// Must start with IDENT (or keyword-as-ident)
if p.current.Kind != token.IDENT && p.current.Literal == "" {
return false
}
// Next token must be COMMA for multi-assign
if p.pos+1 >= len(p.tokens) || p.peekAt(1) != token.COMMA {
return false
}
// Scan from after first IDENT: COMMA, IDENT, COMMA, IDENT, ..., ASSIGN
expectComma := true // first IDENT already consumed, expect COMMA next
for i := p.pos + 1; i < len(p.tokens); i++ {
tk := p.tokens[i]
if tk.Kind == token.ASSIGN {
return expectComma == true // last was IDENT (expectComma=true), then :=
}
if tk.Kind == token.NEWLINE || tk.Kind == token.EOF {
return false
}
if expectComma {
if tk.Kind != token.COMMA {
return false
}
expectComma = false
} else {
// Expect IDENT or keyword-as-ident (including "_")
if tk.Kind != token.IDENT && tk.Literal == "" {
return false
}
expectComma = true
}
}
return false
}
// parseMultiAssign: a, b := Func() or a, b, c := x, y, z or _, b := Func()
func (p *Parser) parseMultiAssign() *ast.MultiAssignStmt {
pos := p.current.Pos
var targets []string
// Parse target list: a, b, c or _, b
for {
name := "_"
if p.current.Kind == token.IDENT && p.current.Literal == "_" {
p.advance()
} else {
tok := p.expectMethodName()
name = tok.Literal
}
targets = append(targets, name)
if !p.match(token.COMMA) {
break
}
}
p.expect(token.ASSIGN) // :=
// Parse value list
var values []ast.Expr
values = append(values, p.parseExpr())
for p.match(token.COMMA) {
values = append(values, p.parseExpr())
}
p.expectEndOfStmt()
return &ast.MultiAssignStmt{AssignPos: pos, Targets: targets, Values: values}
}
// parseDefer: DEFER expr
func (p *Parser) parseDefer() *ast.DeferStmt {
pos := p.expect(token.DEFER_KW).Pos
// Parse the expression to defer (typically a method call: db:Close())
call := p.parseExpr()
p.expectEndOfStmt()
return &ast.DeferStmt{DeferPos: pos, Call: call}
}
// parseConstBlock: CONST ... END CONST
func (p *Parser) parseConstBlock() ast.Stmt {
pos := p.expect(token.CONST_KW).Pos
p.expectEndOfStmt()
var items []ast.ConstItem
p.skipNewlines()
for !p.atAny(token.END, token.ENDCASE, token.EOF) {
if p.current.Kind == token.IDENT || p.current.Literal != "" {
name := p.expectMethodName().Literal
var val ast.Expr
if p.match(token.ASSIGN) {
val = p.parseExpr()
}
items = append(items, ast.ConstItem{Name: name, Value: val})
p.expectEndOfStmt()
} else {
break
}
p.skipNewlines()
}
if p.match(token.END) {
// Skip optional CONST after END
if p.current.Kind == token.CONST_KW {
p.advance()
}
}
p.expectEndOfStmt()
_ = pos
return &ast.ExprStmt{X: &ast.LiteralExpr{Kind: token.NIL_LIT, Value: "NIL"}}
}
// parseShortIfAssign: IF var := expr ; condition ... ENDIF
// Detected inside parseIf when IF is followed by IDENT := expr ;
// === Five Concurrency Parsers ===
// parseWatch: WATCH / CASE msg := <- ch / CASE ch <- val / OTHERWISE / ENDWATCH
func (p *Parser) parseWatch() *ast.WatchStmt {
watchPos := p.expect(token.WATCH_KW).Pos
p.expectEndOfStmt()
var cases []*ast.WatchCase
var otherwise []ast.Stmt
p.skipNewlines()
for p.current.Kind == token.CASE {
casePos := p.advance().Pos
wc := &ast.WatchCase{CasePos: casePos}
if p.at(token.ARROW_LEFT) {
// CASE <- ch (receive, discard value)
p.advance() // consume <-
wc.RecvChan = p.parseExpr()
} else {
// CASE var := <- ch OR CASE ch <- val
first := p.parseExpr()
if p.at(token.ASSIGN) && p.peekAt(1) == token.ARROW_LEFT {
// CASE var := <- ch
p.advance() // consume :=
p.advance() // consume <-
wc.RecvChan = p.parseExpr()
if ident, ok := first.(*ast.IdentExpr); ok {
wc.RecvVar = ident.Name
}
} else if p.at(token.ARROW_LEFT) {
// CASE ch <- val (send)
p.advance() // consume <-
wc.SendChan = first
wc.SendVal = p.parseExpr()
} else {
// CASE expr (boolean guard — less common)
wc.RecvChan = first
}
}
p.expectEndOfStmt()
wc.Body = p.parseStmtBlock(token.CASE, token.OTHERWISE, token.END)
cases = append(cases, wc)
}
if p.match(token.OTHERWISE) {
p.expectEndOfStmt()
otherwise = p.parseStmtBlock(token.END)
}
endPos := p.current.Pos
p.match(token.END)
// Skip optional WATCH after END
if p.current.Kind == token.WATCH_KW {
p.advance()
}
p.expectEndOfStmt()
return &ast.WatchStmt{WatchPos: watchPos, Cases: cases, Otherwise: otherwise, EndPos: endPos}
}
// parseParallelFor: PARALLEL FOR i := 1 TO n / body / NEXT
func (p *Parser) parseParallelFor() ast.Stmt {
p.expect(token.PARALLEL_KW)
if p.current.Kind != token.FOR {
// Not PARALLEL FOR — skip
p.skipToEndOfLine()
p.expectEndOfStmt()
return &ast.ExprStmt{X: &ast.LiteralExpr{Kind: token.NIL_LIT, Value: "NIL"}}
}
forPos := p.expect(token.FOR).Pos
varTok := p.expectMethodName()
varName := varTok.Literal
if p.at(token.ARROW) {
p.advance()
fieldTok := p.expectMethodName()
varName = fieldTok.Literal
}
p.expect(token.ASSIGN)
start := p.parseExpr()
p.expect(token.TO)
toExpr := p.parseExpr()
var step ast.Expr
if p.match(token.STEP) {
step = p.parseExpr()
}
p.expectEndOfStmt()
body := p.parseStmtBlock(token.NEXT, token.END)
endPos := p.current.Pos
if !p.match(token.NEXT) {
p.match(token.END)
}
if p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
p.skipToEndOfLine()
}
p.expectEndOfStmt()
return &ast.ParallelForStmt{
ForPos: forPos, Var: varName, Start: start, To: toExpr,
Step: step, Body: body, EndPos: endPos,
}
}
// parseWithTimeout: WITH TIMEOUT n / body / ENDWITH
func (p *Parser) parseWithTimeout() *ast.TimeoutStmt {
withPos := p.advance().Pos // consume WITH
p.expect(token.TIMEOUT_KW) // consume TIMEOUT
duration := p.parseExpr()
p.expectEndOfStmt()
body := p.parseStmtBlock(token.END)
endPos := p.current.Pos
p.match(token.END)
// Skip optional WITH after END
if p.current.Kind == token.IDENT && p.currentUpper() == "WITH" {
p.advance()
}
p.expectEndOfStmt()
return &ast.TimeoutStmt{WithPos: withPos, Duration: duration, Body: body, EndPos: endPos}
}
func (p *Parser) isShortIfAssign() bool {
// Look ahead: IF ident := expr ; condition
if p.pos+3 >= len(p.tokens) {
return false
}
// Check pattern: IDENT := ... ;
for i := p.pos + 1; i < len(p.tokens); i++ {
if p.tokens[i].Kind == token.SEMICOLON {
// Found ; before newline — it's short if
return true
}
if p.tokens[i].Kind == token.NEWLINE || p.tokens[i].Kind == token.EOF {
return false
}
}
return false
}