Files
five/compiler/parser/parser.go
CharlesKWON f4ed42556b checkpoint: season-wide bug fix campaign + infra
Cumulative season's silent-bug hunting (~62 fixes) across the FiveSql2
SQL engine, the Five compiler/runtime, and the hbrdd RDD layer. Saved
as a single checkpoint before refactoring the parser to delegate xBase
command translation to the preprocessor.

Highlights:

FiveSql2 engine (_FiveSql2/src/)
- prefix-glob index attach -> explicit convention (<table>_pk.ntx,
  <table>_uq.ntx, <table>.cdx) — fixes silent multi-row INSERT row-drop
- DROP/CREATE TABLE FErase chain extended (.cdx, .fsc, .fsv, .dbt, .fpt)
- COUNT(DISTINCT col) parsed + aggregated via hSeen hash
- UNION column-count mismatch returns SQL_ERR_GRAMMAR (was silent)
- DISTINCT + ORDER BY hidden-col leak fixed (trim before DISTINCT)
- Derived table FROM (SELECT...) + JOIN right-side derived
- Self-FK CASCADE depth 2+ via SqlGetSingleColPK pre-collect
- LAG/LEAD default arg uses SqlEvalRowExpr (handles -N const exprs)
- DATE literal round-trip validation (Feb 29 non-leap rejected)
- CREATE OR REPLACE VIEW; CREATE VIEW errors on already-exists
- AlterTable type dispatcher comma-wrapped (1-char type "A" no longer
  matches CHARACTER)

Compiler / runtime
- gengo: HB_ -> FV_ prefix on emitted Go function names (Five identity)
- gengo split: emit_block.go, emit_stmt.go, folding.go extracted
- parser/stmtreg.go nudges
- hbrt: debug TUI/CLI restructure (debugcmd, debugkey, termios_*),
  windows debug stubs collapsed
- thread/vm/value/class/pcinterp tightening from panic traces

RDD layer (hbrdd/)
- dbf: null bitmap support (null.go + null_test.go), mmap split
  (mmap_posix.go / mmap_windows.go), byte-level numeric parse
- ntx/cdx: windows mmap parity
- workarea + mem RDD: cross-area state-bleed fixes

RTL (hbrtl/)
- errorlog rewrite with platform-specific FD (errorlog_fd_unix /
  errorlog_fd_other)
- sqlscan, sqlhelpers, indexrtl, datetime extensions

Gates green at checkpoint:
- 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-04-30 09:26:25 +09:00

2498 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
switch upper {
case "COPY", "SORT", "COUNT", "SUM", "AVERAGE", "TOTAL", "UPDATE",
"LABEL", "REPORT", "ACCEPT", "INPUT", "LOCATE", "CONTINUE",
"JOIN", "RELEASE", "SAVE", "RESTORE", "ERASE", "RENAME",
"RUN", "DIR", "STORE", "NOTE", "TEXT", "ENDTEXT",
"WITH", "KEYBOARD", "CLEAR", "DISPLAY", "LIST", "REINDEX":
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 "COMMIT":
p.advance()
p.expectEndOfStmt()
return &ast.ExprStmt{X: &ast.CallExpr{
Func: &ast.IdentExpr{Name: "DbCommit"},
}}
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()
}
// CLOSE [DATABASES|ALL] — close work areas
if p.current.Kind == token.IDENT && p.currentUpper() == "CLOSE" {
p.advance()
// Skip optional DATABASES/ALL keyword
if p.current.Kind == token.IDENT {
p.advance()
}
p.expectEndOfStmt()
return &ast.ExprStmt{X: &ast.CallExpr{
Func: &ast.IdentExpr{Name: "DbCloseArea"},
}}
}
// xBase commands that consume entire line (COPY, SORT, COUNT, SUM, etc.)
if p.current.Kind == token.IDENT {
// WITH TIMEOUT n / body / ENDWITH
if p.currentUpper() == "WITH" &&
p.peekAt(1) == token.TIMEOUT_KW {
return p.parseWithTimeout()
}
switch p.currentUpper() {
case "COPY", "SORT", "COUNT", "SUM", "AVERAGE", "TOTAL", "UPDATE",
"LABEL", "REPORT", "ACCEPT", "INPUT", "LOCATE", "CONTINUE",
"JOIN", "RELEASE", "SAVE", "RESTORE", "ERASE", "RENAME",
"RUN", "DIR", "STORE", "NOTE", "TEXT", "ENDTEXT",
"WITH", "KEYBOARD", "CLEAR", "DISPLAY", "LIST", "REINDEX":
// 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"}}
}
}
// COMMIT — flush work area
if p.current.Kind == token.IDENT && p.currentUpper() == "COMMIT" {
p.advance()
p.expectEndOfStmt()
return &ast.ExprStmt{X: &ast.CallExpr{
Func: &ast.IdentExpr{Name: "DbCommit"},
}}
}
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
}