Cumulative season's silent-bug hunting (~62 fixes) across the FiveSql2 SQL engine, the Five compiler/runtime, and the hbrdd RDD layer. Saved as a single checkpoint before refactoring the parser to delegate xBase command translation to the preprocessor. Highlights: FiveSql2 engine (_FiveSql2/src/) - prefix-glob index attach -> explicit convention (<table>_pk.ntx, <table>_uq.ntx, <table>.cdx) — fixes silent multi-row INSERT row-drop - DROP/CREATE TABLE FErase chain extended (.cdx, .fsc, .fsv, .dbt, .fpt) - COUNT(DISTINCT col) parsed + aggregated via hSeen hash - UNION column-count mismatch returns SQL_ERR_GRAMMAR (was silent) - DISTINCT + ORDER BY hidden-col leak fixed (trim before DISTINCT) - Derived table FROM (SELECT...) + JOIN right-side derived - Self-FK CASCADE depth 2+ via SqlGetSingleColPK pre-collect - LAG/LEAD default arg uses SqlEvalRowExpr (handles -N const exprs) - DATE literal round-trip validation (Feb 29 non-leap rejected) - CREATE OR REPLACE VIEW; CREATE VIEW errors on already-exists - AlterTable type dispatcher comma-wrapped (1-char type "A" no longer matches CHARACTER) Compiler / runtime - gengo: HB_ -> FV_ prefix on emitted Go function names (Five identity) - gengo split: emit_block.go, emit_stmt.go, folding.go extracted - parser/stmtreg.go nudges - hbrt: debug TUI/CLI restructure (debugcmd, debugkey, termios_*), windows debug stubs collapsed - thread/vm/value/class/pcinterp tightening from panic traces RDD layer (hbrdd/) - dbf: null bitmap support (null.go + null_test.go), mmap split (mmap_posix.go / mmap_windows.go), byte-level numeric parse - ntx/cdx: windows mmap parity - workarea + mem RDD: cross-area state-bleed fixes RTL (hbrtl/) - errorlog rewrite with platform-specific FD (errorlog_fd_unix / errorlog_fd_other) - sqlscan, sqlhelpers, indexrtl, datetime extensions Gates green at checkpoint: - go test ./... : PASS - FiveSql2 SQL:1999 : 43/43 - Harbour compat : 56/56 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
285 lines
8.2 KiB
Go
285 lines
8.2 KiB
Go
// 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"
|
|
)
|
|
|
|
// 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 ---
|
|
|
|
// rewriteAsIdent treats the current keyword token as an identifier for expression parsing.
|
|
// Instead of mutating the token array (which breaks backtracking), it temporarily
|
|
// overwrites the current token and restores nothing — the token is consumed immediately.
|
|
func (p *Parser) rewriteAsIdent(name string) {
|
|
p.current = token.Token{Kind: token.IDENT, Literal: name, Pos: p.current.Pos}
|
|
}
|
|
|
|
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.rewriteAsIdent("Do")
|
|
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.rewriteAsIdent("While")
|
|
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.rewriteAsIdent("for")
|
|
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.rewriteAsIdent("begin")
|
|
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.rewriteAsIdent("return")
|
|
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.current = token.Token{Kind: token.LOCAL, Literal: "LOCAL", Pos: p.current.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.rewriteAsIdent("Go")
|
|
return p.parseExprStmt()
|
|
}
|
|
return p.parseGo()
|
|
}
|
|
|
|
func (p *Parser) stmtDelete() ast.Stmt {
|
|
pos := p.advance().Pos
|
|
if p.current.Kind == token.IDENT {
|
|
upper := p.currentUpper()
|
|
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(...)` with parentheses is a function call (the Harbour Set()
|
|
// runtime function for reading/writing SET slots), not a SET command.
|
|
// Treat it as an expression statement so the args reach the call.
|
|
if p.peekAt(1) == token.LPAREN {
|
|
p.rewriteAsIdent("Set")
|
|
return p.parseExprStmt()
|
|
}
|
|
return p.parseSet()
|
|
}
|
|
|
|
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}}
|
|
}
|