// 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: 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 "" ARG INLINE 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 [(params)] INLINE // Harbour sugar for an inline method. Emit as MethodDecl so // emitClassDecl routes it through AddMethod and the inline // body emitter generates the HB__ function. members = append(members, p.parseMessageDecl()) } else if upper == "OPERATOR" { // OPERATOR "" ARG INLINE 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 "" [ARG ] INLINE // // The ARG binds the RHS operand to a local when the operator is // dispatched via the VM's binary op. INLINE is the body. // Harbour also allows `OPERATOR "" ... 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_ 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 [(params)] INLINE ` 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 () 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 } 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 var aliasExprNode ast.Expr var shared, readOnly bool // 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} } // Bare ident as filename: USE myfile / USE myfile.dbf / USE myfile NEW // In Harbour, USE treats name as a filename string, not a variable. // Only use parseExpr for parenthesized (USE (expr)) or string literal (USE "file"). if p.at(token.IDENT) { // Check if it's a bare filename (ident optionally followed by .ext) 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) { p.parseMacro() // macro alias — skip } 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) { 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 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 }