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