`LIST [<fields>] [OFF] [FOR ...] [WHILE ...] [NEXT ...] [RECORD ...]
[REST] [ALL]` and `DISPLAY [<fields>] [OFF] [FOR ...] ... [ALL]`
reach the parser as plain function calls to a new RTL primitive
__dbList (rtlDbList in hbrtl/database.go).
Implementation: walk the workarea under dbEval-style FOR/WHILE/NEXT/
RECORD/REST bounds. For each visible record, evaluate each column
block and emit the rendered values via valueToDisplay (the same
formatter QOut already uses). Empty fields list defaults to
"all fields". OFF suppresses the record-number prefix.
LIST always emits the full filtered range; DISPLAY without ALL emits
only the current record (encoded as nCount=1). TO PRINTER / TO FILE
clauses are not yet wired through — for now everything goes to
stdout.
Wiring up LIST/DISPLAY surfaced four further gaps in PP that were
silently masking bugs in any rule with multiple word-list / list /
optional clauses chained together:
* matchSegment refused MarkerWordList inside `[...]`. The LIST
rule's `[<off:OFF>]` clause therefore never set the off
capture, and `<.off.>` substituted to nothing instead of .T./.F.
matchSegment now matches WordList markers the same way the
top-level matcher does.
* `<v,...>` and `<(f)>` capture stop boundaries didn't include the
values of following MarkerWordList markers. For
`[<v,...>] [<off:OFF>] [<all:ALL>]` against `LIST id, name OFF`,
the v list would happily eat OFF. New addStopFrom helper
contributes both literal keywords and word-list values; both
matchSegment's MarkerList branch and captureExpression now use
it.
* Optional-repeat loop in matchPattern merged a no-progress
iteration's empty capture into the running multi-capture string
(with the `\x01` separator) before the no-progress break check
fired. So a successful first iteration's value got contaminated
and the substitution loop then skipped it as multi-capture
garbage. The merge now happens after the progress check.
* Unreferenced `<.name.>` markers (optional clauses that didn't
match in the input) were getting cleaned up to empty by the
generic marker scrubber instead of the .F. sentinel Harbour's
std.ch expects. New replaceUnreferencedLogify pass mirrors the
existing replaceUnreferencedBlockify and runs just before the
cleanup.
Parser cleanup: LIST and DISPLAY removed from the IDENT-statement
no-op switch in both parseIdentStmt and parseExprStmt.
Gates green:
go test ./... : PASS
FiveSql2 SQL:1999 : 43/43
Harbour compat : 56/56
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2479 lines
67 KiB
Go
2479 lines
67 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 ---
|
|
|
|
// isIdentSuffix reports whether the given token could follow an
|
|
// identifier, signalling that a keyword on the prior slot is being
|
|
// used as a variable name (array index, method call, assignment,
|
|
// etc.). Used by parseStmtBlock to avoid prematurely ending a
|
|
// statement block when a keyword identifier appears.
|
|
func isIdentSuffix(k token.Kind) bool {
|
|
switch k {
|
|
case token.LBRACKET, token.ASSIGN, token.PLUSEQ, token.MINUSEQ,
|
|
token.STAREQ, token.SLASHEQ, token.PERCENTEQ, token.POWEREQ,
|
|
token.INC, token.DEC, token.COLON, token.DOT:
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// parseStmtBlock parses statements until one of the stop tokens.
|
|
func (p *Parser) parseStmtBlock(stopTokens ...token.Kind) []ast.Stmt {
|
|
var stmts []ast.Stmt
|
|
for {
|
|
p.skipNewlines()
|
|
if p.current.Kind == token.EOF {
|
|
break
|
|
}
|
|
for _, stop := range stopTokens {
|
|
if p.current.Kind == stop {
|
|
// Don't stop at RETURN if it's used as variable: return := ...
|
|
if stop == token.RETURN &&
|
|
p.peekAt(1) == token.ASSIGN {
|
|
break // continue parsing as statement
|
|
}
|
|
// Don't stop at a keyword-used-as-identifier. Harbour
|
|
// allows keywords like CASE/DO as variable names, so
|
|
// `case[idx]`, `case := 1`, `case:method()` are
|
|
// expression statements, not new block arms. Peek the
|
|
// next token for identifier-ish suffixes.
|
|
if isIdentSuffix(p.peekAt(1)) {
|
|
break // treat current token as identifier, parse as stmt
|
|
}
|
|
return stmts
|
|
}
|
|
}
|
|
stmt := p.parseStmt()
|
|
if stmt != nil {
|
|
stmts = append(stmts, stmt)
|
|
}
|
|
}
|
|
return stmts
|
|
}
|
|
|
|
func (p *Parser) parseStmt() ast.Stmt {
|
|
// Registry lookup — O(1) dispatch
|
|
if fn := p.lookupStmtParser(); fn != nil {
|
|
return fn(p)
|
|
}
|
|
|
|
// IDENT-based commands (xBase multi-word: COPY, SORT, etc.)
|
|
if p.current.Kind == token.IDENT {
|
|
return p.parseIdentStmt()
|
|
}
|
|
|
|
// Multi-assign: a, b := expr
|
|
if p.looksLikeMultiAssign() {
|
|
return p.parseMultiAssign()
|
|
}
|
|
|
|
// Default: expression statement
|
|
return p.parseExprStmt()
|
|
}
|
|
|
|
|
|
// parseIdentStmt handles IDENT-based commands (xBase multi-word: COPY, SORT, etc.)
|
|
func (p *Parser) parseIdentStmt() ast.Stmt {
|
|
upper := p.currentUpper()
|
|
|
|
// WITH TIMEOUT → timeout context
|
|
if upper == "WITH" && p.peekAt(1) == token.TIMEOUT_KW {
|
|
return p.parseWithTimeout()
|
|
}
|
|
|
|
// xBase commands that consume entire line. These are silent no-ops
|
|
// for now — they have no RTL backend, so std.ch deliberately omits
|
|
// rules for them. ERASE / RENAME / LOCATE / CONTINUE / COMMIT /
|
|
// CLOSE / REINDEX / PACK / ZAP / UNLOCK / KEYBOARD / RUN are now
|
|
// rewritten by compiler/pp/std.ch into function calls before the
|
|
// parser sees them.
|
|
switch upper {
|
|
case "TOTAL", "UPDATE",
|
|
"LABEL", "REPORT", "ACCEPT", "INPUT",
|
|
"JOIN", "RELEASE", "SAVE", "RESTORE",
|
|
"DIR", "STORE", "NOTE", "TEXT", "ENDTEXT",
|
|
"WITH", "CLEAR":
|
|
p.advance()
|
|
for p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
|
|
p.advance()
|
|
}
|
|
p.expectEndOfStmt()
|
|
return &ast.ExprStmt{X: &ast.LiteralExpr{Kind: token.NIL_LIT, Value: "NIL"}}
|
|
|
|
case "FIVE_GODUMP__":
|
|
// GoDump is a Decl, wrap as ExprStmt for statement context
|
|
p.advance() // consume FIVE_GODUMP__
|
|
idx := 0
|
|
if p.current.Kind == token.INT {
|
|
fmt.Sscanf(p.current.Literal, "%d", &idx)
|
|
p.advance()
|
|
}
|
|
p.expectEndOfStmt()
|
|
// Store as nil statement — gengo handles GoDumpDecl at file level
|
|
return &ast.ExprStmt{X: &ast.LiteralExpr{Kind: token.NIL_LIT, Value: "NIL"}}
|
|
}
|
|
|
|
// Multi-assign check: a, b := expr
|
|
if p.looksLikeMultiAssign() {
|
|
return p.parseMultiAssign()
|
|
}
|
|
|
|
// Default: expression statement (function call, assignment, etc.)
|
|
return p.parseExprStmt()
|
|
}
|
|
|
|
func (p *Parser) parseExprStmt() ast.Stmt {
|
|
// READ [SAVE] [MSG AT ...] [MSG COLOR ...] — special case
|
|
if p.current.Kind == token.IDENT && p.currentUpper() == "READ" {
|
|
pos := p.advance().Pos
|
|
save := false
|
|
if p.current.Kind == token.IDENT && p.currentUpper() == "SAVE" {
|
|
save = true
|
|
p.advance()
|
|
}
|
|
// Skip optional clauses: MSG AT row,col,col2 / MSG COLOR "..." etc.
|
|
if p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
|
|
p.skipToEndOfLine()
|
|
}
|
|
p.expectEndOfStmt()
|
|
return &ast.ReadCmd{ReadPos: pos, Save: save}
|
|
}
|
|
// TRY / CATCH [oErr] / END — Harbour extension, maps to BEGIN SEQUENCE / RECOVER
|
|
if p.current.Kind == token.IDENT && p.currentUpper() == "TRY" {
|
|
return p.parseTryCatch()
|
|
}
|
|
// xBase commands that consume entire line — duplicate of the switch
|
|
// in parseIdentStmt(). The keyword set is kept in sync; std.ch covers
|
|
// ERASE/RENAME/LOCATE/CONTINUE/COMMIT/CLOSE/REINDEX/PACK/ZAP/UNLOCK/
|
|
// KEYBOARD/RUN, so they're absent here.
|
|
if p.current.Kind == token.IDENT {
|
|
// WITH TIMEOUT n / body / ENDWITH
|
|
if p.currentUpper() == "WITH" &&
|
|
p.peekAt(1) == token.TIMEOUT_KW {
|
|
return p.parseWithTimeout()
|
|
}
|
|
switch p.currentUpper() {
|
|
case "COPY", "SORT", "COUNT", "SUM", "AVERAGE", "TOTAL", "UPDATE",
|
|
"LABEL", "REPORT", "ACCEPT", "INPUT",
|
|
"JOIN", "RELEASE", "SAVE", "RESTORE",
|
|
"DIR", "STORE", "NOTE", "TEXT", "ENDTEXT",
|
|
"WITH", "CLEAR", "DISPLAY", "LIST":
|
|
// Consume entire line — these are complex multi-word commands
|
|
p.advance()
|
|
for p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
|
|
p.advance()
|
|
}
|
|
p.expectEndOfStmt()
|
|
return &ast.ExprStmt{X: &ast.LiteralExpr{Kind: token.NIL_LIT, Value: "NIL"}}
|
|
}
|
|
}
|
|
|
|
expr := p.parseExpr()
|
|
|
|
// ch <- value (channel send)
|
|
if p.at(token.ARROW_LEFT) {
|
|
pos := p.advance().Pos
|
|
val := p.parseExpr()
|
|
p.expectEndOfStmt()
|
|
return &ast.ChanSendStmt{ChanPos: pos, Chan: expr, Value: val}
|
|
}
|
|
|
|
p.expectEndOfStmt()
|
|
return &ast.ExprStmt{X: expr}
|
|
}
|
|
|
|
// --- Control flow ---
|
|
|
|
func (p *Parser) parseIf() *ast.IfStmt {
|
|
ifPos := p.expect(token.IF).Pos
|
|
cond := p.parseExpr()
|
|
p.expectEndOfStmt()
|
|
|
|
body := p.parseStmtBlock(token.ELSEIF, token.ELSE, token.ENDIF, token.END)
|
|
|
|
var elseIfs []*ast.ElseIfClause
|
|
for p.current.Kind == token.ELSEIF {
|
|
eiPos := p.advance().Pos
|
|
eiCond := p.parseExpr()
|
|
p.expectEndOfStmt()
|
|
eiBody := p.parseStmtBlock(token.ELSEIF, token.ELSE, token.ENDIF, token.END)
|
|
elseIfs = append(elseIfs, &ast.ElseIfClause{
|
|
ElseIfPos: eiPos,
|
|
Cond: eiCond,
|
|
Body: eiBody,
|
|
})
|
|
}
|
|
|
|
var elseBody []ast.Stmt
|
|
if p.match(token.ELSE) {
|
|
p.expectEndOfStmt()
|
|
elseBody = p.parseStmtBlock(token.ENDIF, token.END)
|
|
}
|
|
|
|
endPos := p.current.Pos
|
|
if !p.match(token.ENDIF) {
|
|
p.match(token.END) // alternative
|
|
}
|
|
p.expectEndOfStmt()
|
|
|
|
return &ast.IfStmt{
|
|
IfPos: ifPos,
|
|
Cond: cond,
|
|
Body: body,
|
|
ElseIfs: elseIfs,
|
|
ElseBody: elseBody,
|
|
EndPos: endPos,
|
|
}
|
|
}
|
|
|
|
// looksLikeIIF checks if IF( starts an IIF-style call: IF(cond, true, false)
|
|
// by scanning for commas inside the parenthesized expression.
|
|
func (p *Parser) looksLikeIIF() bool {
|
|
// Start after IF token; expect ( at p.pos+1
|
|
if p.pos+1 >= len(p.tokens) || p.peekAt(1) != token.LPAREN {
|
|
return false
|
|
}
|
|
depth := 0
|
|
commas := 0
|
|
for i := p.pos + 2; i < len(p.tokens); i++ { // start INSIDE the parens
|
|
switch p.tokens[i].Kind {
|
|
case token.LPAREN, token.LBRACE, token.LBRACKET:
|
|
depth++
|
|
case token.RPAREN:
|
|
if depth == 0 {
|
|
return commas >= 2
|
|
}
|
|
depth--
|
|
case token.RBRACE, token.RBRACKET:
|
|
if depth > 0 {
|
|
depth--
|
|
}
|
|
case token.COMMA:
|
|
if depth == 0 {
|
|
commas++
|
|
}
|
|
case token.NEWLINE, token.EOF:
|
|
return false
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// parseDoProc: DO funcname [WITH arg1, arg2, ...]
|
|
func (p *Parser) parseDoProc() ast.Stmt {
|
|
p.advance() // skip DO
|
|
funcName := p.expectMethodName().Literal
|
|
|
|
var args []ast.Expr
|
|
if p.current.Kind == token.WITH {
|
|
p.advance() // skip WITH
|
|
for {
|
|
args = append(args, p.parseExpr())
|
|
if !p.match(token.COMMA) {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
p.expectEndOfStmt()
|
|
|
|
return &ast.ExprStmt{X: &ast.CallExpr{
|
|
Func: &ast.IdentExpr{Name: funcName},
|
|
Args: args,
|
|
}}
|
|
}
|
|
|
|
func (p *Parser) parseDoWhile() *ast.DoWhileStmt {
|
|
var doPos token.Position
|
|
if p.current.Kind == token.DO {
|
|
doPos = p.advance().Pos
|
|
p.expect(token.WHILE)
|
|
} else {
|
|
// Bare WHILE (Clipper compatibility)
|
|
doPos = p.expect(token.WHILE).Pos
|
|
}
|
|
cond := p.parseExpr()
|
|
p.expectEndOfStmt()
|
|
|
|
body := p.parseStmtBlock(token.ENDDO, token.END)
|
|
|
|
endPos := p.current.Pos
|
|
if !p.match(token.ENDDO) {
|
|
p.match(token.END)
|
|
}
|
|
p.expectEndOfStmt()
|
|
|
|
return &ast.DoWhileStmt{DoPos: doPos, Cond: cond, Body: body, EndPos: endPos}
|
|
}
|
|
|
|
func (p *Parser) parseFor() ast.Stmt {
|
|
forPos := p.expect(token.FOR).Pos
|
|
|
|
// FOR EACH var IN collection
|
|
if p.match(token.EACH) {
|
|
return p.parseForEach(forPos)
|
|
}
|
|
|
|
// FOR var := start TO end [STEP step]
|
|
// Variable can be aliased: M->TEST or simple: i
|
|
varTok := p.expectMethodName()
|
|
varName := varTok.Literal
|
|
// Handle M->varname
|
|
if p.at(token.ARROW) {
|
|
p.advance() // skip ->
|
|
fieldTok := p.expectMethodName()
|
|
varName = fieldTok.Literal
|
|
}
|
|
p.expect(token.ASSIGN) // :=
|
|
start := p.parseExpr()
|
|
p.expect(token.TO)
|
|
toExpr := p.parseExpr()
|
|
|
|
var step ast.Expr
|
|
if p.match(token.STEP) {
|
|
step = p.parseExpr()
|
|
}
|
|
p.expectEndOfStmt()
|
|
|
|
body := p.parseStmtBlock(token.NEXT, token.END)
|
|
nextPos := p.current.Pos
|
|
if !p.match(token.NEXT) {
|
|
p.match(token.END)
|
|
}
|
|
// Skip optional counter variable after NEXT (e.g. NEXT nVar)
|
|
if p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
|
|
p.skipToEndOfLine()
|
|
}
|
|
p.expectEndOfStmt()
|
|
|
|
return &ast.ForStmt{
|
|
ForPos: forPos, Var: varName, Start: start, To: toExpr,
|
|
Step: step, Body: body, NextPos: nextPos,
|
|
}
|
|
}
|
|
|
|
func (p *Parser) parseForEach(forPos token.Position) *ast.ForEachStmt {
|
|
varName := p.expect(token.IDENT).Literal
|
|
// Multi-variable FOR EACH: FOR EACH a, b, c IN x, y, z
|
|
// Skip extra variables — use only first var and first collection
|
|
var extraVars []string
|
|
for p.match(token.COMMA) {
|
|
extraVars = append(extraVars, p.expect(token.IDENT).Literal)
|
|
}
|
|
p.expect(token.IN)
|
|
collection := p.parseExpr()
|
|
// Skip extra collections
|
|
for p.match(token.COMMA) {
|
|
p.parseExpr() // consume and discard
|
|
}
|
|
descend := false
|
|
if p.current.Kind == token.DESCENDING {
|
|
p.advance()
|
|
descend = true
|
|
}
|
|
p.expectEndOfStmt()
|
|
|
|
body := p.parseStmtBlock(token.NEXT, token.END)
|
|
nextPos := p.current.Pos
|
|
if !p.match(token.NEXT) {
|
|
p.match(token.END)
|
|
}
|
|
// Skip optional counter variable after NEXT
|
|
if p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
|
|
p.skipToEndOfLine()
|
|
}
|
|
p.expectEndOfStmt()
|
|
|
|
return &ast.ForEachStmt{
|
|
ForPos: forPos, Var: varName, Collection: collection,
|
|
Descend: descend, Body: body, NextPos: nextPos,
|
|
}
|
|
}
|
|
|
|
// parseDoCase: DO CASE / CASE cond / OTHERWISE / ENDCASE
|
|
// Harbour: equivalent to IF/ELSEIF/ELSE chain
|
|
func (p *Parser) parseDoCase() *ast.IfStmt {
|
|
doPos := p.expect(token.DO).Pos
|
|
p.expect(token.CASE) // consume CASE after DO
|
|
p.expectEndOfStmt()
|
|
p.skipNewlines()
|
|
|
|
// First CASE
|
|
if p.current.Kind != token.CASE {
|
|
p.error("expected CASE after DO CASE")
|
|
return &ast.IfStmt{IfPos: doPos, EndPos: doPos}
|
|
}
|
|
|
|
p.advance() // consume CASE
|
|
cond := p.parseExpr()
|
|
p.expectEndOfStmt()
|
|
body := p.parseStmtBlock(token.CASE, token.OTHERWISE, token.ENDCASE, token.END)
|
|
|
|
// Build as IfStmt with ElseIfs
|
|
var elseIfs []*ast.ElseIfClause
|
|
for p.current.Kind == token.CASE {
|
|
eiPos := p.advance().Pos
|
|
eiCond := p.parseExpr()
|
|
p.expectEndOfStmt()
|
|
eiBody := p.parseStmtBlock(token.CASE, token.OTHERWISE, token.ENDCASE, token.END)
|
|
elseIfs = append(elseIfs, &ast.ElseIfClause{
|
|
ElseIfPos: eiPos,
|
|
Cond: eiCond,
|
|
Body: eiBody,
|
|
})
|
|
}
|
|
|
|
var elseBody []ast.Stmt
|
|
if p.match(token.OTHERWISE) {
|
|
p.expectEndOfStmt()
|
|
elseBody = p.parseStmtBlock(token.ENDCASE, token.END)
|
|
}
|
|
|
|
endPos := p.current.Pos
|
|
if !p.match(token.ENDCASE) {
|
|
p.match(token.END)
|
|
}
|
|
p.expectEndOfStmt()
|
|
|
|
return &ast.IfStmt{
|
|
IfPos: doPos,
|
|
Cond: cond,
|
|
Body: body,
|
|
ElseIfs: elseIfs,
|
|
ElseBody: elseBody,
|
|
EndPos: endPos,
|
|
}
|
|
}
|
|
|
|
func (p *Parser) parseSwitch() *ast.SwitchStmt {
|
|
switchPos := p.expect(token.SWITCH).Pos
|
|
expr := p.parseExpr()
|
|
p.expectEndOfStmt()
|
|
|
|
var cases []*ast.CaseClause
|
|
var otherwise []ast.Stmt
|
|
p.skipNewlines()
|
|
|
|
for p.current.Kind == token.CASE {
|
|
casePos := p.advance().Pos
|
|
val := p.parseExpr()
|
|
p.expectEndOfStmt()
|
|
caseBody := p.parseStmtBlock(token.CASE, token.OTHERWISE, token.ENDSWITCH, token.ENDCASE, token.END)
|
|
cases = append(cases, &ast.CaseClause{CasePos: casePos, Value: val, Body: caseBody})
|
|
}
|
|
|
|
if p.match(token.OTHERWISE) {
|
|
p.expectEndOfStmt()
|
|
otherwise = p.parseStmtBlock(token.ENDSWITCH, token.ENDCASE, token.END)
|
|
}
|
|
|
|
endPos := p.current.Pos
|
|
if !p.match(token.ENDSWITCH) {
|
|
if !p.match(token.ENDCASE) {
|
|
p.match(token.END)
|
|
}
|
|
}
|
|
p.expectEndOfStmt()
|
|
|
|
return &ast.SwitchStmt{SwitchPos: switchPos, Expr: expr, Cases: cases, Otherwise: otherwise, EndPos: endPos}
|
|
}
|
|
|
|
func (p *Parser) parseBeginSequence() *ast.SeqStmt {
|
|
beginPos := p.expect(token.BEGIN).Pos
|
|
p.expect(token.SEQUENCE)
|
|
p.expectEndOfStmt()
|
|
|
|
body := p.parseStmtBlock(token.RECOVER, token.END)
|
|
|
|
var recoverVar string
|
|
var recoverBody []ast.Stmt
|
|
if p.match(token.RECOVER) {
|
|
if p.match(token.USING) {
|
|
recoverVar = p.expect(token.IDENT).Literal
|
|
}
|
|
p.expectEndOfStmt()
|
|
recoverBody = p.parseStmtBlock(token.END)
|
|
}
|
|
|
|
endPos := p.current.Pos
|
|
p.match(token.END)
|
|
p.match(token.SEQUENCE) // optional: END SEQUENCE
|
|
p.expectEndOfStmt()
|
|
|
|
return &ast.SeqStmt{
|
|
BeginPos: beginPos, Body: body,
|
|
RecoverVar: recoverVar, RecoverBody: recoverBody,
|
|
EndPos: endPos,
|
|
}
|
|
}
|
|
|
|
// parseTryCatch: TRY ... CATCH [oErr] ... END — maps to SeqStmt
|
|
func (p *Parser) parseTryCatch() *ast.SeqStmt {
|
|
beginPos := p.advance().Pos // consume TRY
|
|
p.expectEndOfStmt()
|
|
|
|
// Parse body until CATCH or END
|
|
var body []ast.Stmt
|
|
for !p.atAny(token.EOF) {
|
|
if p.current.Kind == token.IDENT && p.currentUpper() == "CATCH" {
|
|
break
|
|
}
|
|
if p.current.Kind == token.END {
|
|
break
|
|
}
|
|
body = append(body, p.parseStmt())
|
|
p.skipNewlines()
|
|
}
|
|
|
|
var recoverVar string
|
|
var recoverBody []ast.Stmt
|
|
if p.current.Kind == token.IDENT && p.currentUpper() == "CATCH" {
|
|
p.advance() // consume CATCH
|
|
if p.current.Kind == token.IDENT && p.current.Kind != token.NEWLINE {
|
|
recoverVar = p.advance().Literal
|
|
}
|
|
p.expectEndOfStmt()
|
|
recoverBody = p.parseStmtBlock(token.END)
|
|
}
|
|
|
|
endPos := p.current.Pos
|
|
p.match(token.END)
|
|
if p.current.Kind == token.IDENT && p.currentUpper() == "TRY" {
|
|
p.advance() // END TRY
|
|
}
|
|
p.expectEndOfStmt()
|
|
|
|
return &ast.SeqStmt{
|
|
BeginPos: beginPos, Body: body,
|
|
RecoverVar: recoverVar, RecoverBody: recoverBody,
|
|
EndPos: endPos,
|
|
}
|
|
}
|
|
|
|
func (p *Parser) parseReturn() *ast.ReturnStmt {
|
|
pos := p.expect(token.RETURN).Pos
|
|
var val ast.Expr
|
|
var vals []ast.Expr
|
|
if p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
|
|
val = p.parseExpr()
|
|
// Multi-return: RETURN a, b, c
|
|
if p.match(token.COMMA) {
|
|
vals = append(vals, val)
|
|
vals = append(vals, p.parseExpr())
|
|
for p.match(token.COMMA) {
|
|
vals = append(vals, p.parseExpr())
|
|
}
|
|
val = vals[0] // keep first for backward compat
|
|
}
|
|
}
|
|
p.expectEndOfStmt()
|
|
return &ast.ReturnStmt{ReturnPos: pos, Value: val, Values: vals}
|
|
}
|
|
|
|
func (p *Parser) parseQOut(isQQ bool) *ast.QOutStmt {
|
|
pos := p.advance().Pos // consume ? or ??
|
|
var exprs []ast.Expr
|
|
if p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
|
|
exprs = append(exprs, p.parseExpr())
|
|
for p.match(token.COMMA) {
|
|
exprs = append(exprs, p.parseExpr())
|
|
}
|
|
}
|
|
p.expectEndOfStmt()
|
|
return &ast.QOutStmt{QPos: pos, IsQQ: isQQ, Exprs: exprs}
|
|
}
|
|
|
|
func (p *Parser) parsePrivatePublic(scope ast.VarScope) ast.Stmt {
|
|
tok := p.advance()
|
|
var vars []*ast.VarInit
|
|
for {
|
|
// Handle ¯o in PRIVATE/PUBLIC list
|
|
if p.at(token.AMPERSAND) {
|
|
macroExpr := p.parseMacro()
|
|
var init ast.Expr
|
|
if p.match(token.ASSIGN) {
|
|
init = p.parseExpr()
|
|
}
|
|
name := "macro"
|
|
if me, ok := macroExpr.(*ast.MacroExpr); ok {
|
|
if id, ok2 := me.Expr.(*ast.IdentExpr); ok2 {
|
|
name = id.Name
|
|
}
|
|
}
|
|
vars = append(vars, &ast.VarInit{NamePos: macroExpr.Pos(), Name: name, Init: init})
|
|
if !p.match(token.COMMA) {
|
|
break
|
|
}
|
|
continue
|
|
}
|
|
name := p.expectMethodName() // allow keywords as var names (MEMVAR, etc.)
|
|
var init ast.Expr
|
|
// Skip AS type declaration
|
|
if p.match(token.AS) {
|
|
for p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF &&
|
|
p.current.Kind != token.ASSIGN && p.current.Kind != token.COMMA {
|
|
p.advance()
|
|
}
|
|
}
|
|
if p.match(token.ASSIGN) {
|
|
init = p.parseExpr()
|
|
}
|
|
vars = append(vars, &ast.VarInit{NamePos: name.Pos, Name: name.Literal, Init: init})
|
|
if !p.match(token.COMMA) {
|
|
break
|
|
}
|
|
}
|
|
p.expectEndOfStmt()
|
|
return &ast.VarDecl{DeclPos: tok.Pos, Scope: scope, Vars: vars}
|
|
}
|
|
|
|
// --- xBase commands ---
|
|
|
|
func (p *Parser) parseUse() *ast.UseCmd {
|
|
pos := p.expect(token.USE).Pos
|
|
var file ast.Expr
|
|
var via, alias string
|
|
var aliasExprNode ast.Expr
|
|
var shared, readOnly bool
|
|
|
|
// USE without args = close
|
|
if p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
|
|
// `USE &cFile` / `USE &(expr)` — macro expression yields the
|
|
// filename at runtime. Harbour uses this heavily for data-
|
|
// driven apps (USE &cTable INDEX &cIndex ...).
|
|
if p.at(token.AMPERSAND) {
|
|
file = p.parseMacro()
|
|
} else if p.at(token.IDENT) {
|
|
// Bare ident as filename: USE myfile / USE myfile.dbf / USE myfile NEW
|
|
// In Harbour, USE <name> treats name as a filename string, not a variable.
|
|
name := p.advance().Literal
|
|
if p.at(token.DOT) && (p.peekAt(1) == token.IDENT || p.peekAt(1) == token.INT) {
|
|
p.advance() // skip DOT
|
|
ext := p.advance().Literal
|
|
name = name + "." + ext
|
|
}
|
|
file = &ast.LiteralExpr{ValuePos: pos, Kind: token.STRING, Value: name}
|
|
} else {
|
|
file = p.parseExpr()
|
|
p.consumeFileExtension(file)
|
|
}
|
|
}
|
|
|
|
// Parse optional clauses: VIA, ALIAS, EXCLUSIVE, SHARED, NEW, READONLY
|
|
for p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
|
|
if p.current.Kind == token.IDENT {
|
|
upper := p.currentUpper()
|
|
if upper == "VIA" {
|
|
p.advance()
|
|
if p.at(token.STRING) {
|
|
via = p.current.Literal
|
|
p.advance()
|
|
} else {
|
|
via = p.expectMethodName().Literal
|
|
}
|
|
continue
|
|
}
|
|
if upper == "ALIAS" {
|
|
p.advance()
|
|
if p.at(token.AMPERSAND) {
|
|
// `ALIAS &cAlias` / `&cAlias.1` — compute the alias
|
|
// name at runtime via macro evaluation.
|
|
aliasExprNode = p.parseMacro()
|
|
} else if p.at(token.LPAREN) {
|
|
// ALIAS ( expr ) — parenthesized alias expression (runtime)
|
|
p.advance() // skip (
|
|
aliasExpr := p.parseExpr()
|
|
p.expect(token.RPAREN)
|
|
if lit, ok := aliasExpr.(*ast.LiteralExpr); ok && lit.Kind == token.STRING {
|
|
alias = lit.Value // constant string — store directly
|
|
} else {
|
|
aliasExprNode = aliasExpr // dynamic — evaluate at runtime
|
|
}
|
|
} else {
|
|
alias = p.expectMethodName().Literal
|
|
}
|
|
continue
|
|
}
|
|
if upper == "SHARED" {
|
|
shared = true
|
|
p.advance()
|
|
continue
|
|
}
|
|
if upper == "READONLY" {
|
|
readOnly = true
|
|
p.advance()
|
|
continue
|
|
}
|
|
if upper == "EXCLUSIVE" || upper == "NEW" || upper == "ADDITIVE" {
|
|
p.advance()
|
|
continue
|
|
}
|
|
if upper == "INDEX" {
|
|
// INDEX file1[, file2, ...] — skip to EOL
|
|
p.skipToEndOfLine()
|
|
break
|
|
}
|
|
}
|
|
if p.current.Kind == token.ALIAS {
|
|
p.advance()
|
|
if p.at(token.AMPERSAND) {
|
|
aliasExprNode = p.parseMacro()
|
|
} else if p.at(token.LPAREN) {
|
|
// ALIAS ( expr ) — parenthesized alias expression
|
|
p.advance()
|
|
ae := p.parseExpr()
|
|
p.expect(token.RPAREN)
|
|
if lit, ok := ae.(*ast.LiteralExpr); ok && lit.Kind == token.STRING {
|
|
alias = lit.Value
|
|
} else {
|
|
aliasExprNode = ae
|
|
}
|
|
} else {
|
|
alias = p.expectMethodName().Literal
|
|
}
|
|
continue
|
|
}
|
|
if p.current.Kind == token.INDEX {
|
|
// INDEX file1, file2, ... — skip to EOL
|
|
p.skipToEndOfLine()
|
|
break
|
|
}
|
|
break
|
|
}
|
|
|
|
p.expectEndOfStmt()
|
|
return &ast.UseCmd{UsePos: pos, File: file, Via: via, Alias: alias, AliasExpr: aliasExprNode, Shared: shared, ReadOnly: readOnly}
|
|
}
|
|
|
|
func (p *Parser) parseSelect() *ast.SelectCmd {
|
|
pos := p.expect(token.SELECT).Pos
|
|
// Classic Clipper/Harbour semantics: `SELECT <alias>` treats a bare
|
|
// identifier as a literal alias name (string), not as an expression.
|
|
// Wrap in parens to force expression evaluation — e.g. `SELECT (n)`
|
|
// where n is a local holding an area number or alias name.
|
|
//
|
|
// Without this, unresolved identifiers fell back to PushMemvar(name)
|
|
// which returned NIL, and _wa.Select("") quietly allocated a fresh
|
|
// empty workarea, stranding the caller's real data in the previous
|
|
// slot. Visible symptom: `SELECT ALTSRC` inside SqlAlterAddColumn
|
|
// picked up a phantom area and the row-copy loop saw EOF from the
|
|
// first iteration (no rows migrated).
|
|
var area ast.Expr
|
|
if p.current.Kind == token.IDENT {
|
|
// Peek: only treat bare IDENT as literal alias when it's the
|
|
// entire argument (next token ends the statement). `SELECT x:y`
|
|
// or `SELECT f()` must parse as expressions so the dispatch
|
|
// below still routes through parseExpr.
|
|
next := p.peekAt(1)
|
|
if next == token.NEWLINE || next == token.SEMICOLON || next == token.EOF {
|
|
tok := p.advance()
|
|
area = &ast.LiteralExpr{ValuePos: tok.Pos, Kind: token.STRING, Value: tok.Literal}
|
|
}
|
|
}
|
|
if area == nil {
|
|
area = p.parseExpr()
|
|
}
|
|
p.expectEndOfStmt()
|
|
return &ast.SelectCmd{SelectPos: pos, Area: area}
|
|
}
|
|
|
|
func (p *Parser) parseGo() *ast.GoCmd {
|
|
pos := p.advance().Pos // GO or GOTO
|
|
var dir string
|
|
var recNo ast.Expr
|
|
|
|
switch p.current.Kind {
|
|
case token.TOP:
|
|
dir = "TOP"
|
|
p.advance()
|
|
case token.BOTTOM:
|
|
dir = "BOTTOM"
|
|
p.advance()
|
|
default:
|
|
recNo = p.parseExpr()
|
|
}
|
|
p.expectEndOfStmt()
|
|
return &ast.GoCmd{GoPos: pos, Direction: dir, RecNo: recNo}
|
|
}
|
|
|
|
func (p *Parser) parseSkip() *ast.SkipCmd {
|
|
pos := p.expect(token.SKIP_KW).Pos
|
|
var count ast.Expr
|
|
if p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
|
|
count = p.parseExpr()
|
|
}
|
|
p.expectEndOfStmt()
|
|
return &ast.SkipCmd{SkipPos: pos, Count: count}
|
|
}
|
|
|
|
func (p *Parser) parseSeek() *ast.SeekCmd {
|
|
pos := p.expect(token.SEEK).Pos
|
|
key := p.parseExpr()
|
|
softSeek := false
|
|
if p.current.Kind == token.SOFTSEEK {
|
|
p.advance()
|
|
softSeek = true
|
|
}
|
|
p.expectEndOfStmt()
|
|
return &ast.SeekCmd{SeekPos: pos, Key: key, SoftSeek: softSeek}
|
|
}
|
|
|
|
func (p *Parser) parseReplace() *ast.ReplaceCmd {
|
|
pos := p.expect(token.REPLACE).Pos
|
|
var fields []ast.ReplaceField
|
|
for {
|
|
field := p.parseExpr()
|
|
p.expect(token.WITH)
|
|
value := p.parseExpr()
|
|
fields = append(fields, ast.ReplaceField{Field: field, Value: value})
|
|
if !p.match(token.COMMA) {
|
|
break
|
|
}
|
|
}
|
|
p.expectEndOfStmt()
|
|
return &ast.ReplaceCmd{ReplacePos: pos, Fields: fields}
|
|
}
|
|
|
|
func (p *Parser) parseAppend() *ast.AppendCmd {
|
|
pos := p.expect(token.APPEND).Pos
|
|
if p.match(token.FROM) {
|
|
// APPEND FROM filename [DELIMITED|SDF|VIA ...] — skip to EOL
|
|
p.skipToEndOfLine()
|
|
p.expectEndOfStmt()
|
|
return &ast.AppendCmd{AppendPos: pos}
|
|
}
|
|
p.expect(token.BLANK)
|
|
p.expectEndOfStmt()
|
|
return &ast.AppendCmd{AppendPos: pos}
|
|
}
|
|
|
|
func (p *Parser) parseIndex() *ast.IndexCmd {
|
|
pos := p.expect(token.INDEX).Pos
|
|
p.expect(token.ON)
|
|
keyExpr := p.parseExpr()
|
|
|
|
// INDEX ON expr [TAG tagname] TO file [FOR cond] [UNIQUE] [DESCENDING]
|
|
var fileExpr ast.Expr
|
|
var tagName string
|
|
if p.match(token.TO) {
|
|
fileExpr = p.parseExpr()
|
|
p.consumeFileExtension(fileExpr)
|
|
} else if p.current.Kind == token.IDENT && p.currentUpper() == "TAG" {
|
|
p.advance() // skip TAG
|
|
tagName = p.expectMethodName().Literal // capture tag name
|
|
if p.match(token.TO) {
|
|
fileExpr = p.parseExpr()
|
|
} else {
|
|
// TAG without TO: use tag name as file name
|
|
fileExpr = &ast.IdentExpr{NamePos: p.current.Pos, Name: tagName}
|
|
}
|
|
} else {
|
|
fileExpr = p.parseExpr() // fallback
|
|
}
|
|
|
|
var forCond ast.Expr
|
|
unique := false
|
|
descending := false
|
|
|
|
for {
|
|
if p.match(token.FOR) {
|
|
forCond = p.parseExpr()
|
|
} else if p.match(token.UNIQUE) {
|
|
unique = true
|
|
} else if p.match(token.DESCENDING) {
|
|
descending = true
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
p.expectEndOfStmt()
|
|
|
|
return &ast.IndexCmd{
|
|
IndexPos: pos, KeyExpr: keyExpr, File: fileExpr,
|
|
ForCond: forCond, TagName: tagName, Unique: unique, Descending: descending,
|
|
}
|
|
}
|
|
|
|
func (p *Parser) parseSet() *ast.SetCmd {
|
|
pos := p.expect(token.SET).Pos
|
|
|
|
// Accept any token as SET keyword (COLOR, KEY, ORDER, FILTER, etc. may be keyword tokens)
|
|
setting := p.expectMethodName().Literal
|
|
|
|
var expr ast.Expr
|
|
var extra string
|
|
|
|
// SET commands: consume everything until end of line.
|
|
// Boolean toggles: SET DELETED ON/OFF, SET EXACT ON/OFF, etc.
|
|
// Value settings: SET FILTER TO expr, SET ORDER TO n, SET DATE TO fmt
|
|
upperSetting := strings.ToUpper(setting)
|
|
|
|
// Check for ON/OFF boolean toggle
|
|
booleanSets := map[string]bool{
|
|
"DELETED": true, "EXACT": true, "SOFTSEEK": true, "EXCLUSIVE": true,
|
|
"FIXED": true, "CANCEL": true, "BELL": true, "CONFIRM": true,
|
|
"INSERT": true, "ESCAPE": true, "WRAP": true, "INTENSITY": true,
|
|
"SCOREBOARD": true, "CONSOLE": true, "ALTERNATE": true, "PRINTER": true,
|
|
}
|
|
|
|
if booleanSets[upperSetting] {
|
|
if p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
|
|
extra = strings.ToUpper(p.expectMethodName().Literal)
|
|
}
|
|
} else if p.match(token.TO) {
|
|
if upperSetting == "FILTER" || upperSetting == "RELATION" || upperSetting == "ORDER" || upperSetting == "INDEX" {
|
|
if p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
|
|
expr = p.parseExpr()
|
|
// SET INDEX TO a, b, c — collect comma-separated file names
|
|
// into a single string literal "a,b,c" for gengo to split.
|
|
if upperSetting == "INDEX" {
|
|
getName := func(e ast.Expr) string {
|
|
switch v := e.(type) {
|
|
case *ast.IdentExpr:
|
|
return v.Name
|
|
case *ast.LiteralExpr:
|
|
return v.Value
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
combined := getName(expr)
|
|
for p.current.Kind == token.COMMA {
|
|
p.advance()
|
|
if p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
|
|
next := p.parseExpr()
|
|
combined += "," + getName(next)
|
|
}
|
|
}
|
|
if strings.Contains(combined, ",") {
|
|
expr = &ast.LiteralExpr{
|
|
Value: combined,
|
|
Kind: token.STRING,
|
|
ValuePos: expr.Pos(),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if p.current.Kind == token.INTO {
|
|
p.advance()
|
|
extra = p.expectMethodName().Literal
|
|
}
|
|
} else if upperSetting == "DATE" || upperSetting == "DECIMALS" || upperSetting == "EPOCH" {
|
|
if p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
|
|
expr = p.parseExpr()
|
|
}
|
|
}
|
|
}
|
|
|
|
// Consume remaining tokens (for COLOR TO, KEY TO, etc.)
|
|
for p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
|
|
p.advance()
|
|
}
|
|
p.expectEndOfStmt()
|
|
return &ast.SetCmd{SetPos: pos, Setting: setting, Expr: expr, Extra: extra}
|
|
}
|
|
|
|
// parseAtCmd parses @ row, col SAY/GET/PROMPT commands.
|
|
func (p *Parser) parseAtCmd() ast.Stmt {
|
|
pos := p.advance().Pos // consume @
|
|
row := p.parseExpr()
|
|
p.expect(token.COMMA)
|
|
col := p.parseExpr()
|
|
|
|
// Determine sub-command: SAY, GET, PROMPT
|
|
if p.current.Kind == token.IDENT {
|
|
switch p.currentUpper() {
|
|
case "SAY":
|
|
return p.parseAtSay(pos, row, col)
|
|
case "GET":
|
|
return p.parseAtGet(pos, row, col)
|
|
case "PROMPT":
|
|
return p.parseAtPrompt(pos, row, col)
|
|
case "CLEAR":
|
|
// @ row, col CLEAR [TO row2, col2] — clear region
|
|
p.advance() // skip CLEAR
|
|
if p.match(token.TO) {
|
|
p.parseExpr() // row2
|
|
p.expect(token.COMMA)
|
|
p.parseExpr() // col2
|
|
}
|
|
p.expectEndOfStmt()
|
|
return &ast.ExprStmt{X: &ast.CallExpr{
|
|
Func: &ast.IdentExpr{Name: "SetPos"},
|
|
Args: []ast.Expr{row, col},
|
|
}}
|
|
}
|
|
}
|
|
|
|
// @ row, col TO row2, col2 [DOUBLE] — box drawing
|
|
if p.match(token.TO) {
|
|
row2 := p.parseExpr()
|
|
p.expect(token.COMMA)
|
|
col2 := p.parseExpr()
|
|
// Skip optional DOUBLE keyword or other modifiers
|
|
for p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
|
|
p.advance()
|
|
}
|
|
p.expectEndOfStmt()
|
|
return &ast.ExprStmt{X: &ast.CallExpr{
|
|
Func: &ast.IdentExpr{Name: "DispBox"},
|
|
Args: []ast.Expr{row, col, row2, col2},
|
|
}}
|
|
}
|
|
|
|
// Bare @ row, col — just position cursor
|
|
for p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
|
|
p.advance() // skip any remaining tokens
|
|
}
|
|
p.expectEndOfStmt()
|
|
return &ast.ExprStmt{X: &ast.CallExpr{
|
|
Func: &ast.IdentExpr{Name: "SetPos"},
|
|
Args: []ast.Expr{row, col},
|
|
}}
|
|
}
|
|
|
|
func (p *Parser) parseAtSay(pos token.Position, row, col ast.Expr) ast.Stmt {
|
|
p.advance() // consume SAY
|
|
sayExpr := p.parseExpr()
|
|
|
|
// Check for GET after SAY
|
|
if p.current.Kind == token.IDENT && p.currentUpper() == "GET" {
|
|
return p.parseAtSayGet(pos, row, col, sayExpr)
|
|
}
|
|
|
|
// PICTURE clause
|
|
var pic ast.Expr
|
|
if p.current.Kind == token.IDENT && p.currentUpper() == "PICTURE" {
|
|
p.advance()
|
|
pic = p.parseExpr()
|
|
}
|
|
p.expectEndOfStmt()
|
|
return &ast.AtSayCmd{AtPos: pos, Row: row, Col: col, SayExpr: sayExpr, Picture: pic}
|
|
}
|
|
|
|
func (p *Parser) parseAtGet(pos token.Position, row, col ast.Expr) *ast.AtGetCmd {
|
|
p.advance() // consume GET
|
|
varExpr := p.parseExpr()
|
|
varName := ""
|
|
if ident, ok := varExpr.(*ast.IdentExpr); ok {
|
|
varName = ident.Name
|
|
}
|
|
|
|
var pic, valid, when ast.Expr
|
|
for p.current.Kind == token.IDENT {
|
|
switch p.currentUpper() {
|
|
case "PICTURE":
|
|
p.advance()
|
|
pic = p.parseExpr()
|
|
case "VALID":
|
|
p.advance()
|
|
valid = p.parseExpr()
|
|
case "WHEN":
|
|
p.advance()
|
|
when = p.parseExpr()
|
|
case "RANGE":
|
|
// RANGE low, high — skip both values
|
|
p.advance()
|
|
p.parseExpr() // low
|
|
p.expect(token.COMMA)
|
|
p.parseExpr() // high
|
|
case "COLOR", "COLOUR", "MESSAGE", "SEND", "GUISEND",
|
|
"CAPTION", "CARGO", "COLORSPEC":
|
|
// Skip keyword + value
|
|
p.advance()
|
|
p.parseExpr()
|
|
default:
|
|
goto done
|
|
}
|
|
}
|
|
done:
|
|
p.expectEndOfStmt()
|
|
return &ast.AtGetCmd{AtPos: pos, Row: row, Col: col, Var: varExpr, VarName: varName, Picture: pic, Valid: valid, When: when}
|
|
}
|
|
|
|
func (p *Parser) parseAtSayGet(pos token.Position, row, col ast.Expr, sayExpr ast.Expr) *ast.AtSayGetCmd {
|
|
p.advance() // consume GET
|
|
varExpr := p.parseExpr()
|
|
varName := ""
|
|
if ident, ok := varExpr.(*ast.IdentExpr); ok {
|
|
varName = ident.Name
|
|
}
|
|
|
|
var pic, valid, when ast.Expr
|
|
for p.current.Kind == token.IDENT {
|
|
switch p.currentUpper() {
|
|
case "PICTURE":
|
|
p.advance()
|
|
pic = p.parseExpr()
|
|
case "VALID":
|
|
p.advance()
|
|
valid = p.parseExpr()
|
|
case "WHEN":
|
|
p.advance()
|
|
when = p.parseExpr()
|
|
case "RANGE":
|
|
p.advance()
|
|
p.parseExpr()
|
|
p.expect(token.COMMA)
|
|
p.parseExpr()
|
|
case "COLOR", "COLOUR", "MESSAGE", "SEND", "GUISEND",
|
|
"CAPTION", "CARGO", "COLORSPEC":
|
|
p.advance()
|
|
p.parseExpr()
|
|
default:
|
|
goto done
|
|
}
|
|
}
|
|
done:
|
|
p.expectEndOfStmt()
|
|
return &ast.AtSayGetCmd{AtPos: pos, Row: row, Col: col, SayExpr: sayExpr, Var: varExpr, VarName: varName, Picture: pic, Valid: valid, When: when}
|
|
}
|
|
|
|
func (p *Parser) parseAtPrompt(pos token.Position, row, col ast.Expr) ast.Stmt {
|
|
p.advance() // consume PROMPT
|
|
prompt := p.parseExpr()
|
|
var msg ast.Expr
|
|
if p.current.Kind == token.IDENT && p.currentUpper() == "MESSAGE" {
|
|
p.advance()
|
|
msg = p.parseExpr()
|
|
}
|
|
p.expectEndOfStmt()
|
|
// Emit as: __AtPrompt(row, col, prompt [, msg])
|
|
args := []ast.Expr{row, col, prompt}
|
|
if msg != nil {
|
|
args = append(args, msg)
|
|
}
|
|
return &ast.ExprStmt{X: &ast.CallExpr{
|
|
Func: &ast.IdentExpr{Name: "__AtPrompt"},
|
|
Args: args,
|
|
}}
|
|
}
|
|
|
|
// === Five Go Extension Parsers ===
|
|
|
|
// looksLikeMultiAssign checks strictly: IDENT , IDENT [, IDENT...] :=
|
|
// Each token between start and := must be IDENT or COMMA only (no [ ( : etc.)
|
|
func (p *Parser) looksLikeMultiAssign() bool {
|
|
// Must start with IDENT (or keyword-as-ident)
|
|
if p.current.Kind != token.IDENT && p.current.Literal == "" {
|
|
return false
|
|
}
|
|
// Next token must be COMMA for multi-assign
|
|
if p.pos+1 >= len(p.tokens) || p.peekAt(1) != token.COMMA {
|
|
return false
|
|
}
|
|
// Scan from after first IDENT: COMMA, IDENT, COMMA, IDENT, ..., ASSIGN
|
|
expectComma := true // first IDENT already consumed, expect COMMA next
|
|
for i := p.pos + 1; i < len(p.tokens); i++ {
|
|
tk := p.tokens[i]
|
|
if tk.Kind == token.ASSIGN {
|
|
return expectComma == true // last was IDENT (expectComma=true), then :=
|
|
}
|
|
if tk.Kind == token.NEWLINE || tk.Kind == token.EOF {
|
|
return false
|
|
}
|
|
if expectComma {
|
|
if tk.Kind != token.COMMA {
|
|
return false
|
|
}
|
|
expectComma = false
|
|
} else {
|
|
// Expect IDENT or keyword-as-ident (including "_")
|
|
if tk.Kind != token.IDENT && tk.Literal == "" {
|
|
return false
|
|
}
|
|
expectComma = true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// parseMultiAssign: a, b := Func() or a, b, c := x, y, z or _, b := Func()
|
|
func (p *Parser) parseMultiAssign() *ast.MultiAssignStmt {
|
|
pos := p.current.Pos
|
|
var targets []string
|
|
|
|
// Parse target list: a, b, c or _, b
|
|
for {
|
|
name := "_"
|
|
if p.current.Kind == token.IDENT && p.current.Literal == "_" {
|
|
p.advance()
|
|
} else {
|
|
tok := p.expectMethodName()
|
|
name = tok.Literal
|
|
}
|
|
targets = append(targets, name)
|
|
if !p.match(token.COMMA) {
|
|
break
|
|
}
|
|
}
|
|
|
|
p.expect(token.ASSIGN) // :=
|
|
|
|
// Parse value list
|
|
var values []ast.Expr
|
|
values = append(values, p.parseExpr())
|
|
for p.match(token.COMMA) {
|
|
values = append(values, p.parseExpr())
|
|
}
|
|
p.expectEndOfStmt()
|
|
|
|
return &ast.MultiAssignStmt{AssignPos: pos, Targets: targets, Values: values}
|
|
}
|
|
|
|
// parseDefer: DEFER expr
|
|
func (p *Parser) parseDefer() *ast.DeferStmt {
|
|
pos := p.expect(token.DEFER_KW).Pos
|
|
// Parse the expression to defer (typically a method call: db:Close())
|
|
call := p.parseExpr()
|
|
p.expectEndOfStmt()
|
|
return &ast.DeferStmt{DeferPos: pos, Call: call}
|
|
}
|
|
|
|
// parseConstBlock: CONST ... END CONST
|
|
func (p *Parser) parseConstBlock() ast.Stmt {
|
|
pos := p.expect(token.CONST_KW).Pos
|
|
p.expectEndOfStmt()
|
|
|
|
var items []ast.ConstItem
|
|
p.skipNewlines()
|
|
|
|
for !p.atAny(token.END, token.ENDCASE, token.EOF) {
|
|
if p.current.Kind == token.IDENT || p.current.Literal != "" {
|
|
name := p.expectMethodName().Literal
|
|
var val ast.Expr
|
|
if p.match(token.ASSIGN) {
|
|
val = p.parseExpr()
|
|
}
|
|
items = append(items, ast.ConstItem{Name: name, Value: val})
|
|
p.expectEndOfStmt()
|
|
} else {
|
|
break
|
|
}
|
|
p.skipNewlines()
|
|
}
|
|
|
|
if p.match(token.END) {
|
|
// Skip optional CONST after END
|
|
if p.current.Kind == token.CONST_KW {
|
|
p.advance()
|
|
}
|
|
}
|
|
p.expectEndOfStmt()
|
|
|
|
_ = pos
|
|
return &ast.ExprStmt{X: &ast.LiteralExpr{Kind: token.NIL_LIT, Value: "NIL"}}
|
|
}
|
|
|
|
// parseShortIfAssign: IF var := expr ; condition ... ENDIF
|
|
// Detected inside parseIf when IF is followed by IDENT := expr ;
|
|
// === Five Concurrency Parsers ===
|
|
|
|
// parseWatch: WATCH / CASE msg := <- ch / CASE ch <- val / OTHERWISE / ENDWATCH
|
|
func (p *Parser) parseWatch() *ast.WatchStmt {
|
|
watchPos := p.expect(token.WATCH_KW).Pos
|
|
p.expectEndOfStmt()
|
|
|
|
var cases []*ast.WatchCase
|
|
var otherwise []ast.Stmt
|
|
p.skipNewlines()
|
|
|
|
for p.current.Kind == token.CASE {
|
|
casePos := p.advance().Pos
|
|
wc := &ast.WatchCase{CasePos: casePos}
|
|
|
|
if p.at(token.ARROW_LEFT) {
|
|
// CASE <- ch (receive, discard value)
|
|
p.advance() // consume <-
|
|
wc.RecvChan = p.parseExpr()
|
|
} else {
|
|
// CASE var := <- ch OR CASE ch <- val
|
|
first := p.parseExpr()
|
|
if p.at(token.ASSIGN) && p.peekAt(1) == token.ARROW_LEFT {
|
|
// CASE var := <- ch
|
|
p.advance() // consume :=
|
|
p.advance() // consume <-
|
|
wc.RecvChan = p.parseExpr()
|
|
if ident, ok := first.(*ast.IdentExpr); ok {
|
|
wc.RecvVar = ident.Name
|
|
}
|
|
} else if p.at(token.ARROW_LEFT) {
|
|
// CASE ch <- val (send)
|
|
p.advance() // consume <-
|
|
wc.SendChan = first
|
|
wc.SendVal = p.parseExpr()
|
|
} else {
|
|
// CASE expr (boolean guard — less common)
|
|
wc.RecvChan = first
|
|
}
|
|
}
|
|
p.expectEndOfStmt()
|
|
wc.Body = p.parseStmtBlock(token.CASE, token.OTHERWISE, token.END)
|
|
cases = append(cases, wc)
|
|
}
|
|
|
|
if p.match(token.OTHERWISE) {
|
|
p.expectEndOfStmt()
|
|
otherwise = p.parseStmtBlock(token.END)
|
|
}
|
|
|
|
endPos := p.current.Pos
|
|
p.match(token.END)
|
|
// Skip optional WATCH after END
|
|
if p.current.Kind == token.WATCH_KW {
|
|
p.advance()
|
|
}
|
|
p.expectEndOfStmt()
|
|
|
|
return &ast.WatchStmt{WatchPos: watchPos, Cases: cases, Otherwise: otherwise, EndPos: endPos}
|
|
}
|
|
|
|
// parseParallelFor: PARALLEL FOR i := 1 TO n / body / NEXT
|
|
func (p *Parser) parseParallelFor() ast.Stmt {
|
|
p.expect(token.PARALLEL_KW)
|
|
|
|
if p.current.Kind != token.FOR {
|
|
// Not PARALLEL FOR — skip
|
|
p.skipToEndOfLine()
|
|
p.expectEndOfStmt()
|
|
return &ast.ExprStmt{X: &ast.LiteralExpr{Kind: token.NIL_LIT, Value: "NIL"}}
|
|
}
|
|
|
|
forPos := p.expect(token.FOR).Pos
|
|
varTok := p.expectMethodName()
|
|
varName := varTok.Literal
|
|
if p.at(token.ARROW) {
|
|
p.advance()
|
|
fieldTok := p.expectMethodName()
|
|
varName = fieldTok.Literal
|
|
}
|
|
p.expect(token.ASSIGN)
|
|
start := p.parseExpr()
|
|
p.expect(token.TO)
|
|
toExpr := p.parseExpr()
|
|
|
|
var step ast.Expr
|
|
if p.match(token.STEP) {
|
|
step = p.parseExpr()
|
|
}
|
|
p.expectEndOfStmt()
|
|
|
|
body := p.parseStmtBlock(token.NEXT, token.END)
|
|
endPos := p.current.Pos
|
|
if !p.match(token.NEXT) {
|
|
p.match(token.END)
|
|
}
|
|
if p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF {
|
|
p.skipToEndOfLine()
|
|
}
|
|
p.expectEndOfStmt()
|
|
|
|
return &ast.ParallelForStmt{
|
|
ForPos: forPos, Var: varName, Start: start, To: toExpr,
|
|
Step: step, Body: body, EndPos: endPos,
|
|
}
|
|
}
|
|
|
|
// parseWithTimeout: WITH TIMEOUT n / body / ENDWITH
|
|
func (p *Parser) parseWithTimeout() *ast.TimeoutStmt {
|
|
withPos := p.advance().Pos // consume WITH
|
|
p.expect(token.TIMEOUT_KW) // consume TIMEOUT
|
|
duration := p.parseExpr()
|
|
p.expectEndOfStmt()
|
|
|
|
body := p.parseStmtBlock(token.END)
|
|
endPos := p.current.Pos
|
|
p.match(token.END)
|
|
// Skip optional WITH after END
|
|
if p.current.Kind == token.IDENT && p.currentUpper() == "WITH" {
|
|
p.advance()
|
|
}
|
|
p.expectEndOfStmt()
|
|
|
|
return &ast.TimeoutStmt{WithPos: withPos, Duration: duration, Body: body, EndPos: endPos}
|
|
}
|
|
|
|
func (p *Parser) isShortIfAssign() bool {
|
|
// Look ahead: IF ident := expr ; condition
|
|
if p.pos+3 >= len(p.tokens) {
|
|
return false
|
|
}
|
|
// Check pattern: IDENT := ... ;
|
|
for i := p.pos + 1; i < len(p.tokens); i++ {
|
|
if p.tokens[i].Kind == token.SEMICOLON {
|
|
// Found ; before newline — it's short if
|
|
return true
|
|
}
|
|
if p.tokens[i].Kind == token.NEWLINE || p.tokens[i].Kind == token.EOF {
|
|
return false
|
|
}
|
|
}
|
|
return false
|
|
}
|