Files
five/compiler/parser/stmtreg.go
CharlesKWON f4ed42556b checkpoint: season-wide bug fix campaign + infra
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>
2026-04-30 09:26:25 +09:00

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}}
}