Files
five/compiler/parser/parser.go
CharlesKWON 34485cd6c8 feat(oop): METHOD ... INLINE <expr> and MESSAGE handlers
Harbour's inline-method sugar was parsed but the body was skipped,
leaving any `METHOD X() INLINE expr` declaration registered in the
class vtable with no matching HB_<CLASS>_X function — link error
at build time.

Parser: MethodDecl gains an InlineBody Expr field. parseClassMethodDecl
captures the expression after INLINE instead of skipping to EOL.
New parseMessageDecl handles `MESSAGE <name> [(params)] INLINE expr`
and returns the same MethodDecl shape.

Codegen: emitClassDecl walks members a second time after the class
registration init block and emits emitInlineMethodBody for each
IsInline method — a Frame(nParams, 0) + emitExpr(InlineBody) +
RetValue function. curMethodClass is bound so ::super: inside an
inline body still resolves.

Tested (/tmp/test_inline.prg): all four patterns — bare INLINE,
MESSAGE INLINE, INLINE with params, INLINE reading ::field —
produce expected values.

FiveSql2 43/43, Harbour compat 56/56, Go test ALL PASS.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 15:41:36 +09:00

2331 lines
61 KiB
Go

// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
// All rights reserved.
// Recursive descent parser for the Five language (Harbour-compatible).
//
// Design references:
// - Harbour: harbour.y (Bison grammar, /mnt/d/harbour-core/src/compiler/harbour.y)
// Key: operator precedence is mostly right-associative, = is both assign and compare
// - tsgo: internal/parser/ — recursive descent with precedence climbing
// - Pratt parser for expression parsing (precedence climbing)
//
// Harbour-specific rules enforced:
// - LOCAL/STATIC/FIELD declarations must be at function top (before executable code)
// - ? and ?? are QOut/QQOut shorthand
// - (expr)->field is dynamic alias access
// - &variable is macro expansion
package parser
import (
"five/compiler/ast"
"five/compiler/lexer"
"five/compiler/token"
"fmt"
"strings"
)
// Parser parses Five/Harbour source into an AST.
type Parser struct {
tokens []token.Token
pos int
current token.Token
errors []Error
file string
GoDumps []string // inline Go code blocks from PP
}
// Error represents a parse error with position.
type Error struct {
Pos token.Position
Message string
}
func (e Error) Error() string {
return fmt.Sprintf("%s: %s", e.Pos, e.Message)
}
// Parse parses a source file and returns the AST.
func Parse(filename, source string) (*ast.File, []Error) {
return ParseWithGoDumps(filename, source, nil)
}
// ParseWithGoDumps parses with inline Go code blocks from PP.
func ParseWithGoDumps(filename, source string, goDumps []string) (*ast.File, []Error) {
tokens := lexer.Tokenize(filename, source)
p := &Parser{
tokens: tokens,
pos: 0,
file: filename,
GoDumps: goDumps,
}
if len(tokens) > 0 {
p.current = tokens[0]
}
file := p.parseFile()
return file, p.errors
}
// --- Token navigation ---
func (p *Parser) peek() token.Kind {
return p.current.Kind
}
// currentUpper returns the uppercased literal of the current token.
// Uses strings.EqualFold for comparisons where possible to avoid allocation.
func (p *Parser) currentUpper() string {
return strings.ToUpper(p.current.Literal)
}
// peekAt returns the token kind at offset from current position.
// peekAt(0) = current, peekAt(1) = next, etc. Returns EOF if out of range.
func (p *Parser) peekAt(offset int) token.Kind {
idx := p.pos + offset
if idx >= 0 && idx < len(p.tokens) {
return p.tokens[idx].Kind
}
return token.EOF
}
// peekLitAt returns the token literal at offset from current position.
func (p *Parser) peekLitAt(offset int) string {
idx := p.pos + offset
if idx >= 0 && idx < len(p.tokens) {
return p.tokens[idx].Literal
}
return ""
}
// peekTokenAt returns the full token at offset. Safe for out-of-range.
func (p *Parser) peekTokenAt(offset int) token.Token {
idx := p.pos + offset
if idx >= 0 && idx < len(p.tokens) {
return p.tokens[idx]
}
return token.Token{Kind: token.EOF}
}
func (p *Parser) advance() token.Token {
tok := p.current
p.pos++
if p.pos < len(p.tokens) {
p.current = p.tokens[p.pos]
} else {
p.current = token.Token{Kind: token.EOF}
}
return tok
}
func (p *Parser) match(kind token.Kind) bool {
if p.current.Kind == kind {
p.advance()
return true
}
return false
}
func (p *Parser) expect(kind token.Kind) token.Token {
if p.current.Kind == kind {
return p.advance()
}
p.error(fmt.Sprintf("expected %v, got %v %q", kind, p.current.Kind, p.current.Literal))
// Error recovery: return a synthetic token and try to continue
return token.Token{Kind: kind, Pos: p.current.Pos}
}
func (p *Parser) at(kind token.Kind) bool {
return p.current.Kind == kind
}
func (p *Parser) atAny(kinds ...token.Kind) bool {
for _, k := range kinds {
if p.current.Kind == k {
return true
}
}
return false
}
// --- Error handling & recovery ---
func (p *Parser) error(msg string) {
// Avoid duplicate errors at same position
if len(p.errors) > 0 {
last := p.errors[len(p.errors)-1]
if last.Pos == p.current.Pos {
return
}
}
p.errors = append(p.errors, Error{
Pos: p.current.Pos,
Message: msg,
})
}
// syncStmt skips tokens until a statement boundary is found.
// Called after errors to prevent cascading parse failures.
func (p *Parser) syncStmt() {
for p.current.Kind != token.EOF {
// Newline = statement boundary in Harbour
if p.current.Kind == token.NEWLINE {
p.advance()
return
}
// Statement-starting keywords = safe to resume
switch p.current.Kind {
case token.FUNCTION_KW, token.PROCEDURE, token.CLASS, token.METHOD,
token.LOCAL, token.STATIC, token.PRIVATE, token.PUBLIC,
token.IF, token.FOR, token.DO, token.WHILE, token.SWITCH,
token.BEGIN, token.RETURN, token.QMARK, token.QQMARK:
return
}
p.advance()
}
}
// skipNewlines consumes consecutive NEWLINE tokens.
func (p *Parser) skipNewlines() {
for p.current.Kind == token.NEWLINE {
p.advance()
}
}
// expectEndOfStmt expects a NEWLINE or EOF (statement terminator).
func (p *Parser) expectEndOfStmt() {
if p.current.Kind == token.NEWLINE {
p.advance()
return
}
if p.current.Kind == token.EOF {
return
}
// Don't error for some keywords that start new statements
// (Harbour allows missing newline before certain keywords)
}
// --- File parsing ---
func (p *Parser) parseFile() *ast.File {
file := &ast.File{Name: p.file}
p.skipNewlines()
for p.current.Kind != token.EOF {
switch p.current.Kind {
case token.FUNCTION_KW, token.PROCEDURE:
file.Decls = append(file.Decls, p.parseFuncDecl())
case token.EXIT:
// EXIT PROCEDURE/FUNCTION at top level
p.advance()
if p.current.Kind == token.PROCEDURE || p.current.Kind == token.FUNCTION_KW {
file.Decls = append(file.Decls, p.parseFuncDecl())
} else {
p.skipToEndOfLine()
}
case token.CLASS:
file.Decls = append(file.Decls, p.parseClassDecl())
case token.METHOD:
file.Decls = append(file.Decls, p.parseMethodDecl())
case token.STATIC:
// STATIC FUNCTION/PROCEDURE → function declaration with static scope
if p.peekAt(1) == token.FUNCTION_KW || p.peekAt(1) == token.PROCEDURE {
p.advance() // skip STATIC
file.Decls = append(file.Decls, p.parseFuncDecl())
} else {
// Top-level STATIC declaration (module-level variable)
file.Decls = append(file.Decls, p.parseVarDecl())
}
case token.IDENT:
upper := p.currentUpper()
if upper == "THREAD" && p.peekAt(1) == token.STATIC {
// THREAD STATIC → treat as STATIC
p.advance() // skip THREAD
file.Decls = append(file.Decls, p.parseVarDecl())
} else if upper == "CREATE" && p.peekAt(1) == token.CLASS {
// CREATE CLASS → treat as CLASS
p.advance() // skip CREATE
file.Decls = append(file.Decls, p.parseClassDecl())
} else if upper == "EXTERNAL" || upper == "EXTERN" || upper == "REQUEST" || upper == "ANNOUNCE" || upper == "DYNAMIC" || upper == "DECLARE" {
p.skipToEndOfLine()
} else if upper == "INIT" || upper == "EXIT" {
// INIT PROCEDURE/FUNCTION or EXIT PROCEDURE/FUNCTION
p.advance() // skip INIT/EXIT
if p.current.Kind == token.PROCEDURE || p.current.Kind == token.FUNCTION_KW {
file.Decls = append(file.Decls, p.parseFuncDecl())
} else {
p.skipToEndOfLine()
}
} else if upper == "SET" {
// Top-level SET commands (SET PROCEDURE TO, etc.)
p.skipToEndOfLine()
} else if upper == "FIVE_GODUMP__" {
// Inline Go code block from #pragma BEGINDUMP
file.Decls = append(file.Decls, p.parseGoDump())
} else {
// Unknown IDENT at top level — PP residual or bare expression, skip
p.skipToEndOfLine()
}
case token.MEMVAR, token.DECLARE:
// File-level MEMVAR/DECLARE declaration — skip
p.skipToEndOfLine()
case token.IMPORT:
file.Imports = append(file.Imports, p.parseImport())
case token.PP_INCLUDE, token.PP_DEFINE, token.PP_IFDEF, token.PP_IFNDEF,
token.PP_ELSE, token.PP_ENDIF, token.PP_PRAGMA, token.PP_UNDEF,
token.PP_COMMAND, token.PP_TRANSLATE:
p.skipToEndOfLine() // skip preprocessor for now
default:
// Bare expression or PP residual at top level — skip to EOL
// This handles: PP #command residuals, bare function calls, etc.
p.skipToEndOfLine()
}
p.skipNewlines()
}
return file
}
func (p *Parser) skipToEndOfLine() {
for p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
p.advance()
}
if p.current.Kind == token.NEWLINE {
p.advance()
}
}
// consumeFileExtension handles filename.ext (e.g. test.dbf, test.idx)
// where the dot+extension was parsed as separate tokens after parseExpr.
func (p *Parser) consumeFileExtension(fileExpr ast.Expr) {
for p.current.Kind == token.DOT &&
(p.peekAt(1) == token.IDENT || p.peekAt(1) == token.INT) {
p.advance() // skip .
ext := p.advance() // skip extension
if ident, ok := fileExpr.(*ast.IdentExpr); ok {
ident.Name = ident.Name + "." + ext.Literal
}
}
}
// parseGoDump parses FIVE_GODUMP__ <index> marker from PP.
func (p *Parser) parseGoDump() *ast.GoDumpDecl {
pos := p.advance().Pos // consume FIVE_GODUMP__
idx := 0
if p.current.Kind == token.INT {
fmt.Sscanf(p.current.Literal, "%d", &idx)
p.advance()
}
p.expectEndOfStmt()
code := ""
if idx >= 0 && idx < len(p.GoDumps) {
code = p.GoDumps[idx]
}
return &ast.GoDumpDecl{DumpPos: pos, Code: code}
}
// --- IMPORT ---
func (p *Parser) parseImport() *ast.ImportDecl {
pos := p.expect(token.IMPORT).Pos
alias := ""
// IMPORT _ "pkg" — blank import
// IMPORT alias "pkg" — aliased import
if p.current.Kind == token.IDENT && p.peekAt(1) == token.STRING {
alias = p.advance().Literal
}
pathTok := p.expect(token.STRING)
p.expectEndOfStmt()
return &ast.ImportDecl{ImportPos: pos, Alias: alias, Path: pathTok.Literal}
}
// --- FUNCTION / PROCEDURE ---
func (p *Parser) parseFuncDecl() *ast.FuncDecl {
tok := p.advance() // FUNCTION or PROCEDURE
isProc := tok.Kind == token.PROCEDURE
// Allow keywords as function names (GOTO, END, etc.)
nameTok := p.expectMethodName()
// Parameters
var params []*ast.ParamDecl
if p.match(token.LPAREN) {
params = p.parseParamList()
p.expect(token.RPAREN)
}
// Skip CLASS className qualifier (PROCEDURE name CLASS className)
if p.current.Kind == token.CLASS {
p.advance() // skip CLASS
p.expectMethodName() // skip className
}
p.expectEndOfStmt()
// Declarations (LOCAL, STATIC, FIELD — must come first)
var decls []ast.Decl
p.skipNewlines()
for p.atAny(token.LOCAL, token.STATIC, token.FIELD, token.MEMVAR, token.PARAMETERS) {
decls = append(decls, p.parseVarDecl())
p.skipNewlines()
}
// Body (executable statements until RETURN or end)
body := p.parseStmtBlock(token.RETURN, token.FUNCTION_KW, token.PROCEDURE, token.CLASS, token.METHOD, token.EOF)
// Consume RETURN if present
var endPos token.Position
if p.current.Kind == token.RETURN {
retStmt := p.parseReturn()
body = append(body, retStmt)
endPos = retStmt.Pos()
} else {
endPos = p.current.Pos
}
return &ast.FuncDecl{
FuncPos: tok.Pos,
Name: nameTok.Literal,
IsProc: isProc,
Params: params,
Decls: decls,
Body: body,
EndPos: endPos,
}
}
func (p *Parser) parseParamList() []*ast.ParamDecl {
var params []*ast.ParamDecl
if p.at(token.RPAREN) {
return params
}
// Variadic: FUNCTION Foo(...) — skip dots, use PCount() at runtime
if p.at(token.DOT) {
for p.at(token.DOT) {
p.advance()
}
return params
}
for {
byRef := p.match(token.AT)
// Handle trailing ... after named params: FUNCTION Foo(a, b, ...)
if p.at(token.DOT) {
for p.at(token.DOT) {
p.advance()
}
break
}
name := p.expectMethodName() // allow keywords as param names (e.g. data, type)
var asType string
if p.match(token.AS) {
asType = p.expectMethodName().Literal
}
params = append(params, &ast.ParamDecl{
NamePos: name.Pos,
Name: name.Literal,
ByRef: byRef,
AsType: asType,
})
if !p.match(token.COMMA) {
break
}
}
return params
}
// --- Variable declarations ---
func (p *Parser) parseVarDecl() *ast.VarDecl {
tok := p.advance() // LOCAL, STATIC, FIELD, MEMVAR
var scope ast.VarScope
switch tok.Kind {
case token.LOCAL:
scope = ast.ScopeLocal
case token.STATIC:
scope = ast.ScopeStatic
case token.FIELD:
scope = ast.ScopeField
case token.MEMVAR:
// MEMVAR declares field aliases, treated like FIELD
scope = ast.ScopeField
default:
scope = ast.ScopeLocal
}
var vars []*ast.VarInit
for {
// Allow keywords as variable names (data, field, etc.)
name := p.expectMethodName()
var init ast.Expr
var asType string
// LOCAL a[10] or LOCAL a[5,3] — array declaration
if p.at(token.LBRACKET) {
p.advance() // skip [
sizeExpr := p.parseExpr()
// Multi-dim: a[5,3]
for p.match(token.COMMA) {
p.parseExpr() // consume additional dimensions (simplified)
}
p.expect(token.RBRACKET)
// Generate init as Array(size)
init = &ast.CallExpr{
Func: &ast.IdentExpr{Name: "Array"},
Args: []ast.Expr{sizeExpr},
}
}
// AS type (may come before or after :=)
if p.match(token.AS) {
// Skip type: AS STRING, AS NUMERIC, AS CLASS ClassName, AS ARRAY OF type
for p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF &&
p.current.Kind != token.ASSIGN && p.current.Kind != token.COMMA {
p.advance()
}
}
if p.match(token.ASSIGN) { // :=
init = p.parseExpr()
}
// AS type after := (alternative order)
if p.match(token.AS) {
asType = p.expectMethodName().Literal
// Skip complex AS: AS CLASS ClassName, AS ARRAY OF...
for p.current.Kind == token.IDENT {
upper := p.currentUpper()
if upper == "OF" || upper == "CLASS" {
p.advance()
if p.current.Kind == token.IDENT || p.current.Literal != "" {
p.advance()
}
} else {
break
}
}
}
vars = append(vars, &ast.VarInit{
NamePos: name.Pos,
Name: name.Literal,
Init: init,
AsType: asType,
})
if !p.match(token.COMMA) {
break
}
}
p.expectEndOfStmt()
return &ast.VarDecl{DeclPos: tok.Pos, Scope: scope, Vars: vars}
}
// --- CLASS ---
func (p *Parser) parseClassDecl() *ast.ClassDecl {
classPos := p.expect(token.CLASS).Pos
name := p.expect(token.IDENT).Literal
var parent string
if p.match(token.INHERIT) {
p.match(token.FROM) // optional FROM
parent = p.expectMethodName().Literal
}
// Alternative: FROM without INHERIT
if parent == "" && p.match(token.FROM) {
parent = p.expectMethodName().Literal
}
// Multiple inheritance: FROM class1, class2, class3 — skip extra parents
for p.match(token.COMMA) {
p.expectMethodName() // skip additional parent class name
}
p.expectEndOfStmt()
var members []ast.Decl
p.skipNewlines()
for !p.atAny(token.ENDCLASS, token.END, token.EOF) {
switch p.current.Kind {
case token.DATA:
members = append(members, p.parseDataDecl())
case token.METHOD:
members = append(members, p.parseClassMethodDecl())
case token.ACCESS:
members = append(members, p.parseAccessDecl())
case token.ASSIGN_KW:
members = append(members, p.parseAssignDecl())
case token.CLASS:
// CLASS VAR / CLASS METHOD / CLASS DATA inside class body
p.advance() // skip CLASS
if p.current.Kind == token.DATA {
members = append(members, p.parseDataDecl())
} else if p.current.Kind == token.METHOD {
members = append(members, p.parseClassMethodDecl())
} else if p.current.Kind == token.IDENT && p.currentUpper() == "VAR" {
p.tokens[p.pos].Kind = token.DATA
p.current = p.tokens[p.pos]
members = append(members, p.parseDataDecl())
} else {
p.skipToEndOfLine()
}
case token.INLINE_KW, token.ON, token.DESTRUCTOR, token.OPERATOR_KW:
// Stray INLINE, ON ERROR, DESTRUCTOR, OPERATOR — skip to EOL
p.skipToEndOfLine()
p.skipNewlines()
continue
case token.IDENT:
upper := p.currentUpper()
// FRIEND FUNCTION/CLASS — skip declaration
if upper == "FRIEND" {
p.advance() // skip FRIEND
p.skipToEndOfLine()
p.skipNewlines()
continue
}
// VAR = DATA synonym
if upper == "VAR" {
// Rewrite as DATA token
p.tokens[p.pos].Kind = token.DATA
p.current = p.tokens[p.pos]
members = append(members, p.parseDataDecl())
} else if (upper == "PROTECTED" || upper == "EXPORTED" || upper == "HIDDEN" ||
upper == "VISIBLE" || upper == "EXPORT" || upper == "SYNC") &&
p.peekAt(1) == token.COLON {
// Scope qualifier — skip it (Five doesn't enforce visibility)
p.advance() // skip keyword
p.advance() // skip :
p.skipNewlines()
} else if upper == "CLASSDATA" || upper == "CLASSVAR" {
// CLASSDATA / CLASSVAR — class-level variable (treat as DATA)
p.tokens[p.pos].Kind = token.DATA
p.current = p.tokens[p.pos]
members = append(members, p.parseDataDecl())
} else if upper == "CLASS" {
// CLASS VAR — class-level variable
p.advance() // skip CLASS
if p.current.Kind == token.IDENT && p.currentUpper() == "VAR" {
p.tokens[p.pos].Kind = token.DATA
p.current = p.tokens[p.pos]
members = append(members, p.parseDataDecl())
} else if p.current.Kind == token.DATA {
members = append(members, p.parseDataDecl())
} else if p.current.Kind == token.METHOD {
members = append(members, p.parseClassMethodDecl())
} else {
p.skipToEndOfLine()
}
} else if upper == "MESSAGE" {
// MESSAGE <name> [(params)] INLINE <expr>
// Harbour sugar for an inline method. Emit as MethodDecl so
// emitClassDecl routes it through AddMethod and the inline
// body emitter generates the HB_<CLASS>_<NAME> function.
members = append(members, p.parseMessageDecl())
} else if upper == "ON" || upper == "OPERATOR" || 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,
}
}
func (p *Parser) parseDataDecl() *ast.DataDecl {
dataPos := p.expect(token.DATA).Pos
name := p.expectMethodName().Literal // allow keywords as data names
var init ast.Expr
var asType string
// Parse AS, INIT, commas, and qualifiers in any order
for p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
if p.match(token.AS) {
if p.current.Kind == token.IDENT || p.current.Literal != "" {
p.advance() // type name
}
continue
}
if p.match(token.COMMA) {
// VAR One, Two, Three — skip additional names
if p.current.Kind == token.IDENT || p.current.Literal != "" {
p.advance() // skip additional name
}
continue
}
if p.current.Kind == token.IDENT {
upper := p.currentUpper()
if upper == "INIT" {
p.advance()
init = p.parseExpr()
continue
}
// Skip visibility/attribute qualifiers
if upper == "READONLY" || upper == "EXPORTED" || upper == "PROTECTED" ||
upper == "HIDDEN" || upper == "SYNC" || upper == "USUAL" ||
upper == "PROPERTY" || upper == "PERSISTENT" || upper == "SHARED" {
p.advance()
continue
}
}
if p.current.Kind == token.INLINE_KW {
p.skipToEndOfLine()
break
}
break
}
p.expectEndOfStmt()
return &ast.DataDecl{DataPos: dataPos, Name: name, Init: init, AsType: asType}
}
// expectMethodName: method names can be keywords (end, home, left, right, etc.)
func (p *Parser) expectMethodName() token.Token {
if p.current.Kind == token.IDENT {
return p.advance()
}
// Allow keywords as method names
if p.current.Literal != "" {
return p.advance()
}
return p.expect(token.IDENT)
}
func (p *Parser) parseClassMethodDecl() *ast.MethodDecl {
methodPos := p.expect(token.METHOD).Pos
// Skip optional FUNCTION/PROCEDURE qualifier
if p.current.Kind == token.FUNCTION_KW || p.current.Kind == token.PROCEDURE {
p.advance()
}
name := p.expectMethodName().Literal
var params []*ast.ParamDecl
if p.match(token.LPAREN) {
params = p.parseParamList()
p.expect(token.RPAREN)
}
// Check for SETGET keyword
isSetGet := false
if p.current.Kind == token.IDENT && p.currentUpper() == "SETGET" {
p.advance()
isSetGet = true
}
// Skip trailing qualifiers: OPERATOR, VIRTUAL, CONSTRUCTOR, etc.
if p.current.Kind == token.IDENT {
upper := p.currentUpper()
if upper == "OPERATOR" || upper == "VIRTUAL" || upper == "DEFERRED" {
p.skipToEndOfLine()
}
}
if p.current.Kind == token.OPERATOR_KW || p.current.Kind == token.CONSTRUCTOR {
p.skipToEndOfLine()
}
// 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,
}
}
// parseMessageDecl parses `MESSAGE <name> [(params)] INLINE <expr>` in
// a CLASS body and returns a MethodDecl. Harbour semantics: a MESSAGE
// handler is invoked like a method and behaves identically — the form
// exists mostly for readability of "this ident is really a message
// handler, not a regular method". Without INLINE the handler is
// declaration-only (the real body arrives as a separate
// `METHOD <name>() CLASS X` implementation).
func (p *Parser) parseMessageDecl() *ast.MethodDecl {
msgPos := p.current.Pos
p.advance() // MESSAGE
name := p.expectMethodName().Literal
var params []*ast.ParamDecl
if p.match(token.LPAREN) {
params = p.parseParamList()
p.expect(token.RPAREN)
}
isInline := false
var inlineBody ast.Expr
if p.current.Kind == token.INLINE_KW ||
(p.current.Kind == token.IDENT && p.currentUpper() == "INLINE") {
p.advance()
isInline = true
inlineBody = p.parseExpr()
}
p.expectEndOfStmt()
return &ast.MethodDecl{
MethodPos: msgPos,
Name: name,
Params: params,
IsInline: isInline,
InlineBody: inlineBody,
EndPos: msgPos,
}
}
// skipClassInline skips INLINE keyword and the rest of the line.
// Used by ACCESS/ASSIGN decls where inline body handling hasn't been
// wired up yet (falls back to pre-INLINE-capture behaviour).
func (p *Parser) skipClassInline() {
if p.current.Kind == token.INLINE_KW ||
(p.current.Kind == token.IDENT && p.currentUpper() == "INLINE") {
p.skipToEndOfLine()
}
}
// ACCESS name METHOD getterName
func (p *Parser) parseAccessDecl() *ast.MethodDecl {
pos := p.expect(token.ACCESS).Pos
propName := p.expectMethodName().Literal
// Skip optional (params)
if p.match(token.LPAREN) {
for !p.atAny(token.RPAREN, token.EOF) {
p.advance()
}
p.match(token.RPAREN)
}
methodName := propName
if p.match(token.METHOD) {
methodName = p.expectMethodName().Literal
if p.match(token.LPAREN) {
for !p.atAny(token.RPAREN, token.EOF) {
p.advance()
}
p.match(token.RPAREN)
}
}
// Skip INLINE + rest of line
p.skipClassInline()
p.expectEndOfStmt()
return &ast.MethodDecl{
MethodPos: pos,
Name: methodName,
IsAccess: true,
AccessName: propName,
EndPos: pos,
}
}
// ASSIGN name METHOD setterName
func (p *Parser) parseAssignDecl() *ast.MethodDecl {
pos := p.expect(token.ASSIGN_KW).Pos
propName := p.expectMethodName().Literal
// Skip optional (params)
if p.match(token.LPAREN) {
for !p.atAny(token.RPAREN, token.EOF) {
p.advance()
}
p.match(token.RPAREN)
}
methodName := "_" + propName
if p.match(token.METHOD) {
methodName = p.expectMethodName().Literal
if p.match(token.LPAREN) {
for !p.atAny(token.RPAREN, token.EOF) {
p.advance()
}
p.match(token.RPAREN)
}
}
// Skip INLINE + rest of line
p.skipClassInline()
p.expectEndOfStmt()
return &ast.MethodDecl{
MethodPos: pos,
Name: methodName,
IsAssign: true,
AccessName: propName,
EndPos: pos,
}
}
// parseMethodDecl parses standalone: METHOD [FUNCTION|PROCEDURE] name(...) CLASS classname
func (p *Parser) parseMethodDecl() *ast.MethodDecl {
methodPos := p.expect(token.METHOD).Pos
// Skip optional FUNCTION/PROCEDURE qualifier
if p.current.Kind == token.FUNCTION_KW || p.current.Kind == token.PROCEDURE {
p.advance()
}
name := p.expectMethodName().Literal
var params []*ast.ParamDecl
if p.match(token.LPAREN) {
params = p.parseParamList()
p.expect(token.RPAREN)
}
var className string
if p.match(token.CLASS) {
className = p.expect(token.IDENT).Literal
}
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 &macro in PRIVATE/PUBLIC list
if p.at(token.AMPERSAND) {
macroExpr := p.parseMacro()
var init ast.Expr
if p.match(token.ASSIGN) {
init = p.parseExpr()
}
name := "macro"
if me, ok := macroExpr.(*ast.MacroExpr); ok {
if id, ok2 := me.Expr.(*ast.IdentExpr); ok2 {
name = id.Name
}
}
vars = append(vars, &ast.VarInit{NamePos: macroExpr.Pos(), Name: name, Init: init})
if !p.match(token.COMMA) {
break
}
continue
}
name := p.expectMethodName() // allow keywords as var names (MEMVAR, etc.)
var init ast.Expr
// Skip AS type declaration
if p.match(token.AS) {
for p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF &&
p.current.Kind != token.ASSIGN && p.current.Kind != token.COMMA {
p.advance()
}
}
if p.match(token.ASSIGN) {
init = p.parseExpr()
}
vars = append(vars, &ast.VarInit{NamePos: name.Pos, Name: name.Literal, Init: init})
if !p.match(token.COMMA) {
break
}
}
p.expectEndOfStmt()
return &ast.VarDecl{DeclPos: tok.Pos, Scope: scope, Vars: vars}
}
// --- xBase commands ---
func (p *Parser) parseUse() *ast.UseCmd {
pos := p.expect(token.USE).Pos
var file ast.Expr
var via, alias string
var aliasExprNode ast.Expr
var shared, readOnly bool
// USE without args = close
if p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
// 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 <name> 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
}