Classic Clipper/Harbour form writes method implementations as bare
`METHOD Name(params)` statements following a `CLASS X ... ENDCLASS`
declaration, with the binding inferred from the most recent class:
CREATE CLASS Shape
METHOD Area
ENDCLASS
METHOD Area -- binds to Shape
RETURN 0
Five was requiring `METHOD Area CLASS Shape` explicitly. Without it,
parseMethodDecl left MethodDecl.ClassName empty, gengo skipped the
body emission, and the link step failed with `undefined: HB_SHAPE_AREA`.
The class registration had AddMethod("AREA", HB_SHAPE_AREA) pointing
at the missing symbol.
Parser tracks p.lastClassName at parseClassDecl, and parseMethodDecl
falls back to that value when no CLASS clause is supplied. Each new
CLASS declaration updates the tracker, so multi-class files still
dispatch correctly — verified with /tmp/test_implicit_class.prg
(Shape + Box both resolve their own Name/Area methods).
Unblocks harbour-core/tests/clsscope.prg and other OOP compat
tests that use this form. FiveSql2 43/43, Harbour compat 56/56,
Go test ALL PASS.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2451 lines
65 KiB
Go
2451 lines
65 KiB
Go
// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
|
|
// All rights reserved.
|
|
|
|
// Recursive descent parser for the Five language (Harbour-compatible).
|
|
//
|
|
// Design references:
|
|
// - Harbour: harbour.y (Bison grammar, /mnt/d/harbour-core/src/compiler/harbour.y)
|
|
// Key: operator precedence is mostly right-associative, = is both assign and compare
|
|
// - tsgo: internal/parser/ — recursive descent with precedence climbing
|
|
// - Pratt parser for expression parsing (precedence climbing)
|
|
//
|
|
// Harbour-specific rules enforced:
|
|
// - LOCAL/STATIC/FIELD declarations must be at function top (before executable code)
|
|
// - ? and ?? are QOut/QQOut shorthand
|
|
// - (expr)->field is dynamic alias access
|
|
// - &variable is macro expansion
|
|
package parser
|
|
|
|
import (
|
|
"five/compiler/ast"
|
|
"five/compiler/lexer"
|
|
"five/compiler/token"
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// Parser parses Five/Harbour source into an AST.
|
|
type Parser struct {
|
|
tokens []token.Token
|
|
pos int
|
|
current token.Token
|
|
errors []Error
|
|
file string
|
|
GoDumps []string // inline Go code blocks from PP
|
|
// lastClassName tracks the most recent `CLASS X` / `CREATE CLASS X`
|
|
// declaration at file scope. Standalone `METHOD foo(...)` decls
|
|
// without an explicit `CLASS Y` clause default to this name —
|
|
// matches classic Clipper/Harbour where method bodies follow the
|
|
// class declaration and bind implicitly.
|
|
lastClassName string
|
|
}
|
|
|
|
// Error represents a parse error with position.
|
|
type Error struct {
|
|
Pos token.Position
|
|
Message string
|
|
}
|
|
|
|
func (e Error) Error() string {
|
|
return fmt.Sprintf("%s: %s", e.Pos, e.Message)
|
|
}
|
|
|
|
// Parse parses a source file and returns the AST.
|
|
func Parse(filename, source string) (*ast.File, []Error) {
|
|
return ParseWithGoDumps(filename, source, nil)
|
|
}
|
|
|
|
// ParseWithGoDumps parses with inline Go code blocks from PP.
|
|
func ParseWithGoDumps(filename, source string, goDumps []string) (*ast.File, []Error) {
|
|
tokens := lexer.Tokenize(filename, source)
|
|
p := &Parser{
|
|
tokens: tokens,
|
|
pos: 0,
|
|
file: filename,
|
|
GoDumps: goDumps,
|
|
}
|
|
if len(tokens) > 0 {
|
|
p.current = tokens[0]
|
|
}
|
|
|
|
file := p.parseFile()
|
|
return file, p.errors
|
|
}
|
|
|
|
// --- Token navigation ---
|
|
|
|
func (p *Parser) peek() token.Kind {
|
|
return p.current.Kind
|
|
}
|
|
|
|
// currentUpper returns the uppercased literal of the current token.
|
|
// Uses strings.EqualFold for comparisons where possible to avoid allocation.
|
|
func (p *Parser) currentUpper() string {
|
|
return strings.ToUpper(p.current.Literal)
|
|
}
|
|
|
|
// peekAt returns the token kind at offset from current position.
|
|
// peekAt(0) = current, peekAt(1) = next, etc. Returns EOF if out of range.
|
|
func (p *Parser) peekAt(offset int) token.Kind {
|
|
idx := p.pos + offset
|
|
if idx >= 0 && idx < len(p.tokens) {
|
|
return p.tokens[idx].Kind
|
|
}
|
|
return token.EOF
|
|
}
|
|
|
|
// peekLitAt returns the token literal at offset from current position.
|
|
func (p *Parser) peekLitAt(offset int) string {
|
|
idx := p.pos + offset
|
|
if idx >= 0 && idx < len(p.tokens) {
|
|
return p.tokens[idx].Literal
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// peekTokenAt returns the full token at offset. Safe for out-of-range.
|
|
func (p *Parser) peekTokenAt(offset int) token.Token {
|
|
idx := p.pos + offset
|
|
if idx >= 0 && idx < len(p.tokens) {
|
|
return p.tokens[idx]
|
|
}
|
|
return token.Token{Kind: token.EOF}
|
|
}
|
|
|
|
func (p *Parser) advance() token.Token {
|
|
tok := p.current
|
|
p.pos++
|
|
if p.pos < len(p.tokens) {
|
|
p.current = p.tokens[p.pos]
|
|
} else {
|
|
p.current = token.Token{Kind: token.EOF}
|
|
}
|
|
return tok
|
|
}
|
|
|
|
func (p *Parser) match(kind token.Kind) bool {
|
|
if p.current.Kind == kind {
|
|
p.advance()
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (p *Parser) expect(kind token.Kind) token.Token {
|
|
if p.current.Kind == kind {
|
|
return p.advance()
|
|
}
|
|
p.error(fmt.Sprintf("expected %v, got %v %q", kind, p.current.Kind, p.current.Literal))
|
|
// Error recovery: return a synthetic token and try to continue
|
|
return token.Token{Kind: kind, Pos: p.current.Pos}
|
|
}
|
|
|
|
func (p *Parser) at(kind token.Kind) bool {
|
|
return p.current.Kind == kind
|
|
}
|
|
|
|
func (p *Parser) atAny(kinds ...token.Kind) bool {
|
|
for _, k := range kinds {
|
|
if p.current.Kind == k {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// --- Error handling & recovery ---
|
|
|
|
func (p *Parser) error(msg string) {
|
|
// Avoid duplicate errors at same position
|
|
if len(p.errors) > 0 {
|
|
last := p.errors[len(p.errors)-1]
|
|
if last.Pos == p.current.Pos {
|
|
return
|
|
}
|
|
}
|
|
p.errors = append(p.errors, Error{
|
|
Pos: p.current.Pos,
|
|
Message: msg,
|
|
})
|
|
}
|
|
|
|
// syncStmt skips tokens until a statement boundary is found.
|
|
// Called after errors to prevent cascading parse failures.
|
|
func (p *Parser) syncStmt() {
|
|
for p.current.Kind != token.EOF {
|
|
// Newline = statement boundary in Harbour
|
|
if p.current.Kind == token.NEWLINE {
|
|
p.advance()
|
|
return
|
|
}
|
|
// Statement-starting keywords = safe to resume
|
|
switch p.current.Kind {
|
|
case token.FUNCTION_KW, token.PROCEDURE, token.CLASS, token.METHOD,
|
|
token.LOCAL, token.STATIC, token.PRIVATE, token.PUBLIC,
|
|
token.IF, token.FOR, token.DO, token.WHILE, token.SWITCH,
|
|
token.BEGIN, token.RETURN, token.QMARK, token.QQMARK:
|
|
return
|
|
}
|
|
p.advance()
|
|
}
|
|
}
|
|
|
|
// skipNewlines consumes consecutive NEWLINE tokens.
|
|
func (p *Parser) skipNewlines() {
|
|
for p.current.Kind == token.NEWLINE {
|
|
p.advance()
|
|
}
|
|
}
|
|
|
|
// expectEndOfStmt expects a NEWLINE or EOF (statement terminator).
|
|
func (p *Parser) expectEndOfStmt() {
|
|
if p.current.Kind == token.NEWLINE {
|
|
p.advance()
|
|
return
|
|
}
|
|
if p.current.Kind == token.EOF {
|
|
return
|
|
}
|
|
// Don't error for some keywords that start new statements
|
|
// (Harbour allows missing newline before certain keywords)
|
|
}
|
|
|
|
// --- File parsing ---
|
|
|
|
func (p *Parser) parseFile() *ast.File {
|
|
file := &ast.File{Name: p.file}
|
|
p.skipNewlines()
|
|
|
|
for p.current.Kind != token.EOF {
|
|
switch p.current.Kind {
|
|
case token.FUNCTION_KW, token.PROCEDURE:
|
|
file.Decls = append(file.Decls, p.parseFuncDecl())
|
|
case token.EXIT:
|
|
// EXIT PROCEDURE/FUNCTION at top level
|
|
p.advance()
|
|
if p.current.Kind == token.PROCEDURE || p.current.Kind == token.FUNCTION_KW {
|
|
file.Decls = append(file.Decls, p.parseFuncDecl())
|
|
} else {
|
|
p.skipToEndOfLine()
|
|
}
|
|
case token.CLASS:
|
|
file.Decls = append(file.Decls, p.parseClassDecl())
|
|
case token.METHOD:
|
|
file.Decls = append(file.Decls, p.parseMethodDecl())
|
|
case token.STATIC:
|
|
// STATIC FUNCTION/PROCEDURE → function declaration with static scope
|
|
if p.peekAt(1) == token.FUNCTION_KW || p.peekAt(1) == token.PROCEDURE {
|
|
p.advance() // skip STATIC
|
|
file.Decls = append(file.Decls, p.parseFuncDecl())
|
|
} else {
|
|
// Top-level STATIC declaration (module-level variable)
|
|
file.Decls = append(file.Decls, p.parseVarDecl())
|
|
}
|
|
case token.IDENT:
|
|
upper := p.currentUpper()
|
|
if upper == "THREAD" && p.peekAt(1) == token.STATIC {
|
|
// THREAD STATIC → treat as STATIC
|
|
p.advance() // skip THREAD
|
|
file.Decls = append(file.Decls, p.parseVarDecl())
|
|
} else if upper == "CREATE" && p.peekAt(1) == token.CLASS {
|
|
// CREATE CLASS → treat as CLASS
|
|
p.advance() // skip CREATE
|
|
file.Decls = append(file.Decls, p.parseClassDecl())
|
|
} else if upper == "EXTERNAL" || upper == "EXTERN" || upper == "REQUEST" || upper == "ANNOUNCE" || upper == "DYNAMIC" || upper == "DECLARE" {
|
|
p.skipToEndOfLine()
|
|
} else if upper == "INIT" || upper == "EXIT" {
|
|
// INIT PROCEDURE/FUNCTION or EXIT PROCEDURE/FUNCTION
|
|
p.advance() // skip INIT/EXIT
|
|
if p.current.Kind == token.PROCEDURE || p.current.Kind == token.FUNCTION_KW {
|
|
file.Decls = append(file.Decls, p.parseFuncDecl())
|
|
} else {
|
|
p.skipToEndOfLine()
|
|
}
|
|
} else if upper == "SET" {
|
|
// Top-level SET commands (SET PROCEDURE TO, etc.)
|
|
p.skipToEndOfLine()
|
|
} else if upper == "FIVE_GODUMP__" {
|
|
// Inline Go code block from #pragma BEGINDUMP
|
|
file.Decls = append(file.Decls, p.parseGoDump())
|
|
} else {
|
|
// Unknown IDENT at top level — PP residual or bare expression, skip
|
|
p.skipToEndOfLine()
|
|
}
|
|
case token.MEMVAR, token.DECLARE:
|
|
// File-level MEMVAR/DECLARE declaration — skip
|
|
p.skipToEndOfLine()
|
|
case token.IMPORT:
|
|
file.Imports = append(file.Imports, p.parseImport())
|
|
case token.PP_INCLUDE, token.PP_DEFINE, token.PP_IFDEF, token.PP_IFNDEF,
|
|
token.PP_ELSE, token.PP_ENDIF, token.PP_PRAGMA, token.PP_UNDEF,
|
|
token.PP_COMMAND, token.PP_TRANSLATE:
|
|
p.skipToEndOfLine() // skip preprocessor for now
|
|
default:
|
|
// Bare expression or PP residual at top level — skip to EOL
|
|
// This handles: PP #command residuals, bare function calls, etc.
|
|
p.skipToEndOfLine()
|
|
}
|
|
p.skipNewlines()
|
|
}
|
|
|
|
return file
|
|
}
|
|
|
|
func (p *Parser) skipToEndOfLine() {
|
|
for p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
|
|
p.advance()
|
|
}
|
|
if p.current.Kind == token.NEWLINE {
|
|
p.advance()
|
|
}
|
|
}
|
|
|
|
// consumeFileExtension handles filename.ext (e.g. test.dbf, test.idx)
|
|
// where the dot+extension was parsed as separate tokens after parseExpr.
|
|
func (p *Parser) consumeFileExtension(fileExpr ast.Expr) {
|
|
for p.current.Kind == token.DOT &&
|
|
(p.peekAt(1) == token.IDENT || p.peekAt(1) == token.INT) {
|
|
p.advance() // skip .
|
|
ext := p.advance() // skip extension
|
|
if ident, ok := fileExpr.(*ast.IdentExpr); ok {
|
|
ident.Name = ident.Name + "." + ext.Literal
|
|
}
|
|
}
|
|
}
|
|
|
|
// parseGoDump parses FIVE_GODUMP__ <index> marker from PP.
|
|
func (p *Parser) parseGoDump() *ast.GoDumpDecl {
|
|
pos := p.advance().Pos // consume FIVE_GODUMP__
|
|
idx := 0
|
|
if p.current.Kind == token.INT {
|
|
fmt.Sscanf(p.current.Literal, "%d", &idx)
|
|
p.advance()
|
|
}
|
|
p.expectEndOfStmt()
|
|
code := ""
|
|
if idx >= 0 && idx < len(p.GoDumps) {
|
|
code = p.GoDumps[idx]
|
|
}
|
|
return &ast.GoDumpDecl{DumpPos: pos, Code: code}
|
|
}
|
|
|
|
// --- IMPORT ---
|
|
|
|
func (p *Parser) parseImport() *ast.ImportDecl {
|
|
pos := p.expect(token.IMPORT).Pos
|
|
alias := ""
|
|
// IMPORT _ "pkg" — blank import
|
|
// IMPORT alias "pkg" — aliased import
|
|
if p.current.Kind == token.IDENT && p.peekAt(1) == token.STRING {
|
|
alias = p.advance().Literal
|
|
}
|
|
pathTok := p.expect(token.STRING)
|
|
p.expectEndOfStmt()
|
|
return &ast.ImportDecl{ImportPos: pos, Alias: alias, Path: pathTok.Literal}
|
|
}
|
|
|
|
// --- FUNCTION / PROCEDURE ---
|
|
|
|
func (p *Parser) parseFuncDecl() *ast.FuncDecl {
|
|
tok := p.advance() // FUNCTION or PROCEDURE
|
|
isProc := tok.Kind == token.PROCEDURE
|
|
|
|
// Allow keywords as function names (GOTO, END, etc.)
|
|
nameTok := p.expectMethodName()
|
|
|
|
// Parameters
|
|
var params []*ast.ParamDecl
|
|
if p.match(token.LPAREN) {
|
|
params = p.parseParamList()
|
|
p.expect(token.RPAREN)
|
|
}
|
|
|
|
// Skip CLASS className qualifier (PROCEDURE name CLASS className)
|
|
if p.current.Kind == token.CLASS {
|
|
p.advance() // skip CLASS
|
|
p.expectMethodName() // skip className
|
|
}
|
|
p.expectEndOfStmt()
|
|
|
|
// Declarations (LOCAL, STATIC, FIELD — must come first)
|
|
var decls []ast.Decl
|
|
p.skipNewlines()
|
|
for p.atAny(token.LOCAL, token.STATIC, token.FIELD, token.MEMVAR, token.PARAMETERS) {
|
|
decls = append(decls, p.parseVarDecl())
|
|
p.skipNewlines()
|
|
}
|
|
|
|
// Body (executable statements until RETURN or end)
|
|
body := p.parseStmtBlock(token.RETURN, token.FUNCTION_KW, token.PROCEDURE, token.CLASS, token.METHOD, token.EOF)
|
|
|
|
// Consume RETURN if present
|
|
var endPos token.Position
|
|
if p.current.Kind == token.RETURN {
|
|
retStmt := p.parseReturn()
|
|
body = append(body, retStmt)
|
|
endPos = retStmt.Pos()
|
|
} else {
|
|
endPos = p.current.Pos
|
|
}
|
|
|
|
return &ast.FuncDecl{
|
|
FuncPos: tok.Pos,
|
|
Name: nameTok.Literal,
|
|
IsProc: isProc,
|
|
Params: params,
|
|
Decls: decls,
|
|
Body: body,
|
|
EndPos: endPos,
|
|
}
|
|
}
|
|
|
|
func (p *Parser) parseParamList() []*ast.ParamDecl {
|
|
var params []*ast.ParamDecl
|
|
if p.at(token.RPAREN) {
|
|
return params
|
|
}
|
|
// Variadic: FUNCTION Foo(...) — skip dots, use PCount() at runtime
|
|
if p.at(token.DOT) {
|
|
for p.at(token.DOT) {
|
|
p.advance()
|
|
}
|
|
return params
|
|
}
|
|
for {
|
|
byRef := p.match(token.AT)
|
|
// Handle trailing ... after named params: FUNCTION Foo(a, b, ...)
|
|
if p.at(token.DOT) {
|
|
for p.at(token.DOT) {
|
|
p.advance()
|
|
}
|
|
break
|
|
}
|
|
name := p.expectMethodName() // allow keywords as param names (e.g. data, type)
|
|
var asType string
|
|
if p.match(token.AS) {
|
|
asType = p.expectMethodName().Literal
|
|
}
|
|
params = append(params, &ast.ParamDecl{
|
|
NamePos: name.Pos,
|
|
Name: name.Literal,
|
|
ByRef: byRef,
|
|
AsType: asType,
|
|
})
|
|
if !p.match(token.COMMA) {
|
|
break
|
|
}
|
|
}
|
|
return params
|
|
}
|
|
|
|
// --- Variable declarations ---
|
|
|
|
func (p *Parser) parseVarDecl() *ast.VarDecl {
|
|
tok := p.advance() // LOCAL, STATIC, FIELD, MEMVAR
|
|
var scope ast.VarScope
|
|
switch tok.Kind {
|
|
case token.LOCAL:
|
|
scope = ast.ScopeLocal
|
|
case token.STATIC:
|
|
scope = ast.ScopeStatic
|
|
case token.FIELD:
|
|
scope = ast.ScopeField
|
|
case token.MEMVAR:
|
|
// MEMVAR declares field aliases, treated like FIELD
|
|
scope = ast.ScopeField
|
|
default:
|
|
scope = ast.ScopeLocal
|
|
}
|
|
|
|
var vars []*ast.VarInit
|
|
for {
|
|
// Allow keywords as variable names (data, field, etc.)
|
|
name := p.expectMethodName()
|
|
var init ast.Expr
|
|
var asType string
|
|
|
|
// LOCAL a[10] or LOCAL a[5,3] — array declaration
|
|
if p.at(token.LBRACKET) {
|
|
p.advance() // skip [
|
|
sizeExpr := p.parseExpr()
|
|
// Multi-dim: a[5,3]
|
|
for p.match(token.COMMA) {
|
|
p.parseExpr() // consume additional dimensions (simplified)
|
|
}
|
|
p.expect(token.RBRACKET)
|
|
// Generate init as Array(size)
|
|
init = &ast.CallExpr{
|
|
Func: &ast.IdentExpr{Name: "Array"},
|
|
Args: []ast.Expr{sizeExpr},
|
|
}
|
|
}
|
|
// AS type (may come before or after :=)
|
|
if p.match(token.AS) {
|
|
// Skip type: AS STRING, AS NUMERIC, AS CLASS ClassName, AS ARRAY OF type
|
|
for p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF &&
|
|
p.current.Kind != token.ASSIGN && p.current.Kind != token.COMMA {
|
|
p.advance()
|
|
}
|
|
}
|
|
if p.match(token.ASSIGN) { // :=
|
|
init = p.parseExpr()
|
|
}
|
|
// AS type after := (alternative order)
|
|
if p.match(token.AS) {
|
|
asType = p.expectMethodName().Literal
|
|
// Skip complex AS: AS CLASS ClassName, AS ARRAY OF...
|
|
for p.current.Kind == token.IDENT {
|
|
upper := p.currentUpper()
|
|
if upper == "OF" || upper == "CLASS" {
|
|
p.advance()
|
|
if p.current.Kind == token.IDENT || p.current.Literal != "" {
|
|
p.advance()
|
|
}
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
vars = append(vars, &ast.VarInit{
|
|
NamePos: name.Pos,
|
|
Name: name.Literal,
|
|
Init: init,
|
|
AsType: asType,
|
|
})
|
|
|
|
if !p.match(token.COMMA) {
|
|
break
|
|
}
|
|
}
|
|
|
|
p.expectEndOfStmt()
|
|
return &ast.VarDecl{DeclPos: tok.Pos, Scope: scope, Vars: vars}
|
|
}
|
|
|
|
// --- CLASS ---
|
|
|
|
func (p *Parser) parseClassDecl() *ast.ClassDecl {
|
|
classPos := p.expect(token.CLASS).Pos
|
|
name := p.expect(token.IDENT).Literal
|
|
p.lastClassName = name
|
|
|
|
var parent string
|
|
if p.match(token.INHERIT) {
|
|
p.match(token.FROM) // optional FROM
|
|
parent = p.expectMethodName().Literal
|
|
}
|
|
// Alternative: FROM without INHERIT
|
|
if parent == "" && p.match(token.FROM) {
|
|
parent = p.expectMethodName().Literal
|
|
}
|
|
// Multiple inheritance: FROM class1, class2, class3 — skip extra parents
|
|
for p.match(token.COMMA) {
|
|
p.expectMethodName() // skip additional parent class name
|
|
}
|
|
p.expectEndOfStmt()
|
|
|
|
var members []ast.Decl
|
|
p.skipNewlines()
|
|
for !p.atAny(token.ENDCLASS, token.END, token.EOF) {
|
|
switch p.current.Kind {
|
|
case token.DATA:
|
|
for _, dd := range p.parseDataDecl() { members = append(members, dd) }
|
|
case token.METHOD:
|
|
members = append(members, p.parseClassMethodDecl())
|
|
case token.ACCESS:
|
|
members = append(members, p.parseAccessDecl())
|
|
case token.ASSIGN_KW:
|
|
members = append(members, p.parseAssignDecl())
|
|
case token.CLASS:
|
|
// CLASS VAR / CLASS METHOD / CLASS DATA inside class body
|
|
p.advance() // skip CLASS
|
|
if p.current.Kind == token.DATA {
|
|
for _, dd := range p.parseDataDecl() { members = append(members, dd) }
|
|
} else if p.current.Kind == token.METHOD {
|
|
members = append(members, p.parseClassMethodDecl())
|
|
} else if p.current.Kind == token.IDENT && p.currentUpper() == "VAR" {
|
|
p.tokens[p.pos].Kind = token.DATA
|
|
p.current = p.tokens[p.pos]
|
|
for _, dd := range p.parseDataDecl() { members = append(members, dd) }
|
|
} else {
|
|
p.skipToEndOfLine()
|
|
}
|
|
case token.OPERATOR_KW:
|
|
// OPERATOR "<sym>" ARG <name> INLINE <expr>
|
|
if md := p.parseOperatorDecl(); md != nil {
|
|
members = append(members, md)
|
|
}
|
|
case token.INLINE_KW, token.ON, token.DESTRUCTOR:
|
|
// Stray INLINE, ON ERROR, DESTRUCTOR — skip to EOL
|
|
p.skipToEndOfLine()
|
|
p.skipNewlines()
|
|
continue
|
|
case token.IDENT:
|
|
upper := p.currentUpper()
|
|
// FRIEND FUNCTION/CLASS — skip declaration
|
|
if upper == "FRIEND" {
|
|
p.advance() // skip FRIEND
|
|
p.skipToEndOfLine()
|
|
p.skipNewlines()
|
|
continue
|
|
}
|
|
// VAR = DATA synonym
|
|
if upper == "VAR" {
|
|
// Rewrite as DATA token
|
|
p.tokens[p.pos].Kind = token.DATA
|
|
p.current = p.tokens[p.pos]
|
|
for _, dd := range p.parseDataDecl() { members = append(members, dd) }
|
|
} else if (upper == "PROTECTED" || upper == "EXPORTED" || upper == "HIDDEN" ||
|
|
upper == "VISIBLE" || upper == "EXPORT" || upper == "SYNC") &&
|
|
p.peekAt(1) == token.COLON {
|
|
// Scope qualifier — skip it (Five doesn't enforce visibility)
|
|
p.advance() // skip keyword
|
|
p.advance() // skip :
|
|
p.skipNewlines()
|
|
} else if upper == "CLASSDATA" || upper == "CLASSVAR" {
|
|
// CLASSDATA / CLASSVAR — class-level variable (treat as DATA)
|
|
p.tokens[p.pos].Kind = token.DATA
|
|
p.current = p.tokens[p.pos]
|
|
for _, dd := range p.parseDataDecl() { members = append(members, dd) }
|
|
} else if upper == "CLASS" {
|
|
// CLASS VAR — class-level variable
|
|
p.advance() // skip CLASS
|
|
if p.current.Kind == token.IDENT && p.currentUpper() == "VAR" {
|
|
p.tokens[p.pos].Kind = token.DATA
|
|
p.current = p.tokens[p.pos]
|
|
for _, dd := range p.parseDataDecl() { members = append(members, dd) }
|
|
} else if p.current.Kind == token.DATA {
|
|
for _, dd := range p.parseDataDecl() { members = append(members, dd) }
|
|
} else if p.current.Kind == token.METHOD {
|
|
members = append(members, p.parseClassMethodDecl())
|
|
} else {
|
|
p.skipToEndOfLine()
|
|
}
|
|
} else if upper == "MESSAGE" {
|
|
// MESSAGE <name> [(params)] INLINE <expr>
|
|
// Harbour sugar for an inline method. Emit as MethodDecl so
|
|
// emitClassDecl routes it through AddMethod and the inline
|
|
// body emitter generates the HB_<CLASS>_<NAME> function.
|
|
members = append(members, p.parseMessageDecl())
|
|
} else if upper == "OPERATOR" {
|
|
// OPERATOR "<sym>" ARG <name> INLINE <expr>
|
|
if md := p.parseOperatorDecl(); md != nil {
|
|
members = append(members, md)
|
|
}
|
|
} else if upper == "ON" || upper == "DESTRUCTOR" ||
|
|
upper == "DELEGATE" || upper == "ERROR" ||
|
|
upper == "VIRTUAL" || upper == "DEFERRED" {
|
|
// ON ERROR, OPERATOR "+" ARG, DESTRUCTOR, DELEGATE — skip to EOL
|
|
p.skipToEndOfLine()
|
|
p.skipNewlines()
|
|
continue
|
|
} else {
|
|
p.error(fmt.Sprintf("unexpected in CLASS body: %v %q", p.current.Kind, p.current.Literal))
|
|
p.advance()
|
|
}
|
|
default:
|
|
p.error(fmt.Sprintf("unexpected in CLASS body: %v", p.current.Kind))
|
|
p.advance()
|
|
}
|
|
p.skipNewlines()
|
|
}
|
|
|
|
endPos := p.current.Pos
|
|
if p.match(token.ENDCLASS) {
|
|
// ok
|
|
} else if p.match(token.END) {
|
|
// END CLASS — skip optional CLASS keyword
|
|
p.match(token.CLASS)
|
|
}
|
|
p.expectEndOfStmt()
|
|
|
|
return &ast.ClassDecl{
|
|
ClassPos: classPos,
|
|
Name: name,
|
|
ParentName: parent,
|
|
Members: members,
|
|
EndPos: endPos,
|
|
}
|
|
}
|
|
|
|
// parseDataDecl parses `DATA name [AS type] [INIT expr] [qualifiers]`,
|
|
// with optional comma-separated additional names each carrying their
|
|
// own AS / INIT / qualifier run — Harbour syntax:
|
|
//
|
|
// DATA x, y, z -- three decls, no init
|
|
// DATA x INIT 10, y, z INIT 0 -- init attaches to preceding name
|
|
// DATA cName AS CHARACTER INIT "" -- typed single decl
|
|
//
|
|
// Returns the full list; callers flatten it into the class member
|
|
// slice. Previously only the first name survived — the loop skipped
|
|
// extra names after a comma without creating decls for them.
|
|
func (p *Parser) parseDataDecl() []*ast.DataDecl {
|
|
dataPos := p.expect(token.DATA).Pos
|
|
|
|
current := &ast.DataDecl{
|
|
DataPos: dataPos,
|
|
Name: p.expectMethodName().Literal,
|
|
}
|
|
out := []*ast.DataDecl{}
|
|
|
|
for p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
|
|
if p.match(token.AS) {
|
|
if p.current.Kind == token.IDENT || p.current.Literal != "" {
|
|
current.AsType = p.current.Literal
|
|
p.advance()
|
|
}
|
|
continue
|
|
}
|
|
if p.match(token.COMMA) {
|
|
out = append(out, current)
|
|
if p.current.Kind == token.IDENT || p.current.Literal != "" {
|
|
current = &ast.DataDecl{
|
|
DataPos: p.current.Pos,
|
|
Name: p.current.Literal,
|
|
}
|
|
p.advance()
|
|
} else {
|
|
// Trailing comma or missing name — bail cleanly.
|
|
current = nil
|
|
break
|
|
}
|
|
continue
|
|
}
|
|
if p.current.Kind == token.IDENT {
|
|
upper := p.currentUpper()
|
|
if upper == "INIT" {
|
|
p.advance()
|
|
current.Init = p.parseExpr()
|
|
continue
|
|
}
|
|
// Skip visibility/attribute qualifiers
|
|
if upper == "READONLY" || upper == "EXPORTED" || upper == "PROTECTED" ||
|
|
upper == "HIDDEN" || upper == "SYNC" || upper == "USUAL" ||
|
|
upper == "PROPERTY" || upper == "PERSISTENT" || upper == "SHARED" {
|
|
p.advance()
|
|
continue
|
|
}
|
|
}
|
|
if p.current.Kind == token.INLINE_KW {
|
|
p.skipToEndOfLine()
|
|
break
|
|
}
|
|
break
|
|
}
|
|
if current != nil {
|
|
out = append(out, current)
|
|
}
|
|
p.expectEndOfStmt()
|
|
|
|
return out
|
|
}
|
|
|
|
// expectMethodName: method names can be keywords (end, home, left, right, etc.)
|
|
func (p *Parser) expectMethodName() token.Token {
|
|
if p.current.Kind == token.IDENT {
|
|
return p.advance()
|
|
}
|
|
// Allow keywords as method names
|
|
if p.current.Literal != "" {
|
|
return p.advance()
|
|
}
|
|
return p.expect(token.IDENT)
|
|
}
|
|
|
|
func (p *Parser) parseClassMethodDecl() *ast.MethodDecl {
|
|
methodPos := p.expect(token.METHOD).Pos
|
|
|
|
// Skip optional FUNCTION/PROCEDURE qualifier
|
|
if p.current.Kind == token.FUNCTION_KW || p.current.Kind == token.PROCEDURE {
|
|
p.advance()
|
|
}
|
|
|
|
name := p.expectMethodName().Literal
|
|
|
|
var params []*ast.ParamDecl
|
|
if p.match(token.LPAREN) {
|
|
params = p.parseParamList()
|
|
p.expect(token.RPAREN)
|
|
}
|
|
|
|
// Check for SETGET keyword
|
|
isSetGet := false
|
|
if p.current.Kind == token.IDENT && p.currentUpper() == "SETGET" {
|
|
p.advance()
|
|
isSetGet = true
|
|
}
|
|
|
|
// Skip trailing qualifiers: OPERATOR, VIRTUAL, CONSTRUCTOR, etc.
|
|
if p.current.Kind == token.IDENT {
|
|
upper := p.currentUpper()
|
|
if upper == "OPERATOR" || upper == "VIRTUAL" || upper == "DEFERRED" {
|
|
p.skipToEndOfLine()
|
|
}
|
|
}
|
|
if p.current.Kind == token.OPERATOR_KW || p.current.Kind == token.CONSTRUCTOR {
|
|
p.skipToEndOfLine()
|
|
}
|
|
|
|
// INLINE expression — parse as the method body instead of skipping.
|
|
// Harbour semantics: `METHOD X() INLINE expr` is sugar for a method
|
|
// whose body is `RETURN expr`.
|
|
isInline := false
|
|
var inlineBody ast.Expr
|
|
if p.current.Kind == token.INLINE_KW ||
|
|
(p.current.Kind == token.IDENT && p.currentUpper() == "INLINE") {
|
|
p.advance() // consume INLINE
|
|
isInline = true
|
|
inlineBody = p.parseExpr()
|
|
}
|
|
|
|
p.expectEndOfStmt()
|
|
|
|
return &ast.MethodDecl{
|
|
MethodPos: methodPos,
|
|
Name: name,
|
|
Params: params,
|
|
IsSetGet: isSetGet,
|
|
IsInline: isInline,
|
|
InlineBody: inlineBody,
|
|
EndPos: methodPos,
|
|
}
|
|
}
|
|
|
|
// operatorNameIndex maps a Harbour operator symbol to its slot in
|
|
// hbrt.ClassDef.Operators. Values mirror hbrt.Op* constants
|
|
// (hbrt/class.go:50-73).
|
|
//
|
|
// `=` and `==` both route through VM Thread.Equal(), which dispatches
|
|
// on OpEqual (8) — Five doesn't distinguish them at the VM level, so
|
|
// `==` maps to the same slot. `!=`, `<>`, `#` are all OpNotEqual (10).
|
|
var operatorNameIndex = map[string]int{
|
|
"+": 0, "-": 1, "*": 2, "/": 3, "%": 4, "^": 5,
|
|
"++": 6, "--": 7,
|
|
"=": 8, "==": 8, "!=": 10, "<>": 10, "#": 10,
|
|
"<": 11, "<=": 12, ">": 13, ">=": 14,
|
|
":=": 15, "$": 16,
|
|
}
|
|
|
|
// parseOperatorDecl parses:
|
|
// OPERATOR "<sym>" [ARG <name>] INLINE <expr>
|
|
//
|
|
// The ARG binds the RHS operand to a local when the operator is
|
|
// dispatched via the VM's binary op. INLINE <expr> is the body.
|
|
// Harbour also allows `OPERATOR "<sym>" ... METHOD foo CLASS X`
|
|
// (non-inline) but that form is rare; we only support INLINE for
|
|
// now and skip the line otherwise.
|
|
func (p *Parser) parseOperatorDecl() *ast.MethodDecl {
|
|
opPos := p.current.Pos
|
|
p.advance() // OPERATOR
|
|
|
|
if p.current.Kind != token.STRING {
|
|
p.skipToEndOfLine()
|
|
return nil
|
|
}
|
|
symbol := p.current.Literal
|
|
p.advance()
|
|
|
|
opIdx, ok := operatorNameIndex[symbol]
|
|
if !ok {
|
|
p.skipToEndOfLine()
|
|
return nil
|
|
}
|
|
|
|
var params []*ast.ParamDecl
|
|
if p.current.Kind == token.IDENT && p.currentUpper() == "ARG" {
|
|
p.advance()
|
|
argName := p.expectMethodName().Literal
|
|
params = []*ast.ParamDecl{{Name: argName}}
|
|
}
|
|
|
|
if !(p.current.Kind == token.INLINE_KW ||
|
|
(p.current.Kind == token.IDENT && p.currentUpper() == "INLINE")) {
|
|
// Non-inline operator — body arrives as a separate METHOD decl.
|
|
// Unsupported for now; skip.
|
|
p.skipToEndOfLine()
|
|
return nil
|
|
}
|
|
p.advance() // INLINE
|
|
body := p.parseExpr()
|
|
p.expectEndOfStmt()
|
|
|
|
// Synthesise a method named __OP_<idx> so the vtable doesn't collide
|
|
// with user-declared methods. emitClassDecl sees OperatorOp >= 0 and
|
|
// routes registration through AddOperator instead of AddMethod.
|
|
return &ast.MethodDecl{
|
|
MethodPos: opPos,
|
|
Name: fmt.Sprintf("__OP_%d", opIdx),
|
|
Params: params,
|
|
IsInline: true,
|
|
InlineBody: body,
|
|
IsOperator: true,
|
|
OperatorOp: opIdx,
|
|
EndPos: opPos,
|
|
}
|
|
}
|
|
|
|
// parseMessageDecl parses `MESSAGE <name> [(params)] INLINE <expr>` in
|
|
// a CLASS body and returns a MethodDecl. Harbour semantics: a MESSAGE
|
|
// handler is invoked like a method and behaves identically — the form
|
|
// exists mostly for readability of "this ident is really a message
|
|
// handler, not a regular method". Without INLINE the handler is
|
|
// declaration-only (the real body arrives as a separate
|
|
// `METHOD <name>() CLASS X` implementation).
|
|
func (p *Parser) parseMessageDecl() *ast.MethodDecl {
|
|
msgPos := p.current.Pos
|
|
p.advance() // MESSAGE
|
|
|
|
name := p.expectMethodName().Literal
|
|
|
|
var params []*ast.ParamDecl
|
|
if p.match(token.LPAREN) {
|
|
params = p.parseParamList()
|
|
p.expect(token.RPAREN)
|
|
}
|
|
|
|
isInline := false
|
|
var inlineBody ast.Expr
|
|
if p.current.Kind == token.INLINE_KW ||
|
|
(p.current.Kind == token.IDENT && p.currentUpper() == "INLINE") {
|
|
p.advance()
|
|
isInline = true
|
|
inlineBody = p.parseExpr()
|
|
}
|
|
|
|
p.expectEndOfStmt()
|
|
|
|
return &ast.MethodDecl{
|
|
MethodPos: msgPos,
|
|
Name: name,
|
|
Params: params,
|
|
IsInline: isInline,
|
|
InlineBody: inlineBody,
|
|
EndPos: msgPos,
|
|
}
|
|
}
|
|
|
|
// skipClassInline skips INLINE keyword and the rest of the line.
|
|
// Used by ACCESS/ASSIGN decls where inline body handling hasn't been
|
|
// wired up yet (falls back to pre-INLINE-capture behaviour).
|
|
func (p *Parser) skipClassInline() {
|
|
if p.current.Kind == token.INLINE_KW ||
|
|
(p.current.Kind == token.IDENT && p.currentUpper() == "INLINE") {
|
|
p.skipToEndOfLine()
|
|
}
|
|
}
|
|
|
|
// ACCESS name METHOD getterName
|
|
func (p *Parser) parseAccessDecl() *ast.MethodDecl {
|
|
pos := p.expect(token.ACCESS).Pos
|
|
propName := p.expectMethodName().Literal
|
|
|
|
// Skip optional (params)
|
|
if p.match(token.LPAREN) {
|
|
for !p.atAny(token.RPAREN, token.EOF) {
|
|
p.advance()
|
|
}
|
|
p.match(token.RPAREN)
|
|
}
|
|
|
|
methodName := propName
|
|
if p.match(token.METHOD) {
|
|
methodName = p.expectMethodName().Literal
|
|
if p.match(token.LPAREN) {
|
|
for !p.atAny(token.RPAREN, token.EOF) {
|
|
p.advance()
|
|
}
|
|
p.match(token.RPAREN)
|
|
}
|
|
}
|
|
// Skip INLINE + rest of line
|
|
p.skipClassInline()
|
|
p.expectEndOfStmt()
|
|
|
|
return &ast.MethodDecl{
|
|
MethodPos: pos,
|
|
Name: methodName,
|
|
IsAccess: true,
|
|
AccessName: propName,
|
|
EndPos: pos,
|
|
}
|
|
}
|
|
|
|
// ASSIGN name METHOD setterName
|
|
func (p *Parser) parseAssignDecl() *ast.MethodDecl {
|
|
pos := p.expect(token.ASSIGN_KW).Pos
|
|
propName := p.expectMethodName().Literal
|
|
|
|
// Skip optional (params)
|
|
if p.match(token.LPAREN) {
|
|
for !p.atAny(token.RPAREN, token.EOF) {
|
|
p.advance()
|
|
}
|
|
p.match(token.RPAREN)
|
|
}
|
|
|
|
methodName := "_" + propName
|
|
if p.match(token.METHOD) {
|
|
methodName = p.expectMethodName().Literal
|
|
if p.match(token.LPAREN) {
|
|
for !p.atAny(token.RPAREN, token.EOF) {
|
|
p.advance()
|
|
}
|
|
p.match(token.RPAREN)
|
|
}
|
|
}
|
|
// Skip INLINE + rest of line
|
|
p.skipClassInline()
|
|
p.expectEndOfStmt()
|
|
|
|
return &ast.MethodDecl{
|
|
MethodPos: pos,
|
|
Name: methodName,
|
|
IsAssign: true,
|
|
AccessName: propName,
|
|
EndPos: pos,
|
|
}
|
|
}
|
|
|
|
// parseMethodDecl parses standalone: METHOD [FUNCTION|PROCEDURE] name(...) CLASS classname
|
|
func (p *Parser) parseMethodDecl() *ast.MethodDecl {
|
|
methodPos := p.expect(token.METHOD).Pos
|
|
|
|
// Skip optional FUNCTION/PROCEDURE qualifier
|
|
if p.current.Kind == token.FUNCTION_KW || p.current.Kind == token.PROCEDURE {
|
|
p.advance()
|
|
}
|
|
|
|
name := p.expectMethodName().Literal
|
|
|
|
var params []*ast.ParamDecl
|
|
if p.match(token.LPAREN) {
|
|
params = p.parseParamList()
|
|
p.expect(token.RPAREN)
|
|
}
|
|
|
|
var className string
|
|
if p.match(token.CLASS) {
|
|
className = p.expect(token.IDENT).Literal
|
|
} else if p.lastClassName != "" {
|
|
// Implicit binding to the most recent `CREATE CLASS` / `CLASS`
|
|
// declaration — classic Clipper/Harbour form. Real code uses
|
|
// `CREATE CLASS CLSX ... ENDCLASS` followed by a bare
|
|
// `METHOD TestX() ... RETURN Self` without restating the class.
|
|
className = p.lastClassName
|
|
}
|
|
p.expectEndOfStmt()
|
|
|
|
// Declarations
|
|
var decls []ast.Decl
|
|
p.skipNewlines()
|
|
for p.atAny(token.LOCAL, token.STATIC) {
|
|
decls = append(decls, p.parseVarDecl())
|
|
p.skipNewlines()
|
|
}
|
|
|
|
// Body
|
|
body := p.parseStmtBlock(token.RETURN, token.FUNCTION_KW, token.PROCEDURE, token.CLASS, token.METHOD, token.EOF)
|
|
|
|
var endPos token.Position
|
|
if p.current.Kind == token.RETURN {
|
|
retStmt := p.parseReturn()
|
|
body = append(body, retStmt)
|
|
endPos = retStmt.Pos()
|
|
} else {
|
|
endPos = p.current.Pos
|
|
}
|
|
|
|
return &ast.MethodDecl{
|
|
MethodPos: methodPos,
|
|
Name: name,
|
|
ClassName: className,
|
|
Params: params,
|
|
Decls: decls,
|
|
Body: body,
|
|
EndPos: endPos,
|
|
}
|
|
}
|
|
|
|
// --- Statement parsing ---
|
|
|
|
// parseStmtBlock parses statements until one of the stop tokens.
|
|
func (p *Parser) parseStmtBlock(stopTokens ...token.Kind) []ast.Stmt {
|
|
var stmts []ast.Stmt
|
|
for {
|
|
p.skipNewlines()
|
|
if p.current.Kind == token.EOF {
|
|
break
|
|
}
|
|
for _, stop := range stopTokens {
|
|
if p.current.Kind == stop {
|
|
// Don't stop at RETURN if it's used as variable: return := ...
|
|
if stop == token.RETURN &&
|
|
p.peekAt(1) == token.ASSIGN {
|
|
break // continue parsing as statement
|
|
}
|
|
return stmts
|
|
}
|
|
}
|
|
stmt := p.parseStmt()
|
|
if stmt != nil {
|
|
stmts = append(stmts, stmt)
|
|
}
|
|
}
|
|
return stmts
|
|
}
|
|
|
|
func (p *Parser) parseStmt() ast.Stmt {
|
|
// Registry lookup — O(1) dispatch
|
|
if fn := p.lookupStmtParser(); fn != nil {
|
|
return fn(p)
|
|
}
|
|
|
|
// IDENT-based commands (xBase multi-word: COPY, SORT, etc.)
|
|
if p.current.Kind == token.IDENT {
|
|
return p.parseIdentStmt()
|
|
}
|
|
|
|
// Multi-assign: a, b := expr
|
|
if p.looksLikeMultiAssign() {
|
|
return p.parseMultiAssign()
|
|
}
|
|
|
|
// Default: expression statement
|
|
return p.parseExprStmt()
|
|
}
|
|
|
|
|
|
// parseIdentStmt handles IDENT-based commands (xBase multi-word: COPY, SORT, etc.)
|
|
func (p *Parser) parseIdentStmt() ast.Stmt {
|
|
upper := p.currentUpper()
|
|
|
|
// WITH TIMEOUT → timeout context
|
|
if upper == "WITH" && p.peekAt(1) == token.TIMEOUT_KW {
|
|
return p.parseWithTimeout()
|
|
}
|
|
|
|
// xBase commands that consume entire line
|
|
switch upper {
|
|
case "COPY", "SORT", "COUNT", "SUM", "AVERAGE", "TOTAL", "UPDATE",
|
|
"LABEL", "REPORT", "ACCEPT", "INPUT", "LOCATE", "CONTINUE",
|
|
"JOIN", "RELEASE", "SAVE", "RESTORE", "ERASE", "RENAME",
|
|
"RUN", "DIR", "STORE", "NOTE", "TEXT", "ENDTEXT",
|
|
"WITH", "KEYBOARD", "CLEAR", "DISPLAY", "LIST", "REINDEX":
|
|
p.advance()
|
|
for p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
|
|
p.advance()
|
|
}
|
|
p.expectEndOfStmt()
|
|
return &ast.ExprStmt{X: &ast.LiteralExpr{Kind: token.NIL_LIT, Value: "NIL"}}
|
|
|
|
case "COMMIT":
|
|
p.advance()
|
|
p.expectEndOfStmt()
|
|
return &ast.ExprStmt{X: &ast.CallExpr{
|
|
Func: &ast.IdentExpr{Name: "DbCommit"},
|
|
}}
|
|
|
|
case "FIVE_GODUMP__":
|
|
// GoDump is a Decl, wrap as ExprStmt for statement context
|
|
p.advance() // consume FIVE_GODUMP__
|
|
idx := 0
|
|
if p.current.Kind == token.INT {
|
|
fmt.Sscanf(p.current.Literal, "%d", &idx)
|
|
p.advance()
|
|
}
|
|
p.expectEndOfStmt()
|
|
// Store as nil statement — gengo handles GoDumpDecl at file level
|
|
return &ast.ExprStmt{X: &ast.LiteralExpr{Kind: token.NIL_LIT, Value: "NIL"}}
|
|
}
|
|
|
|
// Multi-assign check: a, b := expr
|
|
if p.looksLikeMultiAssign() {
|
|
return p.parseMultiAssign()
|
|
}
|
|
|
|
// Default: expression statement (function call, assignment, etc.)
|
|
return p.parseExprStmt()
|
|
}
|
|
|
|
func (p *Parser) parseExprStmt() ast.Stmt {
|
|
// READ [SAVE] [MSG AT ...] [MSG COLOR ...] — special case
|
|
if p.current.Kind == token.IDENT && p.currentUpper() == "READ" {
|
|
pos := p.advance().Pos
|
|
save := false
|
|
if p.current.Kind == token.IDENT && p.currentUpper() == "SAVE" {
|
|
save = true
|
|
p.advance()
|
|
}
|
|
// Skip optional clauses: MSG AT row,col,col2 / MSG COLOR "..." etc.
|
|
if p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
|
|
p.skipToEndOfLine()
|
|
}
|
|
p.expectEndOfStmt()
|
|
return &ast.ReadCmd{ReadPos: pos, Save: save}
|
|
}
|
|
// TRY / CATCH [oErr] / END — Harbour extension, maps to BEGIN SEQUENCE / RECOVER
|
|
if p.current.Kind == token.IDENT && p.currentUpper() == "TRY" {
|
|
return p.parseTryCatch()
|
|
}
|
|
// CLOSE [DATABASES|ALL] — close work areas
|
|
if p.current.Kind == token.IDENT && p.currentUpper() == "CLOSE" {
|
|
p.advance()
|
|
// Skip optional DATABASES/ALL keyword
|
|
if p.current.Kind == token.IDENT {
|
|
p.advance()
|
|
}
|
|
p.expectEndOfStmt()
|
|
return &ast.ExprStmt{X: &ast.CallExpr{
|
|
Func: &ast.IdentExpr{Name: "DbCloseArea"},
|
|
}}
|
|
}
|
|
// xBase commands that consume entire line (COPY, SORT, COUNT, SUM, etc.)
|
|
if p.current.Kind == token.IDENT {
|
|
// WITH TIMEOUT n / body / ENDWITH
|
|
if p.currentUpper() == "WITH" &&
|
|
p.peekAt(1) == token.TIMEOUT_KW {
|
|
return p.parseWithTimeout()
|
|
}
|
|
switch p.currentUpper() {
|
|
case "COPY", "SORT", "COUNT", "SUM", "AVERAGE", "TOTAL", "UPDATE",
|
|
"LABEL", "REPORT", "ACCEPT", "INPUT", "LOCATE", "CONTINUE",
|
|
"JOIN", "RELEASE", "SAVE", "RESTORE", "ERASE", "RENAME",
|
|
"RUN", "DIR", "STORE", "NOTE", "TEXT", "ENDTEXT",
|
|
"WITH", "KEYBOARD", "CLEAR", "DISPLAY", "LIST", "REINDEX":
|
|
// Consume entire line — these are complex multi-word commands
|
|
p.advance()
|
|
for p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
|
|
p.advance()
|
|
}
|
|
p.expectEndOfStmt()
|
|
return &ast.ExprStmt{X: &ast.LiteralExpr{Kind: token.NIL_LIT, Value: "NIL"}}
|
|
}
|
|
}
|
|
|
|
// COMMIT — flush work area
|
|
if p.current.Kind == token.IDENT && p.currentUpper() == "COMMIT" {
|
|
p.advance()
|
|
p.expectEndOfStmt()
|
|
return &ast.ExprStmt{X: &ast.CallExpr{
|
|
Func: &ast.IdentExpr{Name: "DbCommit"},
|
|
}}
|
|
}
|
|
expr := p.parseExpr()
|
|
|
|
// ch <- value (channel send)
|
|
if p.at(token.ARROW_LEFT) {
|
|
pos := p.advance().Pos
|
|
val := p.parseExpr()
|
|
p.expectEndOfStmt()
|
|
return &ast.ChanSendStmt{ChanPos: pos, Chan: expr, Value: val}
|
|
}
|
|
|
|
p.expectEndOfStmt()
|
|
return &ast.ExprStmt{X: expr}
|
|
}
|
|
|
|
// --- Control flow ---
|
|
|
|
func (p *Parser) parseIf() *ast.IfStmt {
|
|
ifPos := p.expect(token.IF).Pos
|
|
cond := p.parseExpr()
|
|
p.expectEndOfStmt()
|
|
|
|
body := p.parseStmtBlock(token.ELSEIF, token.ELSE, token.ENDIF, token.END)
|
|
|
|
var elseIfs []*ast.ElseIfClause
|
|
for p.current.Kind == token.ELSEIF {
|
|
eiPos := p.advance().Pos
|
|
eiCond := p.parseExpr()
|
|
p.expectEndOfStmt()
|
|
eiBody := p.parseStmtBlock(token.ELSEIF, token.ELSE, token.ENDIF, token.END)
|
|
elseIfs = append(elseIfs, &ast.ElseIfClause{
|
|
ElseIfPos: eiPos,
|
|
Cond: eiCond,
|
|
Body: eiBody,
|
|
})
|
|
}
|
|
|
|
var elseBody []ast.Stmt
|
|
if p.match(token.ELSE) {
|
|
p.expectEndOfStmt()
|
|
elseBody = p.parseStmtBlock(token.ENDIF, token.END)
|
|
}
|
|
|
|
endPos := p.current.Pos
|
|
if !p.match(token.ENDIF) {
|
|
p.match(token.END) // alternative
|
|
}
|
|
p.expectEndOfStmt()
|
|
|
|
return &ast.IfStmt{
|
|
IfPos: ifPos,
|
|
Cond: cond,
|
|
Body: body,
|
|
ElseIfs: elseIfs,
|
|
ElseBody: elseBody,
|
|
EndPos: endPos,
|
|
}
|
|
}
|
|
|
|
// looksLikeIIF checks if IF( starts an IIF-style call: IF(cond, true, false)
|
|
// by scanning for commas inside the parenthesized expression.
|
|
func (p *Parser) looksLikeIIF() bool {
|
|
// Start after IF token; expect ( at p.pos+1
|
|
if p.pos+1 >= len(p.tokens) || p.peekAt(1) != token.LPAREN {
|
|
return false
|
|
}
|
|
depth := 0
|
|
commas := 0
|
|
for i := p.pos + 2; i < len(p.tokens); i++ { // start INSIDE the parens
|
|
switch p.tokens[i].Kind {
|
|
case token.LPAREN, token.LBRACE, token.LBRACKET:
|
|
depth++
|
|
case token.RPAREN:
|
|
if depth == 0 {
|
|
return commas >= 2
|
|
}
|
|
depth--
|
|
case token.RBRACE, token.RBRACKET:
|
|
if depth > 0 {
|
|
depth--
|
|
}
|
|
case token.COMMA:
|
|
if depth == 0 {
|
|
commas++
|
|
}
|
|
case token.NEWLINE, token.EOF:
|
|
return false
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// parseDoProc: DO funcname [WITH arg1, arg2, ...]
|
|
func (p *Parser) parseDoProc() ast.Stmt {
|
|
p.advance() // skip DO
|
|
funcName := p.expectMethodName().Literal
|
|
|
|
var args []ast.Expr
|
|
if p.current.Kind == token.WITH {
|
|
p.advance() // skip WITH
|
|
for {
|
|
args = append(args, p.parseExpr())
|
|
if !p.match(token.COMMA) {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
p.expectEndOfStmt()
|
|
|
|
return &ast.ExprStmt{X: &ast.CallExpr{
|
|
Func: &ast.IdentExpr{Name: funcName},
|
|
Args: args,
|
|
}}
|
|
}
|
|
|
|
func (p *Parser) parseDoWhile() *ast.DoWhileStmt {
|
|
var doPos token.Position
|
|
if p.current.Kind == token.DO {
|
|
doPos = p.advance().Pos
|
|
p.expect(token.WHILE)
|
|
} else {
|
|
// Bare WHILE (Clipper compatibility)
|
|
doPos = p.expect(token.WHILE).Pos
|
|
}
|
|
cond := p.parseExpr()
|
|
p.expectEndOfStmt()
|
|
|
|
body := p.parseStmtBlock(token.ENDDO, token.END)
|
|
|
|
endPos := p.current.Pos
|
|
if !p.match(token.ENDDO) {
|
|
p.match(token.END)
|
|
}
|
|
p.expectEndOfStmt()
|
|
|
|
return &ast.DoWhileStmt{DoPos: doPos, Cond: cond, Body: body, EndPos: endPos}
|
|
}
|
|
|
|
func (p *Parser) parseFor() ast.Stmt {
|
|
forPos := p.expect(token.FOR).Pos
|
|
|
|
// FOR EACH var IN collection
|
|
if p.match(token.EACH) {
|
|
return p.parseForEach(forPos)
|
|
}
|
|
|
|
// FOR var := start TO end [STEP step]
|
|
// Variable can be aliased: M->TEST or simple: i
|
|
varTok := p.expectMethodName()
|
|
varName := varTok.Literal
|
|
// Handle M->varname
|
|
if p.at(token.ARROW) {
|
|
p.advance() // skip ->
|
|
fieldTok := p.expectMethodName()
|
|
varName = fieldTok.Literal
|
|
}
|
|
p.expect(token.ASSIGN) // :=
|
|
start := p.parseExpr()
|
|
p.expect(token.TO)
|
|
toExpr := p.parseExpr()
|
|
|
|
var step ast.Expr
|
|
if p.match(token.STEP) {
|
|
step = p.parseExpr()
|
|
}
|
|
p.expectEndOfStmt()
|
|
|
|
body := p.parseStmtBlock(token.NEXT, token.END)
|
|
nextPos := p.current.Pos
|
|
if !p.match(token.NEXT) {
|
|
p.match(token.END)
|
|
}
|
|
// Skip optional counter variable after NEXT (e.g. NEXT nVar)
|
|
if p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
|
|
p.skipToEndOfLine()
|
|
}
|
|
p.expectEndOfStmt()
|
|
|
|
return &ast.ForStmt{
|
|
ForPos: forPos, Var: varName, Start: start, To: toExpr,
|
|
Step: step, Body: body, NextPos: nextPos,
|
|
}
|
|
}
|
|
|
|
func (p *Parser) parseForEach(forPos token.Position) *ast.ForEachStmt {
|
|
varName := p.expect(token.IDENT).Literal
|
|
// Multi-variable FOR EACH: FOR EACH a, b, c IN x, y, z
|
|
// Skip extra variables — use only first var and first collection
|
|
var extraVars []string
|
|
for p.match(token.COMMA) {
|
|
extraVars = append(extraVars, p.expect(token.IDENT).Literal)
|
|
}
|
|
p.expect(token.IN)
|
|
collection := p.parseExpr()
|
|
// Skip extra collections
|
|
for p.match(token.COMMA) {
|
|
p.parseExpr() // consume and discard
|
|
}
|
|
descend := false
|
|
if p.current.Kind == token.DESCENDING {
|
|
p.advance()
|
|
descend = true
|
|
}
|
|
p.expectEndOfStmt()
|
|
|
|
body := p.parseStmtBlock(token.NEXT, token.END)
|
|
nextPos := p.current.Pos
|
|
if !p.match(token.NEXT) {
|
|
p.match(token.END)
|
|
}
|
|
// Skip optional counter variable after NEXT
|
|
if p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
|
|
p.skipToEndOfLine()
|
|
}
|
|
p.expectEndOfStmt()
|
|
|
|
return &ast.ForEachStmt{
|
|
ForPos: forPos, Var: varName, Collection: collection,
|
|
Descend: descend, Body: body, NextPos: nextPos,
|
|
}
|
|
}
|
|
|
|
// parseDoCase: DO CASE / CASE cond / OTHERWISE / ENDCASE
|
|
// Harbour: equivalent to IF/ELSEIF/ELSE chain
|
|
func (p *Parser) parseDoCase() *ast.IfStmt {
|
|
doPos := p.expect(token.DO).Pos
|
|
p.expect(token.CASE) // consume CASE after DO
|
|
p.expectEndOfStmt()
|
|
p.skipNewlines()
|
|
|
|
// First CASE
|
|
if p.current.Kind != token.CASE {
|
|
p.error("expected CASE after DO CASE")
|
|
return &ast.IfStmt{IfPos: doPos, EndPos: doPos}
|
|
}
|
|
|
|
p.advance() // consume CASE
|
|
cond := p.parseExpr()
|
|
p.expectEndOfStmt()
|
|
body := p.parseStmtBlock(token.CASE, token.OTHERWISE, token.ENDCASE, token.END)
|
|
|
|
// Build as IfStmt with ElseIfs
|
|
var elseIfs []*ast.ElseIfClause
|
|
for p.current.Kind == token.CASE {
|
|
eiPos := p.advance().Pos
|
|
eiCond := p.parseExpr()
|
|
p.expectEndOfStmt()
|
|
eiBody := p.parseStmtBlock(token.CASE, token.OTHERWISE, token.ENDCASE, token.END)
|
|
elseIfs = append(elseIfs, &ast.ElseIfClause{
|
|
ElseIfPos: eiPos,
|
|
Cond: eiCond,
|
|
Body: eiBody,
|
|
})
|
|
}
|
|
|
|
var elseBody []ast.Stmt
|
|
if p.match(token.OTHERWISE) {
|
|
p.expectEndOfStmt()
|
|
elseBody = p.parseStmtBlock(token.ENDCASE, token.END)
|
|
}
|
|
|
|
endPos := p.current.Pos
|
|
if !p.match(token.ENDCASE) {
|
|
p.match(token.END)
|
|
}
|
|
p.expectEndOfStmt()
|
|
|
|
return &ast.IfStmt{
|
|
IfPos: doPos,
|
|
Cond: cond,
|
|
Body: body,
|
|
ElseIfs: elseIfs,
|
|
ElseBody: elseBody,
|
|
EndPos: endPos,
|
|
}
|
|
}
|
|
|
|
func (p *Parser) parseSwitch() *ast.SwitchStmt {
|
|
switchPos := p.expect(token.SWITCH).Pos
|
|
expr := p.parseExpr()
|
|
p.expectEndOfStmt()
|
|
|
|
var cases []*ast.CaseClause
|
|
var otherwise []ast.Stmt
|
|
p.skipNewlines()
|
|
|
|
for p.current.Kind == token.CASE {
|
|
casePos := p.advance().Pos
|
|
val := p.parseExpr()
|
|
p.expectEndOfStmt()
|
|
caseBody := p.parseStmtBlock(token.CASE, token.OTHERWISE, token.ENDSWITCH, token.ENDCASE, token.END)
|
|
cases = append(cases, &ast.CaseClause{CasePos: casePos, Value: val, Body: caseBody})
|
|
}
|
|
|
|
if p.match(token.OTHERWISE) {
|
|
p.expectEndOfStmt()
|
|
otherwise = p.parseStmtBlock(token.ENDSWITCH, token.ENDCASE, token.END)
|
|
}
|
|
|
|
endPos := p.current.Pos
|
|
if !p.match(token.ENDSWITCH) {
|
|
if !p.match(token.ENDCASE) {
|
|
p.match(token.END)
|
|
}
|
|
}
|
|
p.expectEndOfStmt()
|
|
|
|
return &ast.SwitchStmt{SwitchPos: switchPos, Expr: expr, Cases: cases, Otherwise: otherwise, EndPos: endPos}
|
|
}
|
|
|
|
func (p *Parser) parseBeginSequence() *ast.SeqStmt {
|
|
beginPos := p.expect(token.BEGIN).Pos
|
|
p.expect(token.SEQUENCE)
|
|
p.expectEndOfStmt()
|
|
|
|
body := p.parseStmtBlock(token.RECOVER, token.END)
|
|
|
|
var recoverVar string
|
|
var recoverBody []ast.Stmt
|
|
if p.match(token.RECOVER) {
|
|
if p.match(token.USING) {
|
|
recoverVar = p.expect(token.IDENT).Literal
|
|
}
|
|
p.expectEndOfStmt()
|
|
recoverBody = p.parseStmtBlock(token.END)
|
|
}
|
|
|
|
endPos := p.current.Pos
|
|
p.match(token.END)
|
|
p.match(token.SEQUENCE) // optional: END SEQUENCE
|
|
p.expectEndOfStmt()
|
|
|
|
return &ast.SeqStmt{
|
|
BeginPos: beginPos, Body: body,
|
|
RecoverVar: recoverVar, RecoverBody: recoverBody,
|
|
EndPos: endPos,
|
|
}
|
|
}
|
|
|
|
// parseTryCatch: TRY ... CATCH [oErr] ... END — maps to SeqStmt
|
|
func (p *Parser) parseTryCatch() *ast.SeqStmt {
|
|
beginPos := p.advance().Pos // consume TRY
|
|
p.expectEndOfStmt()
|
|
|
|
// Parse body until CATCH or END
|
|
var body []ast.Stmt
|
|
for !p.atAny(token.EOF) {
|
|
if p.current.Kind == token.IDENT && p.currentUpper() == "CATCH" {
|
|
break
|
|
}
|
|
if p.current.Kind == token.END {
|
|
break
|
|
}
|
|
body = append(body, p.parseStmt())
|
|
p.skipNewlines()
|
|
}
|
|
|
|
var recoverVar string
|
|
var recoverBody []ast.Stmt
|
|
if p.current.Kind == token.IDENT && p.currentUpper() == "CATCH" {
|
|
p.advance() // consume CATCH
|
|
if p.current.Kind == token.IDENT && p.current.Kind != token.NEWLINE {
|
|
recoverVar = p.advance().Literal
|
|
}
|
|
p.expectEndOfStmt()
|
|
recoverBody = p.parseStmtBlock(token.END)
|
|
}
|
|
|
|
endPos := p.current.Pos
|
|
p.match(token.END)
|
|
if p.current.Kind == token.IDENT && p.currentUpper() == "TRY" {
|
|
p.advance() // END TRY
|
|
}
|
|
p.expectEndOfStmt()
|
|
|
|
return &ast.SeqStmt{
|
|
BeginPos: beginPos, Body: body,
|
|
RecoverVar: recoverVar, RecoverBody: recoverBody,
|
|
EndPos: endPos,
|
|
}
|
|
}
|
|
|
|
func (p *Parser) parseReturn() *ast.ReturnStmt {
|
|
pos := p.expect(token.RETURN).Pos
|
|
var val ast.Expr
|
|
var vals []ast.Expr
|
|
if p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
|
|
val = p.parseExpr()
|
|
// Multi-return: RETURN a, b, c
|
|
if p.match(token.COMMA) {
|
|
vals = append(vals, val)
|
|
vals = append(vals, p.parseExpr())
|
|
for p.match(token.COMMA) {
|
|
vals = append(vals, p.parseExpr())
|
|
}
|
|
val = vals[0] // keep first for backward compat
|
|
}
|
|
}
|
|
p.expectEndOfStmt()
|
|
return &ast.ReturnStmt{ReturnPos: pos, Value: val, Values: vals}
|
|
}
|
|
|
|
func (p *Parser) parseQOut(isQQ bool) *ast.QOutStmt {
|
|
pos := p.advance().Pos // consume ? or ??
|
|
var exprs []ast.Expr
|
|
if p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
|
|
exprs = append(exprs, p.parseExpr())
|
|
for p.match(token.COMMA) {
|
|
exprs = append(exprs, p.parseExpr())
|
|
}
|
|
}
|
|
p.expectEndOfStmt()
|
|
return &ast.QOutStmt{QPos: pos, IsQQ: isQQ, Exprs: exprs}
|
|
}
|
|
|
|
func (p *Parser) parsePrivatePublic(scope ast.VarScope) ast.Stmt {
|
|
tok := p.advance()
|
|
var vars []*ast.VarInit
|
|
for {
|
|
// Handle ¯o in PRIVATE/PUBLIC list
|
|
if p.at(token.AMPERSAND) {
|
|
macroExpr := p.parseMacro()
|
|
var init ast.Expr
|
|
if p.match(token.ASSIGN) {
|
|
init = p.parseExpr()
|
|
}
|
|
name := "macro"
|
|
if me, ok := macroExpr.(*ast.MacroExpr); ok {
|
|
if id, ok2 := me.Expr.(*ast.IdentExpr); ok2 {
|
|
name = id.Name
|
|
}
|
|
}
|
|
vars = append(vars, &ast.VarInit{NamePos: macroExpr.Pos(), Name: name, Init: init})
|
|
if !p.match(token.COMMA) {
|
|
break
|
|
}
|
|
continue
|
|
}
|
|
name := p.expectMethodName() // allow keywords as var names (MEMVAR, etc.)
|
|
var init ast.Expr
|
|
// Skip AS type declaration
|
|
if p.match(token.AS) {
|
|
for p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF &&
|
|
p.current.Kind != token.ASSIGN && p.current.Kind != token.COMMA {
|
|
p.advance()
|
|
}
|
|
}
|
|
if p.match(token.ASSIGN) {
|
|
init = p.parseExpr()
|
|
}
|
|
vars = append(vars, &ast.VarInit{NamePos: name.Pos, Name: name.Literal, Init: init})
|
|
if !p.match(token.COMMA) {
|
|
break
|
|
}
|
|
}
|
|
p.expectEndOfStmt()
|
|
return &ast.VarDecl{DeclPos: tok.Pos, Scope: scope, Vars: vars}
|
|
}
|
|
|
|
// --- xBase commands ---
|
|
|
|
func (p *Parser) parseUse() *ast.UseCmd {
|
|
pos := p.expect(token.USE).Pos
|
|
var file ast.Expr
|
|
var via, alias string
|
|
var aliasExprNode ast.Expr
|
|
var shared, readOnly bool
|
|
|
|
// USE without args = close
|
|
if p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
|
|
// If file starts with macro &, skip entire USE to EOL (complex macro syntax)
|
|
if p.at(token.AMPERSAND) {
|
|
p.skipToEndOfLine()
|
|
p.expectEndOfStmt()
|
|
return &ast.UseCmd{UsePos: pos}
|
|
}
|
|
// Bare ident as filename: USE myfile / USE myfile.dbf / USE myfile NEW
|
|
// In Harbour, USE <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
|
|
}
|