// 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 } // 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__ 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 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: members = append(members, p.parseDataDecl()) 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 { members = append(members, p.parseDataDecl()) } else if p.current.Kind == token.METHOD { members = append(members, p.parseClassMethodDecl()) } else if p.current.Kind == token.IDENT && p.currentUpper() == "VAR" { p.tokens[p.pos].Kind = token.DATA p.current = p.tokens[p.pos] members = append(members, p.parseDataDecl()) } else { p.skipToEndOfLine() } case token.INLINE_KW, token.ON, token.DESTRUCTOR, token.OPERATOR_KW: // Stray INLINE, ON ERROR, DESTRUCTOR, OPERATOR — 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] members = append(members, p.parseDataDecl()) } 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 == "CLASS" { // CLASS VAR — class-level variable p.advance() // skip CLASS if p.current.Kind == token.IDENT && p.currentUpper() == "VAR" { p.tokens[p.pos].Kind = token.DATA p.current = p.tokens[p.pos] members = append(members, p.parseDataDecl()) } else if p.current.Kind == token.DATA { members = append(members, p.parseDataDecl()) } else if p.current.Kind == token.METHOD { members = append(members, p.parseClassMethodDecl()) } else { p.skipToEndOfLine() } } else if upper == "ON" || upper == "OPERATOR" || upper == "DESTRUCTOR" || upper == "DELEGATE" || upper == "ERROR" || upper == "MESSAGE" || 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, } } func (p *Parser) parseDataDecl() *ast.DataDecl { dataPos := p.expect(token.DATA).Pos name := p.expectMethodName().Literal // allow keywords as data names var init ast.Expr var asType string // Parse AS, INIT, commas, and qualifiers in any order for p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF { if p.match(token.AS) { if p.current.Kind == token.IDENT || p.current.Literal != "" { p.advance() // type name } continue } if p.match(token.COMMA) { // VAR One, Two, Three — skip additional names if p.current.Kind == token.IDENT || p.current.Literal != "" { p.advance() // skip additional name } continue } if p.current.Kind == token.IDENT { upper := p.currentUpper() if upper == "INIT" { p.advance() 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 } p.expectEndOfStmt() return &ast.DataDecl{DataPos: dataPos, Name: name, Init: init, AsType: asType} } // 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() } // Skip INLINE + rest of line (METHOD ... INLINE expr) p.skipClassInline() p.expectEndOfStmt() return &ast.MethodDecl{ MethodPos: methodPos, Name: name, Params: params, IsSetGet: isSetGet, EndPos: methodPos, } } // skipClassInline skips INLINE keyword and the rest of the line (used in CLASS body) 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 } 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 --- // 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 } 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 ¯o 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 // USE without args = close if p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF { // If file starts with macro &, skip entire USE to EOL (complex macro syntax) if p.at(token.AMPERSAND) { p.skipToEndOfLine() p.expectEndOfStmt() return &ast.UseCmd{UsePos: pos} } 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() via = p.expectMethodName().Literal continue } if upper == "ALIAS" { p.advance() if p.at(token.AMPERSAND) { p.parseMacro() // macro alias — skip } else { alias = p.expectMethodName().Literal } continue } if upper == "EXCLUSIVE" || upper == "SHARED" || upper == "NEW" || upper == "READONLY" || 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) { p.parseMacro() } 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} } func (p *Parser) parseSelect() *ast.SelectCmd { pos := p.expect(token.SELECT).Pos 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 TO file OR INDEX ON expr TAG tagname [TO file] var fileExpr ast.Expr 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 tagExpr := p.parseExpr() // tag name if p.match(token.TO) { fileExpr = p.parseExpr() } else { fileExpr = tagExpr // use tag name as file } } 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, 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() } 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 }