- skipFilter: skip deleted records in GoTop/GoBottom/Skip when SET DELETED ON - hbrdd.IsSetDeleted callback: avoids circular import hbrdd→hbrtl - Parser: capture ON/OFF for boolean SET commands (DELETED, EXACT, SOFTSEEK, etc.) - Parser: capture TO expr for SET DATE/DECIMALS/EPOCH - Gengo: emit proper t.Do() calls for 11 SET toggles + 3 value SETs - stmtSet: was stub (skipToEOL), now calls parseSet() - RTL: register 11 SET toggle functions (SETDELETED, SETEXACT, etc.) - RTL: DBLOCATE/DBCONTINUE for sequential search - RTL: DBSETFILTER/DBCLEARFILTER/DBFILTER - PadL/PadR: support 3rd param fill character - Area interface: added SetFound, SetLocate, LocateBlock, filter methods - MemRDD: implements new Area interface methods - Comprehensive PRG test: test_search.prg (7 test suites all pass) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2186 lines
56 KiB
Go
2186 lines
56 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 == "CLASS" {
|
|
// CLASS VAR — class-level variable
|
|
p.advance() // skip CLASS
|
|
if p.current.Kind == token.IDENT && p.currentUpper() == "VAR" {
|
|
p.tokens[p.pos].Kind = token.DATA
|
|
p.current = p.tokens[p.pos]
|
|
members = append(members, p.parseDataDecl())
|
|
} else if p.current.Kind == token.DATA {
|
|
members = append(members, p.parseDataDecl())
|
|
} else if p.current.Kind == token.METHOD {
|
|
members = append(members, p.parseClassMethodDecl())
|
|
} else {
|
|
p.skipToEndOfLine()
|
|
}
|
|
} else if upper == "ON" || upper == "OPERATOR" || upper == "DESTRUCTOR" ||
|
|
upper == "DELEGATE" || upper == "ERROR" || upper == "MESSAGE" ||
|
|
upper == "VIRTUAL" || upper == "DEFERRED" {
|
|
// ON ERROR, OPERATOR "+" ARG, DESTRUCTOR, DELEGATE — skip to EOL
|
|
p.skipToEndOfLine()
|
|
p.skipNewlines()
|
|
continue
|
|
} else {
|
|
p.error(fmt.Sprintf("unexpected in CLASS body: %v %q", p.current.Kind, p.current.Literal))
|
|
p.advance()
|
|
}
|
|
default:
|
|
p.error(fmt.Sprintf("unexpected in CLASS body: %v", p.current.Kind))
|
|
p.advance()
|
|
}
|
|
p.skipNewlines()
|
|
}
|
|
|
|
endPos := p.current.Pos
|
|
if p.match(token.ENDCLASS) {
|
|
// ok
|
|
} else if p.match(token.END) {
|
|
// END CLASS — skip optional CLASS keyword
|
|
p.match(token.CLASS)
|
|
}
|
|
p.expectEndOfStmt()
|
|
|
|
return &ast.ClassDecl{
|
|
ClassPos: classPos,
|
|
Name: name,
|
|
ParentName: parent,
|
|
Members: members,
|
|
EndPos: endPos,
|
|
}
|
|
}
|
|
|
|
func (p *Parser) parseDataDecl() *ast.DataDecl {
|
|
dataPos := p.expect(token.DATA).Pos
|
|
name := p.expectMethodName().Literal // allow keywords as data names
|
|
|
|
var init ast.Expr
|
|
var asType string
|
|
|
|
// Parse AS, INIT, commas, and qualifiers in any order
|
|
for p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
|
|
if p.match(token.AS) {
|
|
if p.current.Kind == token.IDENT || p.current.Literal != "" {
|
|
p.advance() // type name
|
|
}
|
|
continue
|
|
}
|
|
if p.match(token.COMMA) {
|
|
// VAR One, Two, Three — skip additional names
|
|
if p.current.Kind == token.IDENT || p.current.Literal != "" {
|
|
p.advance() // skip additional name
|
|
}
|
|
continue
|
|
}
|
|
if p.current.Kind == token.IDENT {
|
|
upper := p.currentUpper()
|
|
if upper == "INIT" {
|
|
p.advance()
|
|
init = p.parseExpr()
|
|
continue
|
|
}
|
|
// Skip visibility/attribute qualifiers
|
|
if upper == "READONLY" || upper == "EXPORTED" || upper == "PROTECTED" ||
|
|
upper == "HIDDEN" || upper == "SYNC" || upper == "USUAL" ||
|
|
upper == "PROPERTY" || upper == "PERSISTENT" || upper == "SHARED" {
|
|
p.advance()
|
|
continue
|
|
}
|
|
}
|
|
if p.current.Kind == token.INLINE_KW {
|
|
p.skipToEndOfLine()
|
|
break
|
|
}
|
|
break
|
|
}
|
|
p.expectEndOfStmt()
|
|
|
|
return &ast.DataDecl{DataPos: dataPos, Name: name, Init: init, AsType: asType}
|
|
}
|
|
|
|
// expectMethodName: method names can be keywords (end, home, left, right, etc.)
|
|
func (p *Parser) expectMethodName() token.Token {
|
|
if p.current.Kind == token.IDENT {
|
|
return p.advance()
|
|
}
|
|
// Allow keywords as method names
|
|
if p.current.Literal != "" {
|
|
return p.advance()
|
|
}
|
|
return p.expect(token.IDENT)
|
|
}
|
|
|
|
func (p *Parser) parseClassMethodDecl() *ast.MethodDecl {
|
|
methodPos := p.expect(token.METHOD).Pos
|
|
|
|
// Skip optional FUNCTION/PROCEDURE qualifier
|
|
if p.current.Kind == token.FUNCTION_KW || p.current.Kind == token.PROCEDURE {
|
|
p.advance()
|
|
}
|
|
|
|
name := p.expectMethodName().Literal
|
|
|
|
var params []*ast.ParamDecl
|
|
if p.match(token.LPAREN) {
|
|
params = p.parseParamList()
|
|
p.expect(token.RPAREN)
|
|
}
|
|
|
|
// Check for SETGET keyword
|
|
isSetGet := false
|
|
if p.current.Kind == token.IDENT && p.currentUpper() == "SETGET" {
|
|
p.advance()
|
|
isSetGet = true
|
|
}
|
|
|
|
// Skip trailing qualifiers: OPERATOR, VIRTUAL, CONSTRUCTOR, etc.
|
|
if p.current.Kind == token.IDENT {
|
|
upper := p.currentUpper()
|
|
if upper == "OPERATOR" || upper == "VIRTUAL" || upper == "DEFERRED" {
|
|
p.skipToEndOfLine()
|
|
}
|
|
}
|
|
if p.current.Kind == token.OPERATOR_KW || p.current.Kind == token.CONSTRUCTOR {
|
|
p.skipToEndOfLine()
|
|
}
|
|
|
|
// Skip INLINE + rest of line (METHOD ... INLINE expr)
|
|
p.skipClassInline()
|
|
|
|
p.expectEndOfStmt()
|
|
|
|
return &ast.MethodDecl{
|
|
MethodPos: methodPos,
|
|
Name: name,
|
|
Params: params,
|
|
IsSetGet: isSetGet,
|
|
EndPos: methodPos,
|
|
}
|
|
}
|
|
|
|
// skipClassInline skips INLINE keyword and the rest of the line (used in CLASS body)
|
|
func (p *Parser) skipClassInline() {
|
|
if p.current.Kind == token.INLINE_KW ||
|
|
(p.current.Kind == token.IDENT && p.currentUpper() == "INLINE") {
|
|
p.skipToEndOfLine()
|
|
}
|
|
}
|
|
|
|
// ACCESS name METHOD getterName
|
|
func (p *Parser) parseAccessDecl() *ast.MethodDecl {
|
|
pos := p.expect(token.ACCESS).Pos
|
|
propName := p.expectMethodName().Literal
|
|
|
|
// Skip optional (params)
|
|
if p.match(token.LPAREN) {
|
|
for !p.atAny(token.RPAREN, token.EOF) {
|
|
p.advance()
|
|
}
|
|
p.match(token.RPAREN)
|
|
}
|
|
|
|
methodName := propName
|
|
if p.match(token.METHOD) {
|
|
methodName = p.expectMethodName().Literal
|
|
if p.match(token.LPAREN) {
|
|
for !p.atAny(token.RPAREN, token.EOF) {
|
|
p.advance()
|
|
}
|
|
p.match(token.RPAREN)
|
|
}
|
|
}
|
|
// Skip INLINE + rest of line
|
|
p.skipClassInline()
|
|
p.expectEndOfStmt()
|
|
|
|
return &ast.MethodDecl{
|
|
MethodPos: pos,
|
|
Name: methodName,
|
|
IsAccess: true,
|
|
AccessName: propName,
|
|
EndPos: pos,
|
|
}
|
|
}
|
|
|
|
// ASSIGN name METHOD setterName
|
|
func (p *Parser) parseAssignDecl() *ast.MethodDecl {
|
|
pos := p.expect(token.ASSIGN_KW).Pos
|
|
propName := p.expectMethodName().Literal
|
|
|
|
// Skip optional (params)
|
|
if p.match(token.LPAREN) {
|
|
for !p.atAny(token.RPAREN, token.EOF) {
|
|
p.advance()
|
|
}
|
|
p.match(token.RPAREN)
|
|
}
|
|
|
|
methodName := "_" + propName
|
|
if p.match(token.METHOD) {
|
|
methodName = p.expectMethodName().Literal
|
|
if p.match(token.LPAREN) {
|
|
for !p.atAny(token.RPAREN, token.EOF) {
|
|
p.advance()
|
|
}
|
|
p.match(token.RPAREN)
|
|
}
|
|
}
|
|
// Skip INLINE + rest of line
|
|
p.skipClassInline()
|
|
p.expectEndOfStmt()
|
|
|
|
return &ast.MethodDecl{
|
|
MethodPos: pos,
|
|
Name: methodName,
|
|
IsAssign: true,
|
|
AccessName: propName,
|
|
EndPos: pos,
|
|
}
|
|
}
|
|
|
|
// parseMethodDecl parses standalone: METHOD [FUNCTION|PROCEDURE] name(...) CLASS classname
|
|
func (p *Parser) parseMethodDecl() *ast.MethodDecl {
|
|
methodPos := p.expect(token.METHOD).Pos
|
|
|
|
// Skip optional FUNCTION/PROCEDURE qualifier
|
|
if p.current.Kind == token.FUNCTION_KW || p.current.Kind == token.PROCEDURE {
|
|
p.advance()
|
|
}
|
|
|
|
name := p.expectMethodName().Literal
|
|
|
|
var params []*ast.ParamDecl
|
|
if p.match(token.LPAREN) {
|
|
params = p.parseParamList()
|
|
p.expect(token.RPAREN)
|
|
}
|
|
|
|
var className string
|
|
if p.match(token.CLASS) {
|
|
className = p.expect(token.IDENT).Literal
|
|
}
|
|
p.expectEndOfStmt()
|
|
|
|
// Declarations
|
|
var decls []ast.Decl
|
|
p.skipNewlines()
|
|
for p.atAny(token.LOCAL, token.STATIC) {
|
|
decls = append(decls, p.parseVarDecl())
|
|
p.skipNewlines()
|
|
}
|
|
|
|
// Body
|
|
body := p.parseStmtBlock(token.RETURN, token.FUNCTION_KW, token.PROCEDURE, token.CLASS, token.METHOD, token.EOF)
|
|
|
|
var endPos token.Position
|
|
if p.current.Kind == token.RETURN {
|
|
retStmt := p.parseReturn()
|
|
body = append(body, retStmt)
|
|
endPos = retStmt.Pos()
|
|
} else {
|
|
endPos = p.current.Pos
|
|
}
|
|
|
|
return &ast.MethodDecl{
|
|
MethodPos: methodPos,
|
|
Name: name,
|
|
ClassName: className,
|
|
Params: params,
|
|
Decls: decls,
|
|
Body: body,
|
|
EndPos: endPos,
|
|
}
|
|
}
|
|
|
|
// --- Statement parsing ---
|
|
|
|
// parseStmtBlock parses statements until one of the stop tokens.
|
|
func (p *Parser) parseStmtBlock(stopTokens ...token.Kind) []ast.Stmt {
|
|
var stmts []ast.Stmt
|
|
for {
|
|
p.skipNewlines()
|
|
if p.current.Kind == token.EOF {
|
|
break
|
|
}
|
|
for _, stop := range stopTokens {
|
|
if p.current.Kind == stop {
|
|
// Don't stop at RETURN if it's used as variable: return := ...
|
|
if stop == token.RETURN &&
|
|
p.peekAt(1) == token.ASSIGN {
|
|
break // continue parsing as statement
|
|
}
|
|
return stmts
|
|
}
|
|
}
|
|
stmt := p.parseStmt()
|
|
if stmt != nil {
|
|
stmts = append(stmts, stmt)
|
|
}
|
|
}
|
|
return stmts
|
|
}
|
|
|
|
func (p *Parser) parseStmt() ast.Stmt {
|
|
// Registry lookup — O(1) dispatch
|
|
if fn := p.lookupStmtParser(); fn != nil {
|
|
return fn(p)
|
|
}
|
|
|
|
// IDENT-based commands (xBase multi-word: COPY, SORT, etc.)
|
|
if p.current.Kind == token.IDENT {
|
|
return p.parseIdentStmt()
|
|
}
|
|
|
|
// Multi-assign: a, b := expr
|
|
if p.looksLikeMultiAssign() {
|
|
return p.parseMultiAssign()
|
|
}
|
|
|
|
// Default: expression statement
|
|
return p.parseExprStmt()
|
|
}
|
|
|
|
|
|
// parseIdentStmt handles IDENT-based commands (xBase multi-word: COPY, SORT, etc.)
|
|
func (p *Parser) parseIdentStmt() ast.Stmt {
|
|
upper := p.currentUpper()
|
|
|
|
// WITH TIMEOUT → timeout context
|
|
if upper == "WITH" && p.peekAt(1) == token.TIMEOUT_KW {
|
|
return p.parseWithTimeout()
|
|
}
|
|
|
|
// xBase commands that consume entire line
|
|
switch upper {
|
|
case "COPY", "SORT", "COUNT", "SUM", "AVERAGE", "TOTAL", "UPDATE",
|
|
"LABEL", "REPORT", "ACCEPT", "INPUT", "LOCATE", "CONTINUE",
|
|
"JOIN", "RELEASE", "SAVE", "RESTORE", "ERASE", "RENAME",
|
|
"RUN", "DIR", "STORE", "NOTE", "TEXT", "ENDTEXT",
|
|
"WITH", "KEYBOARD", "CLEAR", "DISPLAY", "LIST", "REINDEX":
|
|
p.advance()
|
|
for p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
|
|
p.advance()
|
|
}
|
|
p.expectEndOfStmt()
|
|
return &ast.ExprStmt{X: &ast.LiteralExpr{Kind: token.NIL_LIT, Value: "NIL"}}
|
|
|
|
case "COMMIT":
|
|
p.advance()
|
|
p.expectEndOfStmt()
|
|
return &ast.ExprStmt{X: &ast.CallExpr{
|
|
Func: &ast.IdentExpr{Name: "DbCommit"},
|
|
}}
|
|
|
|
case "FIVE_GODUMP__":
|
|
// GoDump is a Decl, wrap as ExprStmt for statement context
|
|
p.advance() // consume FIVE_GODUMP__
|
|
idx := 0
|
|
if p.current.Kind == token.INT {
|
|
fmt.Sscanf(p.current.Literal, "%d", &idx)
|
|
p.advance()
|
|
}
|
|
p.expectEndOfStmt()
|
|
// Store as nil statement — gengo handles GoDumpDecl at file level
|
|
return &ast.ExprStmt{X: &ast.LiteralExpr{Kind: token.NIL_LIT, Value: "NIL"}}
|
|
}
|
|
|
|
// Multi-assign check: a, b := expr
|
|
if p.looksLikeMultiAssign() {
|
|
return p.parseMultiAssign()
|
|
}
|
|
|
|
// Default: expression statement (function call, assignment, etc.)
|
|
return p.parseExprStmt()
|
|
}
|
|
|
|
func (p *Parser) parseExprStmt() ast.Stmt {
|
|
// READ [SAVE] [MSG AT ...] [MSG COLOR ...] — special case
|
|
if p.current.Kind == token.IDENT && p.currentUpper() == "READ" {
|
|
pos := p.advance().Pos
|
|
save := false
|
|
if p.current.Kind == token.IDENT && p.currentUpper() == "SAVE" {
|
|
save = true
|
|
p.advance()
|
|
}
|
|
// Skip optional clauses: MSG AT row,col,col2 / MSG COLOR "..." etc.
|
|
if p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
|
|
p.skipToEndOfLine()
|
|
}
|
|
p.expectEndOfStmt()
|
|
return &ast.ReadCmd{ReadPos: pos, Save: save}
|
|
}
|
|
// TRY / CATCH [oErr] / END — Harbour extension, maps to BEGIN SEQUENCE / RECOVER
|
|
if p.current.Kind == token.IDENT && p.currentUpper() == "TRY" {
|
|
return p.parseTryCatch()
|
|
}
|
|
// CLOSE [DATABASES|ALL] — close work areas
|
|
if p.current.Kind == token.IDENT && p.currentUpper() == "CLOSE" {
|
|
p.advance()
|
|
// Skip optional DATABASES/ALL keyword
|
|
if p.current.Kind == token.IDENT {
|
|
p.advance()
|
|
}
|
|
p.expectEndOfStmt()
|
|
return &ast.ExprStmt{X: &ast.CallExpr{
|
|
Func: &ast.IdentExpr{Name: "DbCloseArea"},
|
|
}}
|
|
}
|
|
// xBase commands that consume entire line (COPY, SORT, COUNT, SUM, etc.)
|
|
if p.current.Kind == token.IDENT {
|
|
// WITH TIMEOUT n / body / ENDWITH
|
|
if p.currentUpper() == "WITH" &&
|
|
p.peekAt(1) == token.TIMEOUT_KW {
|
|
return p.parseWithTimeout()
|
|
}
|
|
switch p.currentUpper() {
|
|
case "COPY", "SORT", "COUNT", "SUM", "AVERAGE", "TOTAL", "UPDATE",
|
|
"LABEL", "REPORT", "ACCEPT", "INPUT", "LOCATE", "CONTINUE",
|
|
"JOIN", "RELEASE", "SAVE", "RESTORE", "ERASE", "RENAME",
|
|
"RUN", "DIR", "STORE", "NOTE", "TEXT", "ENDTEXT",
|
|
"WITH", "KEYBOARD", "CLEAR", "DISPLAY", "LIST", "REINDEX":
|
|
// Consume entire line — these are complex multi-word commands
|
|
p.advance()
|
|
for p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
|
|
p.advance()
|
|
}
|
|
p.expectEndOfStmt()
|
|
return &ast.ExprStmt{X: &ast.LiteralExpr{Kind: token.NIL_LIT, Value: "NIL"}}
|
|
}
|
|
}
|
|
|
|
// COMMIT — flush work area
|
|
if p.current.Kind == token.IDENT && p.currentUpper() == "COMMIT" {
|
|
p.advance()
|
|
p.expectEndOfStmt()
|
|
return &ast.ExprStmt{X: &ast.CallExpr{
|
|
Func: &ast.IdentExpr{Name: "DbCommit"},
|
|
}}
|
|
}
|
|
expr := p.parseExpr()
|
|
|
|
// ch <- value (channel send)
|
|
if p.at(token.ARROW_LEFT) {
|
|
pos := p.advance().Pos
|
|
val := p.parseExpr()
|
|
p.expectEndOfStmt()
|
|
return &ast.ChanSendStmt{ChanPos: pos, Chan: expr, Value: val}
|
|
}
|
|
|
|
p.expectEndOfStmt()
|
|
return &ast.ExprStmt{X: expr}
|
|
}
|
|
|
|
// --- Control flow ---
|
|
|
|
func (p *Parser) parseIf() *ast.IfStmt {
|
|
ifPos := p.expect(token.IF).Pos
|
|
cond := p.parseExpr()
|
|
p.expectEndOfStmt()
|
|
|
|
body := p.parseStmtBlock(token.ELSEIF, token.ELSE, token.ENDIF, token.END)
|
|
|
|
var elseIfs []*ast.ElseIfClause
|
|
for p.current.Kind == token.ELSEIF {
|
|
eiPos := p.advance().Pos
|
|
eiCond := p.parseExpr()
|
|
p.expectEndOfStmt()
|
|
eiBody := p.parseStmtBlock(token.ELSEIF, token.ELSE, token.ENDIF, token.END)
|
|
elseIfs = append(elseIfs, &ast.ElseIfClause{
|
|
ElseIfPos: eiPos,
|
|
Cond: eiCond,
|
|
Body: eiBody,
|
|
})
|
|
}
|
|
|
|
var elseBody []ast.Stmt
|
|
if p.match(token.ELSE) {
|
|
p.expectEndOfStmt()
|
|
elseBody = p.parseStmtBlock(token.ENDIF, token.END)
|
|
}
|
|
|
|
endPos := p.current.Pos
|
|
if !p.match(token.ENDIF) {
|
|
p.match(token.END) // alternative
|
|
}
|
|
p.expectEndOfStmt()
|
|
|
|
return &ast.IfStmt{
|
|
IfPos: ifPos,
|
|
Cond: cond,
|
|
Body: body,
|
|
ElseIfs: elseIfs,
|
|
ElseBody: elseBody,
|
|
EndPos: endPos,
|
|
}
|
|
}
|
|
|
|
// looksLikeIIF checks if IF( starts an IIF-style call: IF(cond, true, false)
|
|
// by scanning for commas inside the parenthesized expression.
|
|
func (p *Parser) looksLikeIIF() bool {
|
|
// Start after IF token; expect ( at p.pos+1
|
|
if p.pos+1 >= len(p.tokens) || p.peekAt(1) != token.LPAREN {
|
|
return false
|
|
}
|
|
depth := 0
|
|
commas := 0
|
|
for i := p.pos + 2; i < len(p.tokens); i++ { // start INSIDE the parens
|
|
switch p.tokens[i].Kind {
|
|
case token.LPAREN, token.LBRACE, token.LBRACKET:
|
|
depth++
|
|
case token.RPAREN:
|
|
if depth == 0 {
|
|
return commas >= 2
|
|
}
|
|
depth--
|
|
case token.RBRACE, token.RBRACKET:
|
|
if depth > 0 {
|
|
depth--
|
|
}
|
|
case token.COMMA:
|
|
if depth == 0 {
|
|
commas++
|
|
}
|
|
case token.NEWLINE, token.EOF:
|
|
return false
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// parseDoProc: DO funcname [WITH arg1, arg2, ...]
|
|
func (p *Parser) parseDoProc() ast.Stmt {
|
|
p.advance() // skip DO
|
|
funcName := p.expectMethodName().Literal
|
|
|
|
var args []ast.Expr
|
|
if p.current.Kind == token.WITH {
|
|
p.advance() // skip WITH
|
|
for {
|
|
args = append(args, p.parseExpr())
|
|
if !p.match(token.COMMA) {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
p.expectEndOfStmt()
|
|
|
|
return &ast.ExprStmt{X: &ast.CallExpr{
|
|
Func: &ast.IdentExpr{Name: funcName},
|
|
Args: args,
|
|
}}
|
|
}
|
|
|
|
func (p *Parser) parseDoWhile() *ast.DoWhileStmt {
|
|
var doPos token.Position
|
|
if p.current.Kind == token.DO {
|
|
doPos = p.advance().Pos
|
|
p.expect(token.WHILE)
|
|
} else {
|
|
// Bare WHILE (Clipper compatibility)
|
|
doPos = p.expect(token.WHILE).Pos
|
|
}
|
|
cond := p.parseExpr()
|
|
p.expectEndOfStmt()
|
|
|
|
body := p.parseStmtBlock(token.ENDDO, token.END)
|
|
|
|
endPos := p.current.Pos
|
|
if !p.match(token.ENDDO) {
|
|
p.match(token.END)
|
|
}
|
|
p.expectEndOfStmt()
|
|
|
|
return &ast.DoWhileStmt{DoPos: doPos, Cond: cond, Body: body, EndPos: endPos}
|
|
}
|
|
|
|
func (p *Parser) parseFor() ast.Stmt {
|
|
forPos := p.expect(token.FOR).Pos
|
|
|
|
// FOR EACH var IN collection
|
|
if p.match(token.EACH) {
|
|
return p.parseForEach(forPos)
|
|
}
|
|
|
|
// FOR var := start TO end [STEP step]
|
|
// Variable can be aliased: M->TEST or simple: i
|
|
varTok := p.expectMethodName()
|
|
varName := varTok.Literal
|
|
// Handle M->varname
|
|
if p.at(token.ARROW) {
|
|
p.advance() // skip ->
|
|
fieldTok := p.expectMethodName()
|
|
varName = fieldTok.Literal
|
|
}
|
|
p.expect(token.ASSIGN) // :=
|
|
start := p.parseExpr()
|
|
p.expect(token.TO)
|
|
toExpr := p.parseExpr()
|
|
|
|
var step ast.Expr
|
|
if p.match(token.STEP) {
|
|
step = p.parseExpr()
|
|
}
|
|
p.expectEndOfStmt()
|
|
|
|
body := p.parseStmtBlock(token.NEXT, token.END)
|
|
nextPos := p.current.Pos
|
|
if !p.match(token.NEXT) {
|
|
p.match(token.END)
|
|
}
|
|
// Skip optional counter variable after NEXT (e.g. NEXT nVar)
|
|
if p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
|
|
p.skipToEndOfLine()
|
|
}
|
|
p.expectEndOfStmt()
|
|
|
|
return &ast.ForStmt{
|
|
ForPos: forPos, Var: varName, Start: start, To: toExpr,
|
|
Step: step, Body: body, NextPos: nextPos,
|
|
}
|
|
}
|
|
|
|
func (p *Parser) parseForEach(forPos token.Position) *ast.ForEachStmt {
|
|
varName := p.expect(token.IDENT).Literal
|
|
// Multi-variable FOR EACH: FOR EACH a, b, c IN x, y, z
|
|
// Skip extra variables — use only first var and first collection
|
|
var extraVars []string
|
|
for p.match(token.COMMA) {
|
|
extraVars = append(extraVars, p.expect(token.IDENT).Literal)
|
|
}
|
|
p.expect(token.IN)
|
|
collection := p.parseExpr()
|
|
// Skip extra collections
|
|
for p.match(token.COMMA) {
|
|
p.parseExpr() // consume and discard
|
|
}
|
|
descend := false
|
|
if p.current.Kind == token.DESCENDING {
|
|
p.advance()
|
|
descend = true
|
|
}
|
|
p.expectEndOfStmt()
|
|
|
|
body := p.parseStmtBlock(token.NEXT, token.END)
|
|
nextPos := p.current.Pos
|
|
if !p.match(token.NEXT) {
|
|
p.match(token.END)
|
|
}
|
|
// Skip optional counter variable after NEXT
|
|
if p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
|
|
p.skipToEndOfLine()
|
|
}
|
|
p.expectEndOfStmt()
|
|
|
|
return &ast.ForEachStmt{
|
|
ForPos: forPos, Var: varName, Collection: collection,
|
|
Descend: descend, Body: body, NextPos: nextPos,
|
|
}
|
|
}
|
|
|
|
// parseDoCase: DO CASE / CASE cond / OTHERWISE / ENDCASE
|
|
// Harbour: equivalent to IF/ELSEIF/ELSE chain
|
|
func (p *Parser) parseDoCase() *ast.IfStmt {
|
|
doPos := p.expect(token.DO).Pos
|
|
p.expect(token.CASE) // consume CASE after DO
|
|
p.expectEndOfStmt()
|
|
p.skipNewlines()
|
|
|
|
// First CASE
|
|
if p.current.Kind != token.CASE {
|
|
p.error("expected CASE after DO CASE")
|
|
return &ast.IfStmt{IfPos: doPos, EndPos: doPos}
|
|
}
|
|
|
|
p.advance() // consume CASE
|
|
cond := p.parseExpr()
|
|
p.expectEndOfStmt()
|
|
body := p.parseStmtBlock(token.CASE, token.OTHERWISE, token.ENDCASE, token.END)
|
|
|
|
// Build as IfStmt with ElseIfs
|
|
var elseIfs []*ast.ElseIfClause
|
|
for p.current.Kind == token.CASE {
|
|
eiPos := p.advance().Pos
|
|
eiCond := p.parseExpr()
|
|
p.expectEndOfStmt()
|
|
eiBody := p.parseStmtBlock(token.CASE, token.OTHERWISE, token.ENDCASE, token.END)
|
|
elseIfs = append(elseIfs, &ast.ElseIfClause{
|
|
ElseIfPos: eiPos,
|
|
Cond: eiCond,
|
|
Body: eiBody,
|
|
})
|
|
}
|
|
|
|
var elseBody []ast.Stmt
|
|
if p.match(token.OTHERWISE) {
|
|
p.expectEndOfStmt()
|
|
elseBody = p.parseStmtBlock(token.ENDCASE, token.END)
|
|
}
|
|
|
|
endPos := p.current.Pos
|
|
if !p.match(token.ENDCASE) {
|
|
p.match(token.END)
|
|
}
|
|
p.expectEndOfStmt()
|
|
|
|
return &ast.IfStmt{
|
|
IfPos: doPos,
|
|
Cond: cond,
|
|
Body: body,
|
|
ElseIfs: elseIfs,
|
|
ElseBody: elseBody,
|
|
EndPos: endPos,
|
|
}
|
|
}
|
|
|
|
func (p *Parser) parseSwitch() *ast.SwitchStmt {
|
|
switchPos := p.expect(token.SWITCH).Pos
|
|
expr := p.parseExpr()
|
|
p.expectEndOfStmt()
|
|
|
|
var cases []*ast.CaseClause
|
|
var otherwise []ast.Stmt
|
|
p.skipNewlines()
|
|
|
|
for p.current.Kind == token.CASE {
|
|
casePos := p.advance().Pos
|
|
val := p.parseExpr()
|
|
p.expectEndOfStmt()
|
|
caseBody := p.parseStmtBlock(token.CASE, token.OTHERWISE, token.ENDSWITCH, token.ENDCASE, token.END)
|
|
cases = append(cases, &ast.CaseClause{CasePos: casePos, Value: val, Body: caseBody})
|
|
}
|
|
|
|
if p.match(token.OTHERWISE) {
|
|
p.expectEndOfStmt()
|
|
otherwise = p.parseStmtBlock(token.ENDSWITCH, token.ENDCASE, token.END)
|
|
}
|
|
|
|
endPos := p.current.Pos
|
|
if !p.match(token.ENDSWITCH) {
|
|
if !p.match(token.ENDCASE) {
|
|
p.match(token.END)
|
|
}
|
|
}
|
|
p.expectEndOfStmt()
|
|
|
|
return &ast.SwitchStmt{SwitchPos: switchPos, Expr: expr, Cases: cases, Otherwise: otherwise, EndPos: endPos}
|
|
}
|
|
|
|
func (p *Parser) parseBeginSequence() *ast.SeqStmt {
|
|
beginPos := p.expect(token.BEGIN).Pos
|
|
p.expect(token.SEQUENCE)
|
|
p.expectEndOfStmt()
|
|
|
|
body := p.parseStmtBlock(token.RECOVER, token.END)
|
|
|
|
var recoverVar string
|
|
var recoverBody []ast.Stmt
|
|
if p.match(token.RECOVER) {
|
|
if p.match(token.USING) {
|
|
recoverVar = p.expect(token.IDENT).Literal
|
|
}
|
|
p.expectEndOfStmt()
|
|
recoverBody = p.parseStmtBlock(token.END)
|
|
}
|
|
|
|
endPos := p.current.Pos
|
|
p.match(token.END)
|
|
p.match(token.SEQUENCE) // optional: END SEQUENCE
|
|
p.expectEndOfStmt()
|
|
|
|
return &ast.SeqStmt{
|
|
BeginPos: beginPos, Body: body,
|
|
RecoverVar: recoverVar, RecoverBody: recoverBody,
|
|
EndPos: endPos,
|
|
}
|
|
}
|
|
|
|
// parseTryCatch: TRY ... CATCH [oErr] ... END — maps to SeqStmt
|
|
func (p *Parser) parseTryCatch() *ast.SeqStmt {
|
|
beginPos := p.advance().Pos // consume TRY
|
|
p.expectEndOfStmt()
|
|
|
|
// Parse body until CATCH or END
|
|
var body []ast.Stmt
|
|
for !p.atAny(token.EOF) {
|
|
if p.current.Kind == token.IDENT && p.currentUpper() == "CATCH" {
|
|
break
|
|
}
|
|
if p.current.Kind == token.END {
|
|
break
|
|
}
|
|
body = append(body, p.parseStmt())
|
|
p.skipNewlines()
|
|
}
|
|
|
|
var recoverVar string
|
|
var recoverBody []ast.Stmt
|
|
if p.current.Kind == token.IDENT && p.currentUpper() == "CATCH" {
|
|
p.advance() // consume CATCH
|
|
if p.current.Kind == token.IDENT && p.current.Kind != token.NEWLINE {
|
|
recoverVar = p.advance().Literal
|
|
}
|
|
p.expectEndOfStmt()
|
|
recoverBody = p.parseStmtBlock(token.END)
|
|
}
|
|
|
|
endPos := p.current.Pos
|
|
p.match(token.END)
|
|
if p.current.Kind == token.IDENT && p.currentUpper() == "TRY" {
|
|
p.advance() // END TRY
|
|
}
|
|
p.expectEndOfStmt()
|
|
|
|
return &ast.SeqStmt{
|
|
BeginPos: beginPos, Body: body,
|
|
RecoverVar: recoverVar, RecoverBody: recoverBody,
|
|
EndPos: endPos,
|
|
}
|
|
}
|
|
|
|
func (p *Parser) parseReturn() *ast.ReturnStmt {
|
|
pos := p.expect(token.RETURN).Pos
|
|
var val ast.Expr
|
|
var vals []ast.Expr
|
|
if p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
|
|
val = p.parseExpr()
|
|
// Multi-return: RETURN a, b, c
|
|
if p.match(token.COMMA) {
|
|
vals = append(vals, val)
|
|
vals = append(vals, p.parseExpr())
|
|
for p.match(token.COMMA) {
|
|
vals = append(vals, p.parseExpr())
|
|
}
|
|
val = vals[0] // keep first for backward compat
|
|
}
|
|
}
|
|
p.expectEndOfStmt()
|
|
return &ast.ReturnStmt{ReturnPos: pos, Value: val, Values: vals}
|
|
}
|
|
|
|
func (p *Parser) parseQOut(isQQ bool) *ast.QOutStmt {
|
|
pos := p.advance().Pos // consume ? or ??
|
|
var exprs []ast.Expr
|
|
if p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
|
|
exprs = append(exprs, p.parseExpr())
|
|
for p.match(token.COMMA) {
|
|
exprs = append(exprs, p.parseExpr())
|
|
}
|
|
}
|
|
p.expectEndOfStmt()
|
|
return &ast.QOutStmt{QPos: pos, IsQQ: isQQ, Exprs: exprs}
|
|
}
|
|
|
|
func (p *Parser) parsePrivatePublic(scope ast.VarScope) ast.Stmt {
|
|
tok := p.advance()
|
|
var vars []*ast.VarInit
|
|
for {
|
|
// Handle ¯o in PRIVATE/PUBLIC list
|
|
if p.at(token.AMPERSAND) {
|
|
macroExpr := p.parseMacro()
|
|
var init ast.Expr
|
|
if p.match(token.ASSIGN) {
|
|
init = p.parseExpr()
|
|
}
|
|
name := "macro"
|
|
if me, ok := macroExpr.(*ast.MacroExpr); ok {
|
|
if id, ok2 := me.Expr.(*ast.IdentExpr); ok2 {
|
|
name = id.Name
|
|
}
|
|
}
|
|
vars = append(vars, &ast.VarInit{NamePos: macroExpr.Pos(), Name: name, Init: init})
|
|
if !p.match(token.COMMA) {
|
|
break
|
|
}
|
|
continue
|
|
}
|
|
name := p.expectMethodName() // allow keywords as var names (MEMVAR, etc.)
|
|
var init ast.Expr
|
|
// Skip AS type declaration
|
|
if p.match(token.AS) {
|
|
for p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF &&
|
|
p.current.Kind != token.ASSIGN && p.current.Kind != token.COMMA {
|
|
p.advance()
|
|
}
|
|
}
|
|
if p.match(token.ASSIGN) {
|
|
init = p.parseExpr()
|
|
}
|
|
vars = append(vars, &ast.VarInit{NamePos: name.Pos, Name: name.Literal, Init: init})
|
|
if !p.match(token.COMMA) {
|
|
break
|
|
}
|
|
}
|
|
p.expectEndOfStmt()
|
|
return &ast.VarDecl{DeclPos: tok.Pos, Scope: scope, Vars: vars}
|
|
}
|
|
|
|
// --- xBase commands ---
|
|
|
|
func (p *Parser) parseUse() *ast.UseCmd {
|
|
pos := p.expect(token.USE).Pos
|
|
var file ast.Expr
|
|
var via, alias string
|
|
|
|
// USE without args = close
|
|
if p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
|
|
// If file starts with macro &, skip entire USE to EOL (complex macro syntax)
|
|
if p.at(token.AMPERSAND) {
|
|
p.skipToEndOfLine()
|
|
p.expectEndOfStmt()
|
|
return &ast.UseCmd{UsePos: pos}
|
|
}
|
|
file = p.parseExpr()
|
|
p.consumeFileExtension(file)
|
|
}
|
|
|
|
// Parse optional clauses: VIA, ALIAS, EXCLUSIVE, SHARED, NEW, READONLY
|
|
for p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
|
|
if p.current.Kind == token.IDENT {
|
|
upper := p.currentUpper()
|
|
if upper == "VIA" {
|
|
p.advance()
|
|
via = p.expectMethodName().Literal
|
|
continue
|
|
}
|
|
if upper == "ALIAS" {
|
|
p.advance()
|
|
if p.at(token.AMPERSAND) {
|
|
p.parseMacro() // macro alias — skip
|
|
} else {
|
|
alias = p.expectMethodName().Literal
|
|
}
|
|
continue
|
|
}
|
|
if upper == "EXCLUSIVE" || upper == "SHARED" || upper == "NEW" || upper == "READONLY" ||
|
|
upper == "ADDITIVE" {
|
|
p.advance()
|
|
continue
|
|
}
|
|
if upper == "INDEX" {
|
|
// INDEX file1[, file2, ...] — skip to EOL
|
|
p.skipToEndOfLine()
|
|
break
|
|
}
|
|
}
|
|
if p.current.Kind == token.ALIAS {
|
|
p.advance()
|
|
if p.at(token.AMPERSAND) {
|
|
p.parseMacro()
|
|
} else {
|
|
alias = p.expectMethodName().Literal
|
|
}
|
|
continue
|
|
}
|
|
if p.current.Kind == token.INDEX {
|
|
// INDEX file1, file2, ... — skip to EOL
|
|
p.skipToEndOfLine()
|
|
break
|
|
}
|
|
break
|
|
}
|
|
|
|
p.expectEndOfStmt()
|
|
return &ast.UseCmd{UsePos: pos, File: file, Via: via, Alias: alias}
|
|
}
|
|
|
|
func (p *Parser) parseSelect() *ast.SelectCmd {
|
|
pos := p.expect(token.SELECT).Pos
|
|
area := p.parseExpr()
|
|
p.expectEndOfStmt()
|
|
return &ast.SelectCmd{SelectPos: pos, Area: area}
|
|
}
|
|
|
|
func (p *Parser) parseGo() *ast.GoCmd {
|
|
pos := p.advance().Pos // GO or GOTO
|
|
var dir string
|
|
var recNo ast.Expr
|
|
|
|
switch p.current.Kind {
|
|
case token.TOP:
|
|
dir = "TOP"
|
|
p.advance()
|
|
case token.BOTTOM:
|
|
dir = "BOTTOM"
|
|
p.advance()
|
|
default:
|
|
recNo = p.parseExpr()
|
|
}
|
|
p.expectEndOfStmt()
|
|
return &ast.GoCmd{GoPos: pos, Direction: dir, RecNo: recNo}
|
|
}
|
|
|
|
func (p *Parser) parseSkip() *ast.SkipCmd {
|
|
pos := p.expect(token.SKIP_KW).Pos
|
|
var count ast.Expr
|
|
if p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
|
|
count = p.parseExpr()
|
|
}
|
|
p.expectEndOfStmt()
|
|
return &ast.SkipCmd{SkipPos: pos, Count: count}
|
|
}
|
|
|
|
func (p *Parser) parseSeek() *ast.SeekCmd {
|
|
pos := p.expect(token.SEEK).Pos
|
|
key := p.parseExpr()
|
|
softSeek := false
|
|
if p.current.Kind == token.SOFTSEEK {
|
|
p.advance()
|
|
softSeek = true
|
|
}
|
|
p.expectEndOfStmt()
|
|
return &ast.SeekCmd{SeekPos: pos, Key: key, SoftSeek: softSeek}
|
|
}
|
|
|
|
func (p *Parser) parseReplace() *ast.ReplaceCmd {
|
|
pos := p.expect(token.REPLACE).Pos
|
|
var fields []ast.ReplaceField
|
|
for {
|
|
field := p.parseExpr()
|
|
p.expect(token.WITH)
|
|
value := p.parseExpr()
|
|
fields = append(fields, ast.ReplaceField{Field: field, Value: value})
|
|
if !p.match(token.COMMA) {
|
|
break
|
|
}
|
|
}
|
|
p.expectEndOfStmt()
|
|
return &ast.ReplaceCmd{ReplacePos: pos, Fields: fields}
|
|
}
|
|
|
|
func (p *Parser) parseAppend() *ast.AppendCmd {
|
|
pos := p.expect(token.APPEND).Pos
|
|
if p.match(token.FROM) {
|
|
// APPEND FROM filename [DELIMITED|SDF|VIA ...] — skip to EOL
|
|
p.skipToEndOfLine()
|
|
p.expectEndOfStmt()
|
|
return &ast.AppendCmd{AppendPos: pos}
|
|
}
|
|
p.expect(token.BLANK)
|
|
p.expectEndOfStmt()
|
|
return &ast.AppendCmd{AppendPos: pos}
|
|
}
|
|
|
|
func (p *Parser) parseIndex() *ast.IndexCmd {
|
|
pos := p.expect(token.INDEX).Pos
|
|
p.expect(token.ON)
|
|
keyExpr := p.parseExpr()
|
|
|
|
// INDEX ON expr TO file OR INDEX ON expr TAG tagname [TO file]
|
|
var fileExpr ast.Expr
|
|
if p.match(token.TO) {
|
|
fileExpr = p.parseExpr()
|
|
p.consumeFileExtension(fileExpr)
|
|
} else if p.current.Kind == token.IDENT && p.currentUpper() == "TAG" {
|
|
p.advance() // skip TAG
|
|
tagExpr := p.parseExpr() // tag name
|
|
if p.match(token.TO) {
|
|
fileExpr = p.parseExpr()
|
|
} else {
|
|
fileExpr = tagExpr // use tag name as file
|
|
}
|
|
} else {
|
|
fileExpr = p.parseExpr() // fallback
|
|
}
|
|
|
|
var forCond ast.Expr
|
|
unique := false
|
|
descending := false
|
|
|
|
for {
|
|
if p.match(token.FOR) {
|
|
forCond = p.parseExpr()
|
|
} else if p.match(token.UNIQUE) {
|
|
unique = true
|
|
} else if p.match(token.DESCENDING) {
|
|
descending = true
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
p.expectEndOfStmt()
|
|
|
|
return &ast.IndexCmd{
|
|
IndexPos: pos, KeyExpr: keyExpr, File: fileExpr,
|
|
ForCond: forCond, Unique: unique, Descending: descending,
|
|
}
|
|
}
|
|
|
|
func (p *Parser) parseSet() *ast.SetCmd {
|
|
pos := p.expect(token.SET).Pos
|
|
|
|
// Accept any token as SET keyword (COLOR, KEY, ORDER, FILTER, etc. may be keyword tokens)
|
|
setting := p.expectMethodName().Literal
|
|
|
|
var expr ast.Expr
|
|
var extra string
|
|
|
|
// SET commands: consume everything until end of line.
|
|
// Boolean toggles: SET DELETED ON/OFF, SET EXACT ON/OFF, etc.
|
|
// Value settings: SET FILTER TO expr, SET ORDER TO n, SET DATE TO fmt
|
|
upperSetting := strings.ToUpper(setting)
|
|
|
|
// Check for ON/OFF boolean toggle
|
|
booleanSets := map[string]bool{
|
|
"DELETED": true, "EXACT": true, "SOFTSEEK": true, "EXCLUSIVE": true,
|
|
"FIXED": true, "CANCEL": true, "BELL": true, "CONFIRM": true,
|
|
"INSERT": true, "ESCAPE": true, "WRAP": true, "INTENSITY": true,
|
|
"SCOREBOARD": true, "CONSOLE": true, "ALTERNATE": true, "PRINTER": true,
|
|
}
|
|
|
|
if booleanSets[upperSetting] {
|
|
if p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
|
|
extra = strings.ToUpper(p.expectMethodName().Literal)
|
|
}
|
|
} else if p.match(token.TO) {
|
|
if upperSetting == "FILTER" || upperSetting == "RELATION" || upperSetting == "ORDER" || upperSetting == "INDEX" {
|
|
if p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
|
|
expr = p.parseExpr()
|
|
}
|
|
if p.current.Kind == token.INTO {
|
|
p.advance()
|
|
extra = p.expectMethodName().Literal
|
|
}
|
|
} else if upperSetting == "DATE" || upperSetting == "DECIMALS" || upperSetting == "EPOCH" {
|
|
if p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
|
|
expr = p.parseExpr()
|
|
}
|
|
}
|
|
}
|
|
|
|
// Consume remaining tokens (for COLOR TO, KEY TO, etc.)
|
|
for p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
|
|
p.advance()
|
|
}
|
|
p.expectEndOfStmt()
|
|
return &ast.SetCmd{SetPos: pos, Setting: setting, Expr: expr, Extra: extra}
|
|
}
|
|
|
|
// parseAtCmd parses @ row, col SAY/GET/PROMPT commands.
|
|
func (p *Parser) parseAtCmd() ast.Stmt {
|
|
pos := p.advance().Pos // consume @
|
|
row := p.parseExpr()
|
|
p.expect(token.COMMA)
|
|
col := p.parseExpr()
|
|
|
|
// Determine sub-command: SAY, GET, PROMPT
|
|
if p.current.Kind == token.IDENT {
|
|
switch p.currentUpper() {
|
|
case "SAY":
|
|
return p.parseAtSay(pos, row, col)
|
|
case "GET":
|
|
return p.parseAtGet(pos, row, col)
|
|
case "PROMPT":
|
|
return p.parseAtPrompt(pos, row, col)
|
|
case "CLEAR":
|
|
// @ row, col CLEAR [TO row2, col2] — clear region
|
|
p.advance() // skip CLEAR
|
|
if p.match(token.TO) {
|
|
p.parseExpr() // row2
|
|
p.expect(token.COMMA)
|
|
p.parseExpr() // col2
|
|
}
|
|
p.expectEndOfStmt()
|
|
return &ast.ExprStmt{X: &ast.CallExpr{
|
|
Func: &ast.IdentExpr{Name: "SetPos"},
|
|
Args: []ast.Expr{row, col},
|
|
}}
|
|
}
|
|
}
|
|
|
|
// @ row, col TO row2, col2 [DOUBLE] — box drawing
|
|
if p.match(token.TO) {
|
|
row2 := p.parseExpr()
|
|
p.expect(token.COMMA)
|
|
col2 := p.parseExpr()
|
|
// Skip optional DOUBLE keyword or other modifiers
|
|
for p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
|
|
p.advance()
|
|
}
|
|
p.expectEndOfStmt()
|
|
return &ast.ExprStmt{X: &ast.CallExpr{
|
|
Func: &ast.IdentExpr{Name: "DispBox"},
|
|
Args: []ast.Expr{row, col, row2, col2},
|
|
}}
|
|
}
|
|
|
|
// Bare @ row, col — just position cursor
|
|
for p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
|
|
p.advance() // skip any remaining tokens
|
|
}
|
|
p.expectEndOfStmt()
|
|
return &ast.ExprStmt{X: &ast.CallExpr{
|
|
Func: &ast.IdentExpr{Name: "SetPos"},
|
|
Args: []ast.Expr{row, col},
|
|
}}
|
|
}
|
|
|
|
func (p *Parser) parseAtSay(pos token.Position, row, col ast.Expr) ast.Stmt {
|
|
p.advance() // consume SAY
|
|
sayExpr := p.parseExpr()
|
|
|
|
// Check for GET after SAY
|
|
if p.current.Kind == token.IDENT && p.currentUpper() == "GET" {
|
|
return p.parseAtSayGet(pos, row, col, sayExpr)
|
|
}
|
|
|
|
// PICTURE clause
|
|
var pic ast.Expr
|
|
if p.current.Kind == token.IDENT && p.currentUpper() == "PICTURE" {
|
|
p.advance()
|
|
pic = p.parseExpr()
|
|
}
|
|
p.expectEndOfStmt()
|
|
return &ast.AtSayCmd{AtPos: pos, Row: row, Col: col, SayExpr: sayExpr, Picture: pic}
|
|
}
|
|
|
|
func (p *Parser) parseAtGet(pos token.Position, row, col ast.Expr) *ast.AtGetCmd {
|
|
p.advance() // consume GET
|
|
varExpr := p.parseExpr()
|
|
varName := ""
|
|
if ident, ok := varExpr.(*ast.IdentExpr); ok {
|
|
varName = ident.Name
|
|
}
|
|
|
|
var pic, valid, when ast.Expr
|
|
for p.current.Kind == token.IDENT {
|
|
switch p.currentUpper() {
|
|
case "PICTURE":
|
|
p.advance()
|
|
pic = p.parseExpr()
|
|
case "VALID":
|
|
p.advance()
|
|
valid = p.parseExpr()
|
|
case "WHEN":
|
|
p.advance()
|
|
when = p.parseExpr()
|
|
case "RANGE":
|
|
// RANGE low, high — skip both values
|
|
p.advance()
|
|
p.parseExpr() // low
|
|
p.expect(token.COMMA)
|
|
p.parseExpr() // high
|
|
case "COLOR", "COLOUR", "MESSAGE", "SEND", "GUISEND",
|
|
"CAPTION", "CARGO", "COLORSPEC":
|
|
// Skip keyword + value
|
|
p.advance()
|
|
p.parseExpr()
|
|
default:
|
|
goto done
|
|
}
|
|
}
|
|
done:
|
|
p.expectEndOfStmt()
|
|
return &ast.AtGetCmd{AtPos: pos, Row: row, Col: col, Var: varExpr, VarName: varName, Picture: pic, Valid: valid, When: when}
|
|
}
|
|
|
|
func (p *Parser) parseAtSayGet(pos token.Position, row, col ast.Expr, sayExpr ast.Expr) *ast.AtSayGetCmd {
|
|
p.advance() // consume GET
|
|
varExpr := p.parseExpr()
|
|
varName := ""
|
|
if ident, ok := varExpr.(*ast.IdentExpr); ok {
|
|
varName = ident.Name
|
|
}
|
|
|
|
var pic, valid, when ast.Expr
|
|
for p.current.Kind == token.IDENT {
|
|
switch p.currentUpper() {
|
|
case "PICTURE":
|
|
p.advance()
|
|
pic = p.parseExpr()
|
|
case "VALID":
|
|
p.advance()
|
|
valid = p.parseExpr()
|
|
case "WHEN":
|
|
p.advance()
|
|
when = p.parseExpr()
|
|
case "RANGE":
|
|
p.advance()
|
|
p.parseExpr()
|
|
p.expect(token.COMMA)
|
|
p.parseExpr()
|
|
case "COLOR", "COLOUR", "MESSAGE", "SEND", "GUISEND",
|
|
"CAPTION", "CARGO", "COLORSPEC":
|
|
p.advance()
|
|
p.parseExpr()
|
|
default:
|
|
goto done
|
|
}
|
|
}
|
|
done:
|
|
p.expectEndOfStmt()
|
|
return &ast.AtSayGetCmd{AtPos: pos, Row: row, Col: col, SayExpr: sayExpr, Var: varExpr, VarName: varName, Picture: pic, Valid: valid, When: when}
|
|
}
|
|
|
|
func (p *Parser) parseAtPrompt(pos token.Position, row, col ast.Expr) ast.Stmt {
|
|
p.advance() // consume PROMPT
|
|
prompt := p.parseExpr()
|
|
var msg ast.Expr
|
|
if p.current.Kind == token.IDENT && p.currentUpper() == "MESSAGE" {
|
|
p.advance()
|
|
msg = p.parseExpr()
|
|
}
|
|
p.expectEndOfStmt()
|
|
// Emit as: __AtPrompt(row, col, prompt [, msg])
|
|
args := []ast.Expr{row, col, prompt}
|
|
if msg != nil {
|
|
args = append(args, msg)
|
|
}
|
|
return &ast.ExprStmt{X: &ast.CallExpr{
|
|
Func: &ast.IdentExpr{Name: "__AtPrompt"},
|
|
Args: args,
|
|
}}
|
|
}
|
|
|
|
// === Five Go Extension Parsers ===
|
|
|
|
// looksLikeMultiAssign checks strictly: IDENT , IDENT [, IDENT...] :=
|
|
// Each token between start and := must be IDENT or COMMA only (no [ ( : etc.)
|
|
func (p *Parser) looksLikeMultiAssign() bool {
|
|
// Must start with IDENT (or keyword-as-ident)
|
|
if p.current.Kind != token.IDENT && p.current.Literal == "" {
|
|
return false
|
|
}
|
|
// Next token must be COMMA for multi-assign
|
|
if p.pos+1 >= len(p.tokens) || p.peekAt(1) != token.COMMA {
|
|
return false
|
|
}
|
|
// Scan from after first IDENT: COMMA, IDENT, COMMA, IDENT, ..., ASSIGN
|
|
expectComma := true // first IDENT already consumed, expect COMMA next
|
|
for i := p.pos + 1; i < len(p.tokens); i++ {
|
|
tk := p.tokens[i]
|
|
if tk.Kind == token.ASSIGN {
|
|
return expectComma == true // last was IDENT (expectComma=true), then :=
|
|
}
|
|
if tk.Kind == token.NEWLINE || tk.Kind == token.EOF {
|
|
return false
|
|
}
|
|
if expectComma {
|
|
if tk.Kind != token.COMMA {
|
|
return false
|
|
}
|
|
expectComma = false
|
|
} else {
|
|
// Expect IDENT or keyword-as-ident (including "_")
|
|
if tk.Kind != token.IDENT && tk.Literal == "" {
|
|
return false
|
|
}
|
|
expectComma = true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// parseMultiAssign: a, b := Func() or a, b, c := x, y, z or _, b := Func()
|
|
func (p *Parser) parseMultiAssign() *ast.MultiAssignStmt {
|
|
pos := p.current.Pos
|
|
var targets []string
|
|
|
|
// Parse target list: a, b, c or _, b
|
|
for {
|
|
name := "_"
|
|
if p.current.Kind == token.IDENT && p.current.Literal == "_" {
|
|
p.advance()
|
|
} else {
|
|
tok := p.expectMethodName()
|
|
name = tok.Literal
|
|
}
|
|
targets = append(targets, name)
|
|
if !p.match(token.COMMA) {
|
|
break
|
|
}
|
|
}
|
|
|
|
p.expect(token.ASSIGN) // :=
|
|
|
|
// Parse value list
|
|
var values []ast.Expr
|
|
values = append(values, p.parseExpr())
|
|
for p.match(token.COMMA) {
|
|
values = append(values, p.parseExpr())
|
|
}
|
|
p.expectEndOfStmt()
|
|
|
|
return &ast.MultiAssignStmt{AssignPos: pos, Targets: targets, Values: values}
|
|
}
|
|
|
|
// parseDefer: DEFER expr
|
|
func (p *Parser) parseDefer() *ast.DeferStmt {
|
|
pos := p.expect(token.DEFER_KW).Pos
|
|
// Parse the expression to defer (typically a method call: db:Close())
|
|
call := p.parseExpr()
|
|
p.expectEndOfStmt()
|
|
return &ast.DeferStmt{DeferPos: pos, Call: call}
|
|
}
|
|
|
|
// parseConstBlock: CONST ... END CONST
|
|
func (p *Parser) parseConstBlock() ast.Stmt {
|
|
pos := p.expect(token.CONST_KW).Pos
|
|
p.expectEndOfStmt()
|
|
|
|
var items []ast.ConstItem
|
|
p.skipNewlines()
|
|
|
|
for !p.atAny(token.END, token.ENDCASE, token.EOF) {
|
|
if p.current.Kind == token.IDENT || p.current.Literal != "" {
|
|
name := p.expectMethodName().Literal
|
|
var val ast.Expr
|
|
if p.match(token.ASSIGN) {
|
|
val = p.parseExpr()
|
|
}
|
|
items = append(items, ast.ConstItem{Name: name, Value: val})
|
|
p.expectEndOfStmt()
|
|
} else {
|
|
break
|
|
}
|
|
p.skipNewlines()
|
|
}
|
|
|
|
if p.match(token.END) {
|
|
// Skip optional CONST after END
|
|
if p.current.Kind == token.CONST_KW {
|
|
p.advance()
|
|
}
|
|
}
|
|
p.expectEndOfStmt()
|
|
|
|
_ = pos
|
|
return &ast.ExprStmt{X: &ast.LiteralExpr{Kind: token.NIL_LIT, Value: "NIL"}}
|
|
}
|
|
|
|
// parseShortIfAssign: IF var := expr ; condition ... ENDIF
|
|
// Detected inside parseIf when IF is followed by IDENT := expr ;
|
|
// === Five Concurrency Parsers ===
|
|
|
|
// parseWatch: WATCH / CASE msg := <- ch / CASE ch <- val / OTHERWISE / ENDWATCH
|
|
func (p *Parser) parseWatch() *ast.WatchStmt {
|
|
watchPos := p.expect(token.WATCH_KW).Pos
|
|
p.expectEndOfStmt()
|
|
|
|
var cases []*ast.WatchCase
|
|
var otherwise []ast.Stmt
|
|
p.skipNewlines()
|
|
|
|
for p.current.Kind == token.CASE {
|
|
casePos := p.advance().Pos
|
|
wc := &ast.WatchCase{CasePos: casePos}
|
|
|
|
if p.at(token.ARROW_LEFT) {
|
|
// CASE <- ch (receive, discard value)
|
|
p.advance() // consume <-
|
|
wc.RecvChan = p.parseExpr()
|
|
} else {
|
|
// CASE var := <- ch OR CASE ch <- val
|
|
first := p.parseExpr()
|
|
if p.at(token.ASSIGN) && p.peekAt(1) == token.ARROW_LEFT {
|
|
// CASE var := <- ch
|
|
p.advance() // consume :=
|
|
p.advance() // consume <-
|
|
wc.RecvChan = p.parseExpr()
|
|
if ident, ok := first.(*ast.IdentExpr); ok {
|
|
wc.RecvVar = ident.Name
|
|
}
|
|
} else if p.at(token.ARROW_LEFT) {
|
|
// CASE ch <- val (send)
|
|
p.advance() // consume <-
|
|
wc.SendChan = first
|
|
wc.SendVal = p.parseExpr()
|
|
} else {
|
|
// CASE expr (boolean guard — less common)
|
|
wc.RecvChan = first
|
|
}
|
|
}
|
|
p.expectEndOfStmt()
|
|
wc.Body = p.parseStmtBlock(token.CASE, token.OTHERWISE, token.END)
|
|
cases = append(cases, wc)
|
|
}
|
|
|
|
if p.match(token.OTHERWISE) {
|
|
p.expectEndOfStmt()
|
|
otherwise = p.parseStmtBlock(token.END)
|
|
}
|
|
|
|
endPos := p.current.Pos
|
|
p.match(token.END)
|
|
// Skip optional WATCH after END
|
|
if p.current.Kind == token.WATCH_KW {
|
|
p.advance()
|
|
}
|
|
p.expectEndOfStmt()
|
|
|
|
return &ast.WatchStmt{WatchPos: watchPos, Cases: cases, Otherwise: otherwise, EndPos: endPos}
|
|
}
|
|
|
|
// parseParallelFor: PARALLEL FOR i := 1 TO n / body / NEXT
|
|
func (p *Parser) parseParallelFor() ast.Stmt {
|
|
p.expect(token.PARALLEL_KW)
|
|
|
|
if p.current.Kind != token.FOR {
|
|
// Not PARALLEL FOR — skip
|
|
p.skipToEndOfLine()
|
|
p.expectEndOfStmt()
|
|
return &ast.ExprStmt{X: &ast.LiteralExpr{Kind: token.NIL_LIT, Value: "NIL"}}
|
|
}
|
|
|
|
forPos := p.expect(token.FOR).Pos
|
|
varTok := p.expectMethodName()
|
|
varName := varTok.Literal
|
|
if p.at(token.ARROW) {
|
|
p.advance()
|
|
fieldTok := p.expectMethodName()
|
|
varName = fieldTok.Literal
|
|
}
|
|
p.expect(token.ASSIGN)
|
|
start := p.parseExpr()
|
|
p.expect(token.TO)
|
|
toExpr := p.parseExpr()
|
|
|
|
var step ast.Expr
|
|
if p.match(token.STEP) {
|
|
step = p.parseExpr()
|
|
}
|
|
p.expectEndOfStmt()
|
|
|
|
body := p.parseStmtBlock(token.NEXT, token.END)
|
|
endPos := p.current.Pos
|
|
if !p.match(token.NEXT) {
|
|
p.match(token.END)
|
|
}
|
|
if p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
|
|
p.skipToEndOfLine()
|
|
}
|
|
p.expectEndOfStmt()
|
|
|
|
return &ast.ParallelForStmt{
|
|
ForPos: forPos, Var: varName, Start: start, To: toExpr,
|
|
Step: step, Body: body, EndPos: endPos,
|
|
}
|
|
}
|
|
|
|
// parseWithTimeout: WITH TIMEOUT n / body / ENDWITH
|
|
func (p *Parser) parseWithTimeout() *ast.TimeoutStmt {
|
|
withPos := p.advance().Pos // consume WITH
|
|
p.expect(token.TIMEOUT_KW) // consume TIMEOUT
|
|
duration := p.parseExpr()
|
|
p.expectEndOfStmt()
|
|
|
|
body := p.parseStmtBlock(token.END)
|
|
endPos := p.current.Pos
|
|
p.match(token.END)
|
|
// Skip optional WITH after END
|
|
if p.current.Kind == token.IDENT && p.currentUpper() == "WITH" {
|
|
p.advance()
|
|
}
|
|
p.expectEndOfStmt()
|
|
|
|
return &ast.TimeoutStmt{WithPos: withPos, Duration: duration, Body: body, EndPos: endPos}
|
|
}
|
|
|
|
func (p *Parser) isShortIfAssign() bool {
|
|
// Look ahead: IF ident := expr ; condition
|
|
if p.pos+3 >= len(p.tokens) {
|
|
return false
|
|
}
|
|
// Check pattern: IDENT := ... ;
|
|
for i := p.pos + 1; i < len(p.tokens); i++ {
|
|
if p.tokens[i].Kind == token.SEMICOLON {
|
|
// Found ; before newline — it's short if
|
|
return true
|
|
}
|
|
if p.tokens[i].Kind == token.NEWLINE || p.tokens[i].Kind == token.EOF {
|
|
return false
|
|
}
|
|
}
|
|
return false
|
|
}
|