Five v0.9 — Harbour + Go fusion language

- Compiler: PP → Lexer → Parser → Analyzer → Gengo pipeline
- Parser: 232/236 (98%) Harbour compatibility, registry-based dispatch
- RTL: 351 Harbour-compatible functions
- RDD: DBF/NTX/CDX engines with Rushmore bitmap optimization
- Go Interop: IMPORT + pkg.Func() + obj:Method() with FastPath (15M calls/sec)
- HB_FUNC API: Full Harbour C API compatible Go bridge
- Concurrency: SPAWN/LAUNCH/GOROUTINE, <-, WATCH, PARALLEL FOR, ASYNC/AWAIT
- Extensions: Multi-return, DEFER, Slice, f-string, Nil-safe ?:, CONST
- Macro Compiler: Runtime AST parsing and evaluation
- Debugger: TUI debugger with source display, breakpoints, stepping
- FRB: Native + Pcode dual mode runtime binary
- Tests: 13 packages ALL PASS

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-31 09:41:50 +09:00
commit 59568f3301
282 changed files with 66658 additions and 0 deletions

760
compiler/parser/expr.go Normal file
View File

@@ -0,0 +1,760 @@
// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
// All rights reserved.
// Expression parsing using Pratt parser (precedence climbing).
//
// Harbour's operator precedence from harbour.y:
// POST < ASSIGN(right) < OR(right) < AND(right) < NOT(right) <
// COMPARE(right) < ADD < MUL < POWER < UNARY < PRE < ALIAS/MACRO
//
// Key Harbour quirks:
// - '=' is BOTH assignment (in statement context) and equality (in expression)
// - Most operators are right-associative (unlike C)
// - (expr)->field for dynamic alias
// - &variable for macro
package parser
import (
"five/compiler/ast"
"five/compiler/token"
"strings"
)
// parseExpr parses an expression using Pratt parsing.
func (p *Parser) parseExpr() ast.Expr {
return p.parseBinaryExpr(token.PrecAssign)
}
// parseBinaryExpr parses binary expressions with precedence climbing.
// tsgo pattern: GetBinaryOperatorPrecedence (ref/typescript-go/internal/ast/precedence.go:338)
func (p *Parser) parseBinaryExpr(minPrec token.Precedence) ast.Expr {
left := p.parseUnaryExpr()
for {
prec := token.GetBinaryPrecedence(p.current.Kind)
if prec < minPrec {
break
}
op := p.advance()
// Right-associative: use same precedence for right side
// Left-associative: use precedence+1 for right side
nextPrec := prec + 1
if token.IsRightAssociative(op.Kind) {
nextPrec = prec
}
right := p.parseBinaryExpr(nextPrec)
// Assignment operators → AssignExpr
if isAssignOp(op.Kind) {
left = &ast.AssignExpr{
Left: left, OpPos: op.Pos, Op: op.Kind, Right: right,
}
} else {
left = &ast.BinaryExpr{
Left: left, OpPos: op.Pos, Op: op.Kind, Right: right,
}
}
}
return left
}
func isAssignOp(k token.Kind) bool {
switch k {
case token.ASSIGN, token.PLUSEQ, token.MINUSEQ,
token.STAREQ, token.SLASHEQ, token.PERCENTEQ, token.POWEREQ:
return true
}
return false
}
// parseUnaryExpr parses prefix unary expressions.
func (p *Parser) parseUnaryExpr() ast.Expr {
switch p.current.Kind {
case token.MINUS:
op := p.advance()
x := p.parseUnaryExpr()
return &ast.UnaryExpr{OpPos: op.Pos, Op: token.MINUS, X: x}
case token.PLUS:
p.advance() // unary plus — no-op, just parse the operand
return p.parseUnaryExpr()
case token.NOT:
op := p.advance()
x := p.parseUnaryExpr()
return &ast.UnaryExpr{OpPos: op.Pos, Op: token.NOT, X: x}
case token.INC:
op := p.advance()
x := p.parseUnaryExpr()
return &ast.UnaryExpr{OpPos: op.Pos, Op: token.INC, X: x}
case token.DEC:
op := p.advance()
x := p.parseUnaryExpr()
return &ast.UnaryExpr{OpPos: op.Pos, Op: token.DEC, X: x}
case token.AT:
op := p.advance()
x := p.parseUnaryExpr()
return &ast.RefExpr{AtPos: op.Pos, X: x}
case token.ARROW_LEFT:
// <- ch (channel receive as expression)
pos := p.advance().Pos
ch := p.parsePostfixExpr()
return &ast.ChanRecvExpr{ArrowPos: pos, Chan: ch}
case token.ASYNC_KW:
// ASYNC expr — launch async, return future
pos := p.advance().Pos
call := p.parsePostfixExpr()
return &ast.AsyncExpr{AsyncPos: pos, Call: call}
case token.AWAIT_KW:
// AWAIT future — wait for result
pos := p.advance().Pos
future := p.parsePostfixExpr()
return &ast.AwaitExpr{AwaitPos: pos, Future: future}
default:
return p.parsePostfixExpr()
}
}
// parsePostfixExpr parses postfix operations: function calls, method sends,
// array indexing, postfix ++/--, and alias-> access.
func (p *Parser) parsePostfixExpr() ast.Expr {
x := p.parsePrimaryExpr()
for {
switch p.current.Kind {
case token.LPAREN:
// Function call: x(args...)
lp := p.advance().Pos
var args []ast.Expr
if !p.at(token.RPAREN) {
args = p.parseExprList()
}
rp := p.expect(token.RPAREN).Pos
x = &ast.CallExpr{Func: x, LParen: lp, Args: args, RParen: rp}
case token.LBRACKET:
// Array index: x[index], multi-dim x[i, j], or slice x[low:high]
lb := p.advance().Pos
// Check for slice syntax: x[:high], x[low:high], x[low:]
// Detect by scanning ahead for : before ]
if p.isSliceSyntax() {
var low, high ast.Expr
if !p.at(token.COLON) {
low = p.parseSliceIndex()
}
p.expect(token.COLON)
if !p.at(token.RBRACKET) {
high = p.parseSliceIndex()
}
rb := p.expect(token.RBRACKET).Pos
x = &ast.SliceExpr{X: x, LBracket: lb, Low: low, High: high, RBracket: rb}
continue
}
// Normal array index
index := p.parseExpr()
rb := token.Position{}
// Multi-dimensional: a[3, 2] → a[3][2]
for p.match(token.COMMA) {
rb = p.current.Pos
x = &ast.IndexExpr{X: x, LBracket: lb, Index: index, RBracket: rb}
index = p.parseExpr()
lb = rb
}
rb = p.expect(token.RBRACKET).Pos
x = &ast.IndexExpr{X: x, LBracket: lb, Index: index, RBracket: rb}
case token.QMARK:
// Nil-safe send: x?:method or x?:method(args...)
if p.peekAt(1) == token.COLON {
p.advance() // consume ?
qpos := p.advance().Pos // consume :
methodName := p.expectMethodName().Literal
var args []ast.Expr
hasParens := false
if p.at(token.LPAREN) {
hasParens = true
p.advance()
if !p.at(token.RPAREN) {
args = p.parseExprList()
}
p.expect(token.RPAREN)
}
x = &ast.NilSafeExpr{X: x, QPos: qpos, Method: methodName, Args: args, HasParens: hasParens}
} else {
return x // bare ? is QOut, not postfix
}
case token.COLON:
// Method send: x:method or x:method(args...)
colonPos := p.advance().Pos
var methodName string
var macroMethod ast.Expr
if p.current.Kind == token.AMPERSAND {
// x:&macro — dynamic method
macroMethod = p.parseMacro()
} else {
// Accept keywords as method names (end, delete, home, etc.)
methodName = p.expectMethodName().Literal
}
// Check for call: x:method(args...)
var args []ast.Expr
var lp, rp token.Position
hasParens := false
if p.at(token.LPAREN) {
hasParens = true
lp = p.advance().Pos
if !p.at(token.RPAREN) {
args = p.parseExprList()
}
rp = p.expect(token.RPAREN).Pos
}
x = &ast.SendExpr{
Object: x, ColonPos: colonPos,
Method: methodName, MacroMethod: macroMethod,
HasParens: hasParens,
LParen: lp, Args: args, RParen: rp,
}
case token.ARROW:
// Alias access: x->field or (expr)->field
arrowPos := p.advance().Pos
field := p.parsePrimaryExpr()
x = &ast.AliasExpr{Alias: x, ArrowPos: arrowPos, Field: field}
case token.INC:
// Postfix increment: x++
opPos := p.advance().Pos
x = &ast.PostfixExpr{X: x, OpPos: opPos, Op: token.INC}
case token.DEC:
// Postfix decrement: x--
opPos := p.advance().Pos
x = &ast.PostfixExpr{X: x, OpPos: opPos, Op: token.DEC}
case token.COLONCOLON:
// ::name — Self access (consumed as postfix of implicit Self)
// This shouldn't happen here normally; :: is handled in primary
return x
case token.DOT:
// Package member access: pkg.Func or obj.Field
// Accept any token with literal (keywords like Index, Count, etc.)
if p.peekLitAt(1) != "" {
dotPos := p.advance().Pos // consume .
member := p.advance() // consume member name
x = &ast.DotExpr{X: x, DotPos: dotPos, Member: member.Literal}
} else {
return x
}
default:
return x
}
}
}
// parsePrimaryExpr parses primary expressions (atoms).
func (p *Parser) parsePrimaryExpr() ast.Expr {
switch p.current.Kind {
case token.INT, token.LONG, token.DOUBLE, token.STRING,
token.DATE_LIT, token.TRUE, token.FALSE, token.NIL_LIT:
tok := p.advance()
return &ast.LiteralExpr{ValuePos: tok.Pos, Kind: tok.Kind, Value: tok.Literal}
case token.COLONCOLON:
// ::name or ::name() or ::name(args)
pos := p.advance().Pos
if p.at(token.IDENT) || p.current.Literal != "" {
name := p.advance()
self := &ast.SelfExpr{ColonPos: pos}
// Check for () — method call
hasParens := false
var args []ast.Expr
var lp, rp token.Position
if p.at(token.LPAREN) {
hasParens = true
lp = p.advance().Pos
if !p.at(token.RPAREN) {
args = p.parseExprList()
}
rp = p.expect(token.RPAREN).Pos
}
return &ast.SendExpr{
Object: self, ColonPos: pos,
Method: name.Literal,
HasParens: hasParens,
LParen: lp, Args: args, RParen: rp,
}
}
return &ast.SelfExpr{ColonPos: pos}
case token.LPAREN:
// Parenthesized expression, comma sequence (a,b,c), or (alias)->field
p.advance()
expr := p.parseExpr()
// Comma sequence: (expr1, expr2, ...) → evaluates all, returns last
for p.match(token.COMMA) {
expr = p.parseExpr()
}
p.expect(token.RPAREN)
return expr
case token.IF:
// if(cond, true, false) — inline IF = IIF
if p.peekAt(1) == token.LPAREN {
return p.parseIIF()
}
// Otherwise fall through to error
p.error("expected expression, got IF")
tok := p.advance()
return &ast.LiteralExpr{ValuePos: tok.Pos, Kind: token.NIL_LIT, Value: "NIL"}
case token.IDENT:
// Check for IIF(cond, true, false)
if strings.ToUpper(p.current.Literal) == "IIF" {
return p.parseIIF()
}
// f"Hello {name}" — string interpolation
if p.current.Literal == "f" && p.peekAt(1) == token.STRING {
return p.parseInterpolatedString()
}
tok := p.advance()
return &ast.IdentExpr{NamePos: tok.Pos, Name: tok.Literal}
case token.AMPERSAND:
return p.parseMacro()
case token.COLON:
// :field — WITH OBJECT send (bare colon prefix)
// Treat as self-send: withObj:field
pos := p.advance().Pos // consume :
if p.at(token.IDENT) || p.current.Literal != "" {
name := p.advance()
return &ast.SendExpr{
Object: &ast.IdentExpr{NamePos: pos, Name: "__withObject"},
ColonPos: pos,
Method: name.Literal,
}
}
return &ast.IdentExpr{NamePos: pos, Name: "__withObject"}
case token.LBRACE:
return p.parseArrayOrBlock()
default:
// Keywords used as identifiers in expression context:
// 1. Followed by ( → function call: Set(), Type(), Select()
// 2. Keywords that can appear as variable/field names: TO, DATA, FIELD, ON, etc.
if p.current.Literal != "" {
if p.peekAt(1) == token.LPAREN {
tok := p.advance()
return &ast.IdentExpr{NamePos: tok.Pos, Name: tok.Literal}
}
// Allow certain keywords as bare identifiers in expression context
switch p.current.Kind {
case token.TO, token.DATA, token.FIELD, token.IN, token.FROM,
token.WHILE, token.EACH, token.ENDDO, token.END, token.NEXT,
token.RECOVER, token.SEQUENCE, token.GO, token.GOTO,
token.MEMVAR, token.ALIAS, token.WITH, token.ON,
token.STEP, token.DESCENDING, token.UNIQUE,
token.DELETE_KW, token.RECALL, token.PACK, token.ZAP,
token.TYPE_KW, token.CLASS, token.DECLARE, token.INLINE_KW,
token.CASE, token.OTHERWISE, token.ENDCASE, token.BEGIN,
token.DO, token.ENDIF, token.FOR, token.IF,
token.SWITCH, token.RETURN, token.EXIT, token.LOOP,
token.LOCAL, token.PRIVATE, token.PUBLIC,
token.STATIC, token.PARAMETERS, token.DESTRUCTOR,
token.CONSTRUCTOR, token.OPERATOR_KW,
token.FUNCTION_KW, token.PROCEDURE, token.METHOD,
token.ELSEIF, token.ELSE, token.ENDCLASS,
token.USING, token.ASSIGN_KW, token.ACCESS,
token.APPEND, token.REPLACE, token.INDEX,
token.SEEK, token.SKIP_KW, token.USE,
token.SELECT, token.SET:
tok := p.advance()
return &ast.IdentExpr{NamePos: tok.Pos, Name: tok.Literal}
}
}
p.error("expected expression, got " + p.current.Kind.String() + " " + p.current.Literal)
tok := p.advance()
return &ast.LiteralExpr{ValuePos: tok.Pos, Kind: token.NIL_LIT, Value: "NIL"}
}
}
// parseArrayOrBlock parses { ... } which can be:
// {1, 2, 3} → ArrayLitExpr
// {"a" => 1} → HashLitExpr
// {|x| x + 1} → BlockExpr
// {|| expr} → BlockExpr (no params)
func (p *Parser) parseArrayOrBlock() ast.Expr {
lbrace := p.expect(token.LBRACE).Pos
// Code block: {|params| body}
if p.at(token.PIPE) {
p.advance() // consume first |
var params []string
if !p.at(token.PIPE) {
// Parse parameter names, with optional AS type
for {
params = append(params, p.expectMethodName().Literal)
// Skip optional AS type: AS NUMERIC, AS STRING, etc.
if p.match(token.AS) {
for p.current.Kind != token.PIPE && p.current.Kind != token.COMMA &&
p.current.Kind != token.EOF {
p.advance()
}
}
if !p.match(token.COMMA) {
break
}
}
}
p.expect(token.PIPE) // closing |
// Parse block body — may have comma-separated expressions
// {|x| expr1, expr2} → comma = sequence, returns last value
body := p.parseExpr()
for p.match(token.COMMA) {
// Comma-separated: wrap as sequence, keep last
body = p.parseExpr()
}
rbrace := p.expect(token.RBRACE).Pos
return &ast.BlockExpr{LBrace: lbrace, Params: params, Body: body, RBrace: rbrace}
}
// Empty: {} → empty array
if p.at(token.RBRACE) {
rbrace := p.advance().Pos
return &ast.ArrayLitExpr{LBrace: lbrace, RBrace: rbrace}
}
// { ... } → variadic params array (HB_PARAM_ALL())
if p.at(token.DOT) && p.peekAt(1) == token.DOT && p.peekAt(2) == token.DOT {
p.advance() // .
p.advance() // .
p.advance() // .
rbrace := p.expect(token.RBRACE).Pos
return &ast.CallExpr{
Func: &ast.IdentExpr{NamePos: lbrace, Name: "HB_AParams"},
RParen: rbrace,
}
}
// Empty hash: {=>} → empty hash literal
if p.at(token.DBLARROW) {
p.advance() // consume =>
rbrace := p.expect(token.RBRACE).Pos
return &ast.HashLitExpr{LBrace: lbrace, RBrace: rbrace}
}
// Handle leading comma: {, x, y} → {NIL, x, y}
if p.at(token.COMMA) {
var items []ast.Expr
items = append(items, &ast.LiteralExpr{ValuePos: p.current.Pos, Kind: token.NIL_LIT, Value: "NIL"})
for p.match(token.COMMA) {
if p.at(token.RBRACE) || p.at(token.COMMA) {
items = append(items, &ast.LiteralExpr{ValuePos: p.current.Pos, Kind: token.NIL_LIT, Value: "NIL"})
} else {
items = append(items, p.parseExpr())
}
}
rbrace := p.expect(token.RBRACE).Pos
return &ast.ArrayLitExpr{LBrace: lbrace, Items: items, RBrace: rbrace}
}
// Parse first element to determine: array or hash
first := p.parseExpr()
// Hash: { key => value, ... }
if p.at(token.DBLARROW) {
p.advance() // consume =>
firstVal := p.parseExpr()
keys := []ast.Expr{first}
vals := []ast.Expr{firstVal}
for p.match(token.COMMA) {
keys = append(keys, p.parseExpr())
p.expect(token.DBLARROW)
vals = append(vals, p.parseExpr())
}
rbrace := p.expect(token.RBRACE).Pos
return &ast.HashLitExpr{LBrace: lbrace, Keys: keys, Values: vals, RBrace: rbrace}
}
// Array: {expr, expr, ...}
items := []ast.Expr{first}
for p.match(token.COMMA) {
items = append(items, p.parseExpr())
}
rbrace := p.expect(token.RBRACE).Pos
return &ast.ArrayLitExpr{LBrace: lbrace, Items: items, RBrace: rbrace}
}
// parseMacro parses &variable or &(expression).
func (p *Parser) parseMacro() ast.Expr {
ampPos := p.expect(token.AMPERSAND).Pos
if p.at(token.LPAREN) {
// &(expression)
p.advance()
expr := p.parseExpr()
p.expect(token.RPAREN)
return &ast.MacroExpr{AmpPos: ampPos, Expr: expr}
}
// &variable[.suffix] — variable can be a keyword name
ident := p.expectMethodName()
macroExpr := &ast.MacroExpr{
AmpPos: ampPos,
Expr: &ast.IdentExpr{NamePos: ident.Pos, Name: ident.Literal},
}
// &var.suffix — dot terminates macro, suffix is text concatenation
// &var. — dot terminates macro with no suffix
// &var.1 — lexer may tokenize .1 as DOUBLE
if p.at(token.DOT) {
p.advance() // consume .
// Skip optional suffix identifier (e.g. &a.aa, &a.1)
if p.current.Kind == token.IDENT || p.current.Kind == token.INT {
p.advance()
}
} else if p.current.Kind == token.DOUBLE &&
(strings.HasPrefix(p.current.Literal, ".") || strings.HasPrefix(p.current.Literal, "0.")) {
// Lexer tokenized .1 as DOUBLE — consume it as macro suffix
p.advance()
}
return macroExpr
}
// parseIIF parses IIF(cond, trueExpr, falseExpr).
func (p *Parser) parseIIF() ast.Expr {
pos := p.advance().Pos // consume IIF
p.expect(token.LPAREN)
cond := p.parseExpr()
p.expect(token.COMMA)
var trueExpr ast.Expr
if p.at(token.COMMA) || p.at(token.RPAREN) {
trueExpr = &ast.LiteralExpr{ValuePos: p.current.Pos, Kind: token.NIL_LIT, Value: "NIL"}
} else {
trueExpr = p.parseExpr()
}
p.expect(token.COMMA)
var falseExpr ast.Expr
if p.at(token.RPAREN) {
falseExpr = &ast.LiteralExpr{ValuePos: p.current.Pos, Kind: token.NIL_LIT, Value: "NIL"}
} else {
falseExpr = p.parseExpr()
}
p.expect(token.RPAREN)
return &ast.IIfExpr{IfPos: pos, Cond: cond, True: trueExpr, False: falseExpr}
}
// parseExprList parses a comma-separated list of expressions.
func (p *Parser) parseExprList() []ast.Expr {
var list []ast.Expr
// Handle leading empty param: f(,x) → NIL, x
if p.at(token.COMMA) {
list = append(list, &ast.LiteralExpr{ValuePos: p.current.Pos, Kind: token.NIL_LIT, Value: "NIL"})
} else {
list = append(list, p.parseExpr())
}
for p.match(token.COMMA) {
// Empty param: f(x,,y) → x, NIL, y
if p.at(token.COMMA) || p.at(token.RPAREN) || p.at(token.RBRACE) {
list = append(list, &ast.LiteralExpr{ValuePos: p.current.Pos, Kind: token.NIL_LIT, Value: "NIL"})
} else {
list = append(list, p.parseExpr())
}
}
return list
}
// isSliceSyntax checks if current position inside [...] has a : before ].
// Limited lookahead — scans at most 10 tokens (covers 99% of real cases).
func (p *Parser) isSliceSyntax() bool {
depth := 0
maxLook := 10 // limit scan to avoid O(n)
for i := 0; i < maxLook; i++ {
k := p.peekAt(i)
switch k {
case token.COLON:
if depth == 0 {
return true
}
case token.LBRACKET, token.LPAREN, token.LBRACE:
depth++
case token.RPAREN, token.RBRACE:
depth--
case token.RBRACKET:
if depth == 0 {
return false
}
depth--
case token.NEWLINE, token.EOF:
return false
}
}
return false // too complex — treat as normal index
}
// parseSliceIndex parses expression inside slice but stops at : and ]
func (p *Parser) parseSliceIndex() ast.Expr {
return p.parsePrimaryExpr() // simple: just primary (number, ident, call)
}
// parseInterpolatedString: f"Hello {name}, age {age}"
// Parses the format string and extracts {expr} references.
// Converts to: fmt.Sprintf("Hello %v, age %v", name, age)
// --- Extracted helpers for expression registry ---
// parsePostfixSend: x:method or x:method(args...)
func (p *Parser) parsePostfixSend(x ast.Expr) ast.Expr {
colonPos := p.advance().Pos
var methodName string
var macroMethod ast.Expr
if p.current.Kind == token.AMPERSAND {
macroMethod = p.parseMacro()
} else {
methodName = p.expectMethodName().Literal
}
var args []ast.Expr
var lp, rp token.Position
hasParens := false
if p.at(token.LPAREN) {
hasParens = true
lp = p.advance().Pos
if !p.at(token.RPAREN) {
args = p.parseExprList()
}
rp = p.expect(token.RPAREN).Pos
}
return &ast.SendExpr{
Object: x, ColonPos: colonPos,
Method: methodName, MacroMethod: macroMethod,
HasParens: hasParens,
LParen: lp, Args: args, RParen: rp,
}
}
// parsePrimaryIdent: IDENT (variable, function ref, IIF, f-string)
func (p *Parser) parsePrimaryIdent() ast.Expr {
if strings.ToUpper(p.current.Literal) == "IIF" {
return p.parseIIF()
}
if p.current.Literal == "f" && p.peekAt(1) == token.STRING {
return p.parseInterpolatedString()
}
tok := p.advance()
return &ast.IdentExpr{NamePos: tok.Pos, Name: tok.Literal}
}
// parsePrimaryWithSend: :field (WITH OBJECT bare colon)
func (p *Parser) parsePrimaryWithSend() ast.Expr {
pos := p.advance().Pos
if p.at(token.IDENT) || p.current.Literal != "" {
name := p.advance()
return &ast.SendExpr{
Object: &ast.IdentExpr{NamePos: pos, Name: "__withObject"},
ColonPos: pos,
Method: name.Literal,
}
}
return &ast.IdentExpr{NamePos: pos, Name: "__withObject"}
}
// parsePrimarySelf: ::name or ::name(args)
func (p *Parser) parsePrimarySelf() ast.Expr {
pos := p.advance().Pos
if p.at(token.IDENT) || p.current.Literal != "" {
name := p.advance()
self := &ast.SelfExpr{ColonPos: pos}
hasParens := false
var args []ast.Expr
var lp, rp token.Position
if p.at(token.LPAREN) {
hasParens = true
lp = p.advance().Pos
if !p.at(token.RPAREN) {
args = p.parseExprList()
}
rp = p.expect(token.RPAREN).Pos
}
return &ast.SendExpr{
Object: self, ColonPos: pos, Method: name.Literal,
HasParens: hasParens, LParen: lp, Args: args, RParen: rp,
}
}
return &ast.SelfExpr{ColonPos: pos}
}
func (p *Parser) parseInterpolatedString() ast.Expr {
fPos := p.advance().Pos // consume 'f'
strTok := p.expect(token.STRING)
src := strTok.Literal
var parts []ast.Expr
var fmtBuf string
var args []ast.Expr
i := 0
for i < len(src) {
if src[i] == '{' {
// Find closing }
j := i + 1
depth := 1
for j < len(src) && depth > 0 {
if src[j] == '{' { depth++ }
if src[j] == '}' { depth-- }
j++
}
exprStr := src[i+1 : j-1]
// Check for format spec: {expr:fmt}
fmtSpec := "%v"
if colonIdx := strings.LastIndex(exprStr, ":"); colonIdx >= 0 {
fmtSpec = "%" + exprStr[colonIdx+1:]
exprStr = exprStr[:colonIdx]
}
fmtBuf += fmtSpec
// Parse the expression inside {}
// Simple: just use IdentExpr for variable names
args = append(args, &ast.IdentExpr{NamePos: fPos, Name: exprStr})
i = j
} else {
fmtBuf += string(src[i])
i++
}
}
if len(args) == 0 {
// No interpolation — return as plain string
return &ast.LiteralExpr{ValuePos: fPos, Kind: token.STRING, Value: src}
}
// Build: fmt.Sprintf(fmtStr, arg1, arg2, ...)
_ = parts // not used in Sprintf approach
allArgs := make([]ast.Expr, 0, len(args)+1)
allArgs = append(allArgs, &ast.LiteralExpr{ValuePos: fPos, Kind: token.STRING, Value: fmtBuf})
allArgs = append(allArgs, args...)
return &ast.CallExpr{
Func: &ast.DotExpr{
X: &ast.IdentExpr{NamePos: fPos, Name: "fmt"},
DotPos: fPos,
Member: "Sprintf",
},
LParen: fPos,
Args: allArgs,
RParen: fPos,
}
}

258
compiler/parser/exprreg.go Normal file
View File

@@ -0,0 +1,258 @@
// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
// All rights reserved.
// exprreg.go — Expression parser registries for Pratt parser.
//
// Three registries:
// prefixParsers — unary prefix: -, !, ++, --, <-, ASYNC, AWAIT
// postfixParsers — postfix: (), [], :, ., ?:, ++, --, ->
// primaryParsers — atoms: INT, STRING, IDENT, (, {, ::
//
// Adding a new operator = one line in init().
package parser
import (
"five/compiler/ast"
"five/compiler/token"
)
// PrefixParser parses a prefix unary expression.
type PrefixParser func(p *Parser) ast.Expr
// PostfixParser parses a postfix expression given the left-hand side.
type PostfixParser func(p *Parser, x ast.Expr) ast.Expr
// PrimaryParser parses an atomic/primary expression.
type PrimaryParser func(p *Parser) ast.Expr
var (
prefixParsers map[token.Kind]PrefixParser
postfixParsers map[token.Kind]PostfixParser
primaryParsers map[token.Kind]PrimaryParser
)
func init() {
prefixParsers = map[token.Kind]PrefixParser{
token.MINUS: prefixUnary(token.MINUS),
token.PLUS: prefixPlus,
token.NOT: prefixUnary(token.NOT),
token.INC: prefixUnary(token.INC),
token.DEC: prefixUnary(token.DEC),
token.ARROW_LEFT: prefixChanRecv,
token.ASYNC_KW: prefixAsync,
token.AWAIT_KW: prefixAwait,
token.AT: prefixRef,
}
postfixParsers = map[token.Kind]PostfixParser{
token.LPAREN: postfixCall,
token.LBRACKET: postfixIndex,
token.COLON: postfixSend,
token.QMARK: postfixNilSafe,
token.DOT: postfixDot,
token.ARROW: postfixAlias,
token.INC: postfixIncDec(token.INC),
token.DEC: postfixIncDec(token.DEC),
token.COLONCOLON: postfixSelfStop,
}
primaryParsers = map[token.Kind]PrimaryParser{
token.INT: primaryLiteral,
token.LONG: primaryLiteral,
token.DOUBLE: primaryLiteral,
token.STRING: primaryLiteral,
token.DATE_LIT: primaryLiteral,
token.TRUE: primaryLiteral,
token.FALSE: primaryLiteral,
token.NIL_LIT: primaryLiteral,
token.COLONCOLON: primarySelf,
token.LPAREN: primaryParen,
token.IF: primaryIf,
token.IDENT: primaryIdent,
token.AMPERSAND: primaryMacro,
token.COLON: primaryWithSend,
token.LBRACE: primaryArrayOrBlock,
}
}
// --- Prefix parsers ---
func prefixUnary(op token.Kind) PrefixParser {
return func(p *Parser) ast.Expr {
tok := p.advance()
x := p.parseUnaryExpr()
return &ast.UnaryExpr{OpPos: tok.Pos, Op: op, X: x}
}
}
func prefixPlus(p *Parser) ast.Expr {
p.advance() // unary plus — no-op
return p.parseUnaryExpr()
}
func prefixChanRecv(p *Parser) ast.Expr {
pos := p.advance().Pos
ch := p.parsePostfixExpr()
return &ast.ChanRecvExpr{ArrowPos: pos, Chan: ch}
}
func prefixAsync(p *Parser) ast.Expr {
pos := p.advance().Pos
call := p.parsePostfixExpr()
return &ast.AsyncExpr{AsyncPos: pos, Call: call}
}
func prefixAwait(p *Parser) ast.Expr {
pos := p.advance().Pos
future := p.parsePostfixExpr()
return &ast.AwaitExpr{AwaitPos: pos, Future: future}
}
func prefixRef(p *Parser) ast.Expr {
op := p.advance()
x := p.parseUnaryExpr()
return &ast.RefExpr{AtPos: op.Pos, X: x}
}
// --- Postfix parsers ---
func postfixCall(p *Parser, x ast.Expr) ast.Expr {
lp := p.advance().Pos
var args []ast.Expr
if !p.at(token.RPAREN) {
args = p.parseExprList()
}
rp := p.expect(token.RPAREN).Pos
return &ast.CallExpr{Func: x, LParen: lp, Args: args, RParen: rp}
}
func postfixIndex(p *Parser, x ast.Expr) ast.Expr {
lb := p.advance().Pos
// Slice syntax detection
if p.isSliceSyntax() {
var low, high ast.Expr
if !p.at(token.COLON) {
low = p.parseSliceIndex()
}
p.expect(token.COLON)
if !p.at(token.RBRACKET) {
high = p.parseSliceIndex()
}
rb := p.expect(token.RBRACKET).Pos
return &ast.SliceExpr{X: x, LBracket: lb, Low: low, High: high, RBracket: rb}
}
// Normal array index
index := p.parseExpr()
rb := token.Position{}
for p.match(token.COMMA) {
rb = p.current.Pos
x = &ast.IndexExpr{X: x, LBracket: lb, Index: index, RBracket: rb}
index = p.parseExpr()
lb = rb
}
rb = p.expect(token.RBRACKET).Pos
return &ast.IndexExpr{X: x, LBracket: lb, Index: index, RBracket: rb}
}
func postfixDot(p *Parser, x ast.Expr) ast.Expr {
if p.peekLitAt(1) != "" {
dotPos := p.advance().Pos
member := p.advance()
return &ast.DotExpr{X: x, DotPos: dotPos, Member: member.Literal}
}
return nil // signal: stop postfix loop
}
func postfixIncDec(op token.Kind) PostfixParser {
return func(p *Parser, x ast.Expr) ast.Expr {
opPos := p.advance().Pos
return &ast.PostfixExpr{X: x, OpPos: opPos, Op: op}
}
}
func postfixSelfStop(p *Parser, x ast.Expr) ast.Expr {
return nil // :: after expression — stop
}
// postfixNilSafe and postfixSend/postfixAlias are complex — kept in expr.go
// They call back into the main parser methods.
func postfixNilSafe(p *Parser, x ast.Expr) ast.Expr {
if p.peekAt(1) != token.COLON {
return nil // bare ? = QOut, not postfix
}
p.advance() // consume ?
qpos := p.advance().Pos // consume :
methodName := p.expectMethodName().Literal
var args []ast.Expr
hasParens := false
if p.at(token.LPAREN) {
hasParens = true
p.advance()
if !p.at(token.RPAREN) {
args = p.parseExprList()
}
p.expect(token.RPAREN)
}
return &ast.NilSafeExpr{X: x, QPos: qpos, Method: methodName, Args: args, HasParens: hasParens}
}
func postfixAlias(p *Parser, x ast.Expr) ast.Expr {
arrowPos := p.advance().Pos
field := p.parsePrimaryExpr()
return &ast.AliasExpr{Alias: x, ArrowPos: arrowPos, Field: field}
}
func postfixSend(p *Parser, x ast.Expr) ast.Expr {
return p.parsePostfixSend(x)
}
// --- Primary parsers ---
func primaryLiteral(p *Parser) ast.Expr {
tok := p.advance()
return &ast.LiteralExpr{ValuePos: tok.Pos, Kind: tok.Kind, Value: tok.Literal}
}
func primaryParen(p *Parser) ast.Expr {
p.advance()
expr := p.parseExpr()
for p.match(token.COMMA) {
expr = p.parseExpr()
}
p.expect(token.RPAREN)
return expr
}
func primaryIf(p *Parser) ast.Expr {
if p.peekAt(1) == token.LPAREN {
return p.parseIIF()
}
p.error("expected expression, got IF")
tok := p.advance()
return &ast.LiteralExpr{ValuePos: tok.Pos, Kind: token.NIL_LIT, Value: "NIL"}
}
func primaryIdent(p *Parser) ast.Expr {
return p.parsePrimaryIdent()
}
func primaryMacro(p *Parser) ast.Expr {
return p.parseMacro()
}
func primaryWithSend(p *Parser) ast.Expr {
return p.parsePrimaryWithSend()
}
func primaryArrayOrBlock(p *Parser) ast.Expr {
return p.parseArrayOrBlock()
}
func primarySelf(p *Parser) ast.Expr {
return p.parsePrimarySelf()
}

2162
compiler/parser/parser.go Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,427 @@
// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
// All rights reserved.
package parser
import (
"five/compiler/ast"
"five/compiler/token"
"testing"
)
func parseOK(t *testing.T, source string) *ast.File {
t.Helper()
file, errs := Parse("test.prg", source)
if len(errs) > 0 {
for _, e := range errs {
t.Errorf("parse error: %s", e)
}
t.FailNow()
}
return file
}
// --- Function declaration ---
func TestParseSimpleFunction(t *testing.T) {
file := parseOK(t, `FUNCTION Main()
RETURN NIL
`)
if len(file.Decls) != 1 {
t.Fatalf("expected 1 decl, got %d", len(file.Decls))
}
fn, ok := file.Decls[0].(*ast.FuncDecl)
if !ok {
t.Fatalf("expected FuncDecl, got %T", file.Decls[0])
}
if fn.Name != "Main" {
t.Errorf("name = %q, want %q", fn.Name, "Main")
}
if fn.IsProc {
t.Error("should not be PROCEDURE")
}
}
func TestParseFunctionWithLocals(t *testing.T) {
file := parseOK(t, `FUNCTION Foo(a, b)
LOCAL n := 10
LOCAL cName := "hello", x
RETURN n
`)
fn := file.Decls[0].(*ast.FuncDecl)
if len(fn.Params) != 2 {
t.Errorf("params = %d, want 2", len(fn.Params))
}
if len(fn.Decls) != 2 {
t.Errorf("decls = %d, want 2 (two LOCAL statements)", len(fn.Decls))
}
// Check second LOCAL has 2 vars
vd := fn.Decls[1].(*ast.VarDecl)
if len(vd.Vars) != 2 {
t.Errorf("second LOCAL vars = %d, want 2", len(vd.Vars))
}
}
func TestParseProcedure(t *testing.T) {
file := parseOK(t, `PROCEDURE DoStuff()
RETURN
`)
fn := file.Decls[0].(*ast.FuncDecl)
if !fn.IsProc {
t.Error("should be PROCEDURE")
}
}
// --- Expressions ---
func TestParseArithmetic(t *testing.T) {
file := parseOK(t, `FUNCTION Main()
RETURN 1 + 2 * 3
`)
fn := file.Decls[0].(*ast.FuncDecl)
ret := fn.Body[0].(*ast.ReturnStmt)
// Should be: 1 + (2 * 3) due to precedence
bin, ok := ret.Value.(*ast.BinaryExpr)
if !ok {
t.Fatalf("expected BinaryExpr, got %T", ret.Value)
}
if bin.Op != token.PLUS {
t.Errorf("top op = %v, want PLUS", bin.Op)
}
// Right side should be 2 * 3
right, ok := bin.Right.(*ast.BinaryExpr)
if !ok {
t.Fatalf("right should be BinaryExpr, got %T", bin.Right)
}
if right.Op != token.STAR {
t.Errorf("right op = %v, want STAR", right.Op)
}
}
func TestParseAssignment(t *testing.T) {
file := parseOK(t, `FUNCTION Main()
LOCAL n
n := 10
RETURN n
`)
fn := file.Decls[0].(*ast.FuncDecl)
// Body[0] should be assignment: n := 10
es := fn.Body[0].(*ast.ExprStmt)
assign, ok := es.X.(*ast.AssignExpr)
if !ok {
t.Fatalf("expected AssignExpr, got %T", es.X)
}
if assign.Op != token.ASSIGN {
t.Errorf("assign op = %v, want ASSIGN", assign.Op)
}
}
func TestParseFunctionCall(t *testing.T) {
file := parseOK(t, `FUNCTION Main()
RETURN Str(42)
`)
fn := file.Decls[0].(*ast.FuncDecl)
ret := fn.Body[0].(*ast.ReturnStmt)
call, ok := ret.Value.(*ast.CallExpr)
if !ok {
t.Fatalf("expected CallExpr, got %T", ret.Value)
}
ident := call.Func.(*ast.IdentExpr)
if ident.Name != "Str" {
t.Errorf("func name = %q, want Str", ident.Name)
}
if len(call.Args) != 1 {
t.Errorf("args = %d, want 1", len(call.Args))
}
}
func TestParseStringConcat(t *testing.T) {
file := parseOK(t, `FUNCTION Main()
RETURN "Hello, " + "World!"
`)
fn := file.Decls[0].(*ast.FuncDecl)
ret := fn.Body[0].(*ast.ReturnStmt)
bin := ret.Value.(*ast.BinaryExpr)
if bin.Op != token.PLUS {
t.Errorf("op = %v, want PLUS", bin.Op)
}
}
// --- Control flow ---
func TestParseIfElse(t *testing.T) {
file := parseOK(t, `FUNCTION Main()
LOCAL n := 10
IF n > 5
RETURN .T.
ELSE
RETURN .F.
ENDIF
`)
fn := file.Decls[0].(*ast.FuncDecl)
ifStmt, ok := fn.Body[0].(*ast.IfStmt)
if !ok {
t.Fatalf("expected IfStmt, got %T", fn.Body[0])
}
if len(ifStmt.Body) != 1 {
t.Errorf("if body = %d stmts", len(ifStmt.Body))
}
if len(ifStmt.ElseBody) != 1 {
t.Errorf("else body = %d stmts", len(ifStmt.ElseBody))
}
}
func TestParseIfElseIf(t *testing.T) {
file := parseOK(t, `FUNCTION Main()
LOCAL n := 10
IF n > 10
RETURN 1
ELSEIF n > 5
RETURN 2
ELSEIF n > 0
RETURN 3
ELSE
RETURN 0
ENDIF
`)
fn := file.Decls[0].(*ast.FuncDecl)
ifStmt := fn.Body[0].(*ast.IfStmt)
if len(ifStmt.ElseIfs) != 2 {
t.Errorf("elseifs = %d, want 2", len(ifStmt.ElseIfs))
}
}
func TestParseDoWhile(t *testing.T) {
file := parseOK(t, `FUNCTION Main()
LOCAL i := 0
DO WHILE i < 10
i++
ENDDO
RETURN i
`)
fn := file.Decls[0].(*ast.FuncDecl)
dw, ok := fn.Body[0].(*ast.DoWhileStmt)
if !ok {
t.Fatalf("expected DoWhileStmt, got %T", fn.Body[0])
}
if len(dw.Body) != 1 {
t.Errorf("body = %d stmts", len(dw.Body))
}
}
func TestParseForNext(t *testing.T) {
file := parseOK(t, `FUNCTION Main()
LOCAL i
FOR i := 1 TO 10
? i
NEXT
RETURN NIL
`)
fn := file.Decls[0].(*ast.FuncDecl)
forStmt, ok := fn.Body[0].(*ast.ForStmt)
if !ok {
t.Fatalf("expected ForStmt, got %T", fn.Body[0])
}
if forStmt.Var != "i" {
t.Errorf("var = %q, want i", forStmt.Var)
}
}
func TestParseForEach(t *testing.T) {
file := parseOK(t, `FUNCTION Main()
LOCAL x
FOR EACH x IN {1, 2, 3}
? x
NEXT
RETURN NIL
`)
fn := file.Decls[0].(*ast.FuncDecl)
fe, ok := fn.Body[0].(*ast.ForEachStmt)
if !ok {
t.Fatalf("expected ForEachStmt, got %T", fn.Body[0])
}
if fe.Var != "x" {
t.Errorf("var = %q, want x", fe.Var)
}
}
// --- QOut ---
func TestParseQOut(t *testing.T) {
file := parseOK(t, `FUNCTION Main()
? "Hello"
? 1 + 2, "World"
RETURN NIL
`)
fn := file.Decls[0].(*ast.FuncDecl)
q1, ok := fn.Body[0].(*ast.QOutStmt)
if !ok {
t.Fatalf("expected QOutStmt, got %T", fn.Body[0])
}
if len(q1.Exprs) != 1 {
t.Errorf("? args = %d, want 1", len(q1.Exprs))
}
q2 := fn.Body[1].(*ast.QOutStmt)
if len(q2.Exprs) != 2 {
t.Errorf("? args = %d, want 2", len(q2.Exprs))
}
}
// --- xBase commands ---
func TestParseUse(t *testing.T) {
file := parseOK(t, `FUNCTION Main()
USE "customers" VIA DBFCDX ALIAS cust
RETURN NIL
`)
fn := file.Decls[0].(*ast.FuncDecl)
use, ok := fn.Body[0].(*ast.UseCmd)
if !ok {
t.Fatalf("expected UseCmd, got %T", fn.Body[0])
}
if use.Via != "DBFCDX" {
t.Errorf("via = %q, want DBFCDX", use.Via)
}
if use.Alias != "cust" {
t.Errorf("alias = %q, want cust", use.Alias)
}
}
func TestParseGoTop(t *testing.T) {
file := parseOK(t, `FUNCTION Main()
GO TOP
RETURN NIL
`)
fn := file.Decls[0].(*ast.FuncDecl)
goCmd, ok := fn.Body[0].(*ast.GoCmd)
if !ok {
t.Fatalf("expected GoCmd, got %T", fn.Body[0])
}
if goCmd.Direction != "TOP" {
t.Errorf("direction = %q, want TOP", goCmd.Direction)
}
}
func TestParseSeek(t *testing.T) {
file := parseOK(t, `FUNCTION Main()
SEEK "SMITH"
RETURN NIL
`)
fn := file.Decls[0].(*ast.FuncDecl)
seek, ok := fn.Body[0].(*ast.SeekCmd)
if !ok {
t.Fatalf("expected SeekCmd, got %T", fn.Body[0])
}
lit := seek.Key.(*ast.LiteralExpr)
if lit.Value != "SMITH" {
t.Errorf("key = %q, want SMITH", lit.Value)
}
}
func TestParseReplace(t *testing.T) {
file := parseOK(t, `FUNCTION Main()
REPLACE name WITH "Kim", salary WITH 50000
RETURN NIL
`)
fn := file.Decls[0].(*ast.FuncDecl)
rep, ok := fn.Body[0].(*ast.ReplaceCmd)
if !ok {
t.Fatalf("expected ReplaceCmd, got %T", fn.Body[0])
}
if len(rep.Fields) != 2 {
t.Errorf("fields = %d, want 2", len(rep.Fields))
}
}
// --- Array and Hash literals ---
func TestParseArrayLiteral(t *testing.T) {
file := parseOK(t, `FUNCTION Main()
RETURN {1, 2, 3}
`)
fn := file.Decls[0].(*ast.FuncDecl)
ret := fn.Body[0].(*ast.ReturnStmt)
arr, ok := ret.Value.(*ast.ArrayLitExpr)
if !ok {
t.Fatalf("expected ArrayLitExpr, got %T", ret.Value)
}
if len(arr.Items) != 3 {
t.Errorf("items = %d, want 3", len(arr.Items))
}
}
func TestParseHashLiteral(t *testing.T) {
file := parseOK(t, `FUNCTION Main()
RETURN {"a" => 1, "b" => 2}
`)
fn := file.Decls[0].(*ast.FuncDecl)
ret := fn.Body[0].(*ast.ReturnStmt)
hash, ok := ret.Value.(*ast.HashLitExpr)
if !ok {
t.Fatalf("expected HashLitExpr, got %T", ret.Value)
}
if len(hash.Keys) != 2 {
t.Errorf("keys = %d, want 2", len(hash.Keys))
}
}
func TestParseCodeBlock(t *testing.T) {
file := parseOK(t, `FUNCTION Main()
RETURN {|x| x + 1}
`)
fn := file.Decls[0].(*ast.FuncDecl)
ret := fn.Body[0].(*ast.ReturnStmt)
blk, ok := ret.Value.(*ast.BlockExpr)
if !ok {
t.Fatalf("expected BlockExpr, got %T", ret.Value)
}
if len(blk.Params) != 1 || blk.Params[0] != "x" {
t.Errorf("params = %v, want [x]", blk.Params)
}
}
// --- IMPORT ---
func TestParseImport(t *testing.T) {
file := parseOK(t, `IMPORT "net/http"
FUNCTION Main()
RETURN NIL
`)
if len(file.Imports) != 1 {
t.Fatalf("imports = %d, want 1", len(file.Imports))
}
if file.Imports[0].Path != "net/http" {
t.Errorf("import path = %q, want net/http", file.Imports[0].Path)
}
}
// --- Full program ---
func TestParseFullProgram(t *testing.T) {
src := `FUNCTION Main()
LOCAL nSum := 0, i
FOR i := 1 TO 10
nSum += i
NEXT
? "Sum =", nSum
IF nSum > 50
? "Big"
ELSE
? "Small"
ENDIF
RETURN nSum
`
file := parseOK(t, src)
fn := file.Decls[0].(*ast.FuncDecl)
if fn.Name != "Main" {
t.Errorf("name = %q", fn.Name)
}
if len(fn.Decls) != 1 {
t.Errorf("decls = %d, want 1 (LOCAL)", len(fn.Decls))
}
// Body: FOR + ? + IF + RETURN
if len(fn.Body) < 3 {
t.Errorf("body stmts = %d, want at least 3", len(fn.Body))
}
}

287
compiler/parser/stmtreg.go Normal file
View File

@@ -0,0 +1,287 @@
// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
// All rights reserved.
// stmtreg.go — Statement parser registry.
//
// Instead of a 800+ line switch in parseStmt(), each statement type
// registers its parser function. New statements can be added by
// simply adding one line to initStmtRegistry().
//
// Pattern: token.Kind → func(*Parser) ast.Stmt
package parser
import (
"five/compiler/ast"
"five/compiler/token"
"strings"
)
// StmtParser is a function that parses a statement starting with the current token.
type StmtParser func(p *Parser) ast.Stmt
// stmtRegistry maps token kinds to their statement parsers.
var stmtRegistry map[token.Kind]StmtParser
func init() {
stmtRegistry = map[token.Kind]StmtParser{
// Control flow
token.IF: (*Parser).stmtIf,
token.DO: (*Parser).stmtDo,
token.WHILE: (*Parser).stmtWhile,
token.FOR: (*Parser).stmtFor,
token.BEGIN: (*Parser).stmtBegin,
token.SWITCH: (*Parser).stmtSwitch,
token.RETURN: (*Parser).stmtReturn,
token.EXIT: (*Parser).stmtExit,
token.LOOP: (*Parser).stmtLoop,
// I/O
token.QMARK: (*Parser).stmtQOut,
token.QQMARK: (*Parser).stmtQQOut,
// Variables
token.PRIVATE: (*Parser).stmtPrivate,
token.PUBLIC: (*Parser).stmtPublic,
token.LOCAL: (*Parser).stmtVarDecl,
token.STATIC: (*Parser).stmtVarDecl,
token.PARAMETERS: (*Parser).stmtParameters,
token.DECLARE: (*Parser).stmtDeclare,
// xBase database
token.USE: (*Parser).stmtUse,
token.SELECT: (*Parser).stmtSelect,
token.GO: (*Parser).stmtGo,
token.GOTO: (*Parser).stmtGo,
token.SKIP_KW: (*Parser).stmtSkip,
token.SEEK: (*Parser).stmtSeek,
token.REPLACE: (*Parser).stmtReplace,
token.APPEND: (*Parser).stmtAppend,
token.DELETE_KW: (*Parser).stmtDelete,
token.RECALL: (*Parser).stmtRecallPackZap,
token.PACK: (*Parser).stmtRecallPackZap,
token.ZAP: (*Parser).stmtRecallPackZap,
token.INDEX: (*Parser).stmtIndex,
token.SET: (*Parser).stmtSet,
// Screen
token.AT: (*Parser).stmtAt,
// Five Go extensions
token.DEFER_KW: (*Parser).stmtDefer,
token.CONST_KW: (*Parser).stmtConst,
token.WATCH_KW: (*Parser).stmtWatch,
token.WITH: (*Parser).stmtWith,
token.PARALLEL_KW: (*Parser).stmtParallel,
token.SPAWN_KW: (*Parser).stmtSpawn,
token.ARROW_LEFT: (*Parser).stmtArrowLeft,
}
}
// lookupStmtParser finds a registered parser for the current token.
func (p *Parser) lookupStmtParser() StmtParser {
if fn, ok := stmtRegistry[p.current.Kind]; ok {
return fn
}
return nil
}
// --- Thin wrappers: each calls the existing parse method ---
func (p *Parser) stmtIf() ast.Stmt {
if p.peekAt(1) == token.LPAREN {
if p.looksLikeIIF() {
return p.parseExprStmt()
}
}
return p.parseIf()
}
func (p *Parser) stmtDo() ast.Stmt {
if p.peekAt(1) == token.LPAREN {
p.tokens[p.pos].Kind = token.IDENT
p.tokens[p.pos].Literal = "Do"
p.current = p.tokens[p.pos]
return p.parseExprStmt()
}
if p.peekAt(1) == token.CASE || token.LookupKeyword(p.peekLitAt(1)) == token.CASE {
return p.parseDoCase()
}
if p.peekAt(1) == token.WHILE {
return p.parseDoWhile()
}
if p.peekAt(1) == token.IDENT {
return p.parseDoProc()
}
return p.parseDoWhile()
}
func (p *Parser) stmtWhile() ast.Stmt {
if p.peekAt(1) == token.LPAREN {
p.tokens[p.pos].Kind = token.IDENT
p.tokens[p.pos].Literal = "While"
p.current = p.tokens[p.pos]
return p.parseExprStmt()
}
return p.parseDoWhile()
}
func (p *Parser) stmtFor() ast.Stmt {
next := p.peekAt(1)
if next == token.ASSIGN || next == token.LPAREN ||
next == token.PLUSEQ || next == token.MINUSEQ {
p.tokens[p.pos].Kind = token.IDENT
p.tokens[p.pos].Literal = "for"
p.current = p.tokens[p.pos]
return p.parseExprStmt()
}
return p.parseFor()
}
func (p *Parser) stmtBegin() ast.Stmt {
if p.peekAt(1) != token.SEQUENCE && p.peekAt(1) != token.NEWLINE && p.peekAt(1) != token.EOF {
p.tokens[p.pos].Kind = token.IDENT
p.tokens[p.pos].Literal = "begin"
p.current = p.tokens[p.pos]
return p.parseExprStmt()
}
return p.parseBeginSequence()
}
func (p *Parser) stmtSwitch() ast.Stmt { return p.parseSwitch() }
func (p *Parser) stmtReturn() ast.Stmt {
next := p.peekAt(1)
if next == token.ASSIGN || next == token.PLUSEQ || next == token.MINUSEQ {
p.tokens[p.pos].Kind = token.IDENT
p.tokens[p.pos].Literal = "return"
p.current = p.tokens[p.pos]
return p.parseExprStmt()
}
return p.parseReturn()
}
func (p *Parser) stmtExit() ast.Stmt {
pos := p.advance().Pos
return &ast.ExitStmt{ExitPos: pos}
}
func (p *Parser) stmtLoop() ast.Stmt {
pos := p.advance().Pos
return &ast.LoopStmt{LoopPos: pos}
}
func (p *Parser) stmtQOut() ast.Stmt { return p.parseQOut(false) }
func (p *Parser) stmtQQOut() ast.Stmt { return p.parseQOut(true) }
func (p *Parser) stmtPrivate() ast.Stmt { return p.parsePrivatePublic(ast.ScopePrivate) }
func (p *Parser) stmtPublic() ast.Stmt { return p.parsePrivatePublic(ast.ScopePublic) }
func (p *Parser) stmtVarDecl() ast.Stmt { return p.parseVarDecl() }
func (p *Parser) stmtParameters() ast.Stmt {
p.tokens[p.pos].Kind = token.LOCAL
p.current = p.tokens[p.pos]
return p.parseVarDecl()
}
func (p *Parser) stmtDeclare() ast.Stmt {
p.skipToEndOfLine()
p.expectEndOfStmt()
return &ast.ExprStmt{X: &ast.LiteralExpr{Kind: token.NIL_LIT, Value: "NIL"}}
}
func (p *Parser) stmtUse() ast.Stmt { return p.parseUse() }
func (p *Parser) stmtSelect() ast.Stmt { return p.parseSelect() }
func (p *Parser) stmtSkip() ast.Stmt { return p.parseSkip() }
func (p *Parser) stmtSeek() ast.Stmt { return p.parseSeek() }
func (p *Parser) stmtReplace() ast.Stmt { return p.parseReplace() }
func (p *Parser) stmtAppend() ast.Stmt { return p.parseAppend() }
func (p *Parser) stmtIndex() ast.Stmt { return p.parseIndex() }
func (p *Parser) stmtAt() ast.Stmt { return p.parseAtCmd() }
func (p *Parser) stmtGo() ast.Stmt {
if p.current.Kind == token.GO && p.peekAt(1) == token.LPAREN {
p.tokens[p.pos].Kind = token.IDENT
p.tokens[p.pos].Literal = "Go"
p.current = p.tokens[p.pos]
return p.parseExprStmt()
}
return p.parseGo()
}
func (p *Parser) stmtDelete() ast.Stmt {
pos := p.advance().Pos
if p.current.Kind == token.IDENT {
upper := strings.ToUpper(p.current.Literal)
if upper == "FILE" {
p.skipToEndOfLine()
p.expectEndOfStmt()
return &ast.ExprStmt{X: &ast.LiteralExpr{Kind: token.NIL_LIT, Value: "NIL"}}
}
if upper == "ALL" || upper == "TAG" {
p.skipToEndOfLine()
p.expectEndOfStmt()
return &ast.ExprStmt{X: &ast.LiteralExpr{Kind: token.NIL_LIT, Value: "NIL"}}
}
}
p.expectEndOfStmt()
return &ast.ExprStmt{X: &ast.CallExpr{
Func: &ast.IdentExpr{NamePos: pos, Name: "DbDelete"},
}}
}
func (p *Parser) stmtRecallPackZap() ast.Stmt {
tok := p.advance()
var fname string
switch tok.Kind {
case token.RECALL:
fname = "DbRecall"
case token.PACK:
fname = "__DbPack"
case token.ZAP:
fname = "__DbZap"
}
p.expectEndOfStmt()
return &ast.ExprStmt{X: &ast.CallExpr{
Func: &ast.IdentExpr{NamePos: tok.Pos, Name: fname},
}}
}
func (p *Parser) stmtSet() ast.Stmt {
// SET command — skip to EOL (SET COLOR, SET FILTER, SET ORDER, etc.)
p.skipToEndOfLine()
p.expectEndOfStmt()
return &ast.ExprStmt{X: &ast.LiteralExpr{Kind: token.NIL_LIT, Value: "NIL"}}
}
func (p *Parser) stmtDefer() ast.Stmt { return p.parseDefer() }
func (p *Parser) stmtConst() ast.Stmt { return p.parseConstBlock() }
func (p *Parser) stmtWatch() ast.Stmt { return p.parseWatch() }
func (p *Parser) stmtParallel() ast.Stmt { return p.parseParallelFor() }
func (p *Parser) stmtWith() ast.Stmt {
if p.peekAt(1) == token.TIMEOUT_KW {
return p.parseWithTimeout()
}
p.skipToEndOfLine()
p.expectEndOfStmt()
return &ast.ExprStmt{X: &ast.LiteralExpr{Kind: token.NIL_LIT, Value: "NIL"}}
}
func (p *Parser) stmtSpawn() ast.Stmt {
goPos := p.advance().Pos
block := p.parseArrayOrBlock()
if blk, ok := block.(*ast.BlockExpr); ok {
p.expectEndOfStmt()
return &ast.GoBlockStmt{GoPos: goPos, Block: blk}
}
p.expectEndOfStmt()
return &ast.ExprStmt{X: block}
}
func (p *Parser) stmtArrowLeft() ast.Stmt {
pos := p.advance().Pos
ch := p.parseExpr()
p.expectEndOfStmt()
return &ast.ExprStmt{X: &ast.ChanRecvExpr{ArrowPos: pos, Chan: ch}}
}