Files
five/compiler/gengo/emit_stmt.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

1352 lines
37 KiB
Go

// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
// All rights reserved.
// Statement emission for the Go code generator.
//
// Every PRG statement kind ultimately routes through emitStmt; this file
// holds emitStmt and every per-kind emitter it dispatches to:
//
// - emitMidVarDecl / emitQOut / emitExprStmt / emitAssign /
// emitCallAsStmt / emitMultiAssign / emitDefer — non-control-flow
// statements.
// - emitIf / emitDoWhile / emitFor / emitForEach / emitSwitch /
// emitBeginSequence — control flow.
//
// A family of helper predicates (boolLiteralValue, hasRDDCommands,
// hasWorkareaChange, collectSymbols, collectReplaceFields,
// hasAppendInBody, bodyHasLoop, hasLoopStmt) supports hoisting and
// fast-path decisions inside the control-flow emitters; they live
// here rather than in gen_util.go because they're only used by
// statement emission.
package gengo
import (
"five/compiler/ast"
"five/compiler/token"
"fmt"
"strings"
)
// --- Statement emission ---
func (g *Generator) emitStmt(stmt ast.Stmt, locals localMap) {
// Line hook — two variants:
// - Non-debug builds get DebugLineFast (inlineable, ~2 field
// writes) so error.log panic traces still carry a line number
// without the per-statement dispatch tax.
// - Debug builds get DebugLine, which additionally drives
// breakpoints, step mode, and the trace ring.
if stmt.Pos().Line > 0 {
if g.Debug {
g.writeln(fmt.Sprintf("t.DebugLine(%q, %d)", g.file.Name, stmt.Pos().Line))
} else {
g.writeln(fmt.Sprintf("t.DebugLineFast(%q, %d)", g.file.Name, stmt.Pos().Line))
}
}
switch s := stmt.(type) {
case *ast.ReturnStmt:
if len(s.Values) > 1 {
// Multi-return: RETURN a, b, c → push array of values
for _, v := range s.Values {
g.emitExpr(v)
}
g.writeln(fmt.Sprintf("t.ArrayGen(%d)", len(s.Values)))
g.writeln("t.RetValue()")
} else if s.Value != nil {
g.emitExpr(s.Value)
g.writeln("t.RetValue()")
} else {
g.writeln("t.RetNil()")
}
g.writeln("return") // Go return to exit function immediately
case *ast.QOutStmt:
g.emitQOut(s, locals)
case *ast.ExprStmt:
g.emitExprStmt(s, locals)
case *ast.IfStmt:
g.emitIf(s, locals)
case *ast.SwitchStmt:
g.emitSwitch(s, locals)
case *ast.DoWhileStmt:
g.emitDoWhile(s, locals)
case *ast.ForStmt:
g.emitFor(s, locals)
case *ast.ForEachStmt:
g.emitForEach(s, locals)
case *ast.ExitStmt:
g.writeln("break")
case *ast.LoopStmt:
if g.curForLabel != "" {
// Inside FOR..NEXT: goto label before increment (continue would skip it)
g.writeln("goto " + g.curForLabel)
} else {
g.writeln("continue")
}
case *ast.MultiAssignStmt:
g.emitMultiAssign(s, locals)
case *ast.DeferStmt:
g.emitDefer(s, locals)
case *ast.VarDecl:
// LOCAL in mid-function or PRIVATE/PUBLIC
g.emitMidVarDecl(s, locals)
// xBase commands — generate calls to hbrdd WorkAreaManager
case *ast.UseCmd:
g.emitUseCmd(s, locals)
case *ast.GoCmd:
g.emitGoCmd(s)
case *ast.SkipCmd:
g.emitSkipCmd(s, locals)
case *ast.SeekCmd:
g.emitSeekCmd(s, locals)
case *ast.ReplaceCmd:
g.emitReplaceCmd(s, locals)
case *ast.AppendCmd:
if g.hoistedFields != nil {
// Use hoisted area variable
g.writeln("if _rarea != nil { _rarea.Append() }")
} else {
g.writeln("{ _wa := t.WA.(*hbrdd.WorkAreaManager)")
g.writeln("if _area := _wa.Current(); _area != nil { _area.Append() } }")
}
case *ast.DeleteCmd:
if g.hoistedDW || g.hoistedFields != nil {
g.writeln(fmt.Sprintf("if %s != nil { %s.Delete() }", g.hoistedAreaVar(), g.hoistedAreaVar()))
} else {
g.writeln("{ _wa := t.WA.(*hbrdd.WorkAreaManager)")
g.writeln("if _area := _wa.Current(); _area != nil { _area.Delete() } }")
}
case *ast.SelectCmd:
g.emitExpr(s.Area)
g.writeln("{ _wa := t.WA.(*hbrdd.WorkAreaManager); _v := t.Pop2()")
g.writeln("if _v.IsNumeric() { _wa.Select(int(_v.AsNumInt())) } else { _wa.Select(_v.AsString()) } }")
case *ast.IndexCmd:
g.writeln("{")
g.indent++
g.writeln("wa := t.WA.(*hbrdd.WorkAreaManager)")
g.writeln("if area := wa.Current(); area != nil {")
g.indent++
g.writeln("if idx, ok := area.(hbrdd.Indexer); ok {")
g.indent++
keyStr := exprToString(s.KeyExpr)
g.writeln(fmt.Sprintf("_keyExpr := %q", keyStr))
// File expression: if it contains a function call, evaluate at
// runtime — Harbour `INDEX ON ... TO ( cExpr )` semantics. Prior
// behavior was static exprToString which serialized calls like
// `Lower(cTable) + "_pk.ntx"` into the literal filename string.
// Detect via containsCall; preserve static path for simple
// `test.ntx` style identifiers.
if containsCall(s.File) {
g.emitExpr(s.File)
g.writeln("_file := t.Pop2().AsString()")
} else {
fileStr := exprToString(s.File)
g.writeln(fmt.Sprintf("_file := %q", fileStr))
}
forExpr := `""`
if s.ForCond != nil {
forExpr = fmt.Sprintf("%q", exprToString(s.ForCond))
}
// Emit compiled key evaluator as Go closure.
// This inlines the AST of the key expression into native Go code,
// eliminating per-record MacroEval string parsing + symbol lookup.
// In INDEX context, bare identifiers are FIELD names (not locals).
g.writeln("_keyFunc := func() hbrt.Value {")
g.indent++
g.emitIndexKeyExpr(s.KeyExpr)
g.writeln("return t.Pop2()")
g.indent--
g.writeln("}")
// Emit compiled FOR evaluator when the source has a FOR clause.
// Mirrors _keyFunc — zero runtime parsing, field-name identifier
// context, closure captures the Thread.
forFuncRef := "nil"
if s.ForCond != nil {
g.writeln("_forFunc := func() bool {")
g.indent++
g.emitIndexKeyExpr(s.ForCond)
g.writeln("return t.Pop2().AsBool()")
g.indent--
g.writeln("}")
forFuncRef = "_forFunc"
}
// Still set MacroEval fallback for evalKeyExprInner (used for keyLen sampling)
g.writeln("dbf.KeyEvalFunc = func(expr string) hbrt.Value { return t.MacroEval(expr) }")
g.writeln(fmt.Sprintf("idx.OrderCreate(hbrdd.OrderCreateParams{KeyExpr: _keyExpr, FilePath: _file, ForExpr: %s, TagName: %q, Unique: %v, Descending: %v, KeyFunc: _keyFunc, ForFunc: %s})",
forExpr, s.TagName, s.Unique, s.Descending, forFuncRef))
g.writeln("dbf.KeyEvalFunc = nil")
g.indent--
g.writeln("}")
g.indent--
g.writeln("}")
g.indent--
g.writeln("}")
case *ast.SetCmd:
upper := strings.ToUpper(s.Setting)
// Boolean SET toggles — call RTL Set function, no workarea needed
setFuncMap := map[string]string{
"DELETED": "SETDELETED",
"EXACT": "SETEXACT",
"SOFTSEEK": "SETSOFTSEEK",
"EXCLUSIVE": "SETEXCLUSIVE",
"FIXED": "SETFIXED",
"CANCEL": "SETCANCEL",
"BELL": "SETBELL",
"CONFIRM": "SETCONFIRM",
"INSERT": "SETINSERT",
"ESCAPE": "SETESCAPE",
"WRAP": "SETWRAP",
}
if funcName, ok := setFuncMap[upper]; ok {
onOff := strings.ToUpper(s.Extra)
if onOff == "ON" || onOff == "OFF" {
val := "true"
if onOff == "OFF" {
val = "false"
}
g.emitPushSymbol(funcName)
g.writeln("t.PushNil()")
g.writeln(fmt.Sprintf("t.PushBool(%s)", val))
g.writeln("t.Do(1)")
}
break
}
// Value SET commands — SET DATE/DECIMALS/EPOCH TO expr
valueFuncMap := map[string]string{
"DATE": "__SETDATEFORMAT",
"DECIMALS": "SETDECIMALS",
"EPOCH": "SETEPOCH",
}
if funcName, ok := valueFuncMap[upper]; ok && s.Expr != nil {
g.emitPushSymbol(funcName)
g.writeln("t.PushNil()")
g.emitExpr(s.Expr)
g.writeln("t.Do(1)")
break
}
// Workarea-specific SET commands
g.writeln("{")
g.indent++
g.writeln("wa := t.WA.(*hbrdd.WorkAreaManager)")
g.writeln("if area := wa.Current(); area != nil {")
g.indent++
switch upper {
case "FILTER":
if s.Expr != nil {
g.emitExpr(s.Expr)
g.writeln(`area.SetFilter(t.Pop2().AsString(), nil)`)
} else {
g.writeln("area.ClearFilter()")
}
case "ORDER":
if s.Expr != nil {
g.writeln("if idx, ok := area.(hbrdd.Indexer); ok {")
g.indent++
g.emitExpr(s.Expr)
g.writeln(`{ _ov := t.Pop2(); var _os string; if _ov.IsNumeric() { _os = hbrt.NtoS(_ov.AsNumInt()) } else { _os = _ov.AsString() }; idx.OrderListFocus(_os) }`)
g.indent--
g.writeln("}")
}
case "INDEX":
if s.Expr != nil {
fileStr := exprToString(s.Expr)
g.writeln("if idx, ok := area.(hbrdd.Indexer); ok {")
g.indent++
if fileStr != "" {
// SET INDEX TO a, b, c — split comma-separated file names
// and call OrderListAdd for each. Harbour loads all NTX
// files into the active index list.
clean := fileStr
if len(clean) >= 2 && clean[0] == '"' && clean[len(clean)-1] == '"' {
clean = clean[1 : len(clean)-1]
}
parts := strings.Split(clean, ",")
for _, p := range parts {
p = strings.TrimSpace(p)
if p != "" {
g.writeln(fmt.Sprintf(`idx.OrderListAdd(%q)`, p))
}
}
} else {
g.emitExpr(s.Expr)
g.writeln(`idx.OrderListAdd(t.Pop2().AsString())`)
}
g.indent--
g.writeln("}")
} else {
g.writeln("if idx, ok := area.(hbrdd.Indexer); ok { idx.OrderListClear() }")
}
default:
g.writeln(fmt.Sprintf("// SET %s: not yet implemented", upper))
}
g.indent--
g.writeln("}")
g.indent--
g.writeln("}")
case *ast.SeqStmt:
g.emitBeginSequence(s, locals)
case *ast.AtSayCmd:
g.emitAtSayCmd(s)
case *ast.AtGetCmd:
g.emitAtGetCmd(s, locals)
case *ast.AtSayGetCmd:
g.emitAtSayGetCmd(s, locals)
case *ast.ReadCmd:
g.emitReadCmd(s, locals)
default:
g.writeln(fmt.Sprintf("// WARN: unhandled statement type %T — skipped", stmt))
}
}
func (g *Generator) emitMidVarDecl(s *ast.VarDecl, locals localMap) {
for _, v := range s.Vars {
idx, found := locals[strings.ToUpper(v.Name)]
if !found {
maxIdx := 0
for _, i := range locals {
if i > maxIdx {
maxIdx = i
}
}
idx = maxIdx + 1
locals[strings.ToUpper(v.Name)] = idx
}
if v.Init != nil {
if _, isConst := g.constLocals[strings.ToUpper(v.Name)]; !isConst {
g.emitExpr(v.Init)
g.writeln(fmt.Sprintf("t.PopLocalFast(%d)", idx))
}
}
}
}
func (g *Generator) emitQOut(s *ast.QOutStmt, locals localMap) {
sym := "QOUT"
if s.IsQQ {
sym = "QQOUT"
}
g.emitPushSymbol(sym)
g.writeln("t.PushNil()")
for _, expr := range s.Exprs {
g.emitExpr(expr)
}
g.writeln(fmt.Sprintf("t.Function(%d)", len(s.Exprs)))
}
func (g *Generator) emitExprStmt(s *ast.ExprStmt, locals localMap) {
// Check if it's an assignment
if assign, ok := s.X.(*ast.AssignExpr); ok {
g.emitAssign(assign, locals)
return
}
// Check if it's a function call (discard result)
if call, ok := s.X.(*ast.CallExpr); ok {
g.emitCallAsStmt(call, locals)
return
}
// Bare identifier as statement (e.g., CLS, CLEAR) — treat as zero-arg function call
if ident, ok := s.X.(*ast.IdentExpr); ok {
if _, found := locals[strings.ToUpper(ident.Name)]; !found {
g.emitPushSymbol(strings.ToUpper(ident.Name))
g.writeln("t.PushNil()")
g.writeln("t.Do(0)")
return
}
}
// Postfix ++/--
if pf, ok := s.X.(*ast.PostfixExpr); ok {
// Local variable: n++
if ident, ok := pf.X.(*ast.IdentExpr); ok {
upper := strings.ToUpper(ident.Name)
if idx, found := locals[upper]; found {
if pf.Op == token.INC {
g.writeln(fmt.Sprintf("t.LocalAddInt(%d, 1)", idx))
} else {
g.writeln(fmt.Sprintf("t.LocalAddInt(%d, -1)", idx))
}
return
}
// STATIC variable: s_nPass++
if goVar, found := g.staticVars[upper]; found {
delta := "1"
if pf.Op == token.DEC {
delta = "-1"
}
g.writeln(fmt.Sprintf("{ _v := %s.AsNumInt() + %s; %s = hbrt.MakeInt(int(_v)) }", goVar, delta, goVar))
return
}
}
// Self field: ::field++
if send, ok := pf.X.(*ast.SendExpr); ok {
if _, isSelf := send.Object.(*ast.SelfExpr); isSelf {
fieldName := strings.ToUpper(send.Method)
g.writeln(fmt.Sprintf("t.PushSelfField(%q)", fieldName))
if pf.Op == token.INC {
g.writeln("t.PushInt(1)")
g.writeln("t.Plus()")
} else {
g.writeln("t.PushInt(1)")
g.writeln("t.Minus()")
}
g.writeln(fmt.Sprintf("t.SetSelfField(%q)", fieldName))
return
}
}
}
// General expression (result on stack, pop it)
g.emitExpr(s.X)
g.writeln("t.Pop()")
}
func (g *Generator) emitAssign(a *ast.AssignExpr, locals localMap) {
// Check for arr[idx] := value (array index assignment)
if idx, ok := a.Left.(*ast.IndexExpr); ok {
if a.Op == token.ASSIGN {
g.emitExpr(idx.X) // array
g.emitExpr(idx.Index) // index
g.emitExpr(a.Right) // value
g.writeln("t.ArrayPop()") // set array[index] = value
return
}
}
// Check for obj:field := value (object field assignment)
if send, ok := a.Left.(*ast.SendExpr); ok {
_, isSelf := send.Object.(*ast.SelfExpr)
if isSelf {
fieldName := strings.ToUpper(send.Method)
switch a.Op {
case token.ASSIGN:
g.emitExpr(a.Right)
g.writeln(fmt.Sprintf("t.SetSelfField(%q)", fieldName))
case token.PLUSEQ:
g.writeln(fmt.Sprintf("t.PushSelfField(%q)", fieldName))
g.emitExpr(a.Right)
g.writeln("t.Plus()")
g.writeln(fmt.Sprintf("t.SetSelfField(%q)", fieldName))
case token.MINUSEQ:
g.writeln(fmt.Sprintf("t.PushSelfField(%q)", fieldName))
g.emitExpr(a.Right)
g.writeln("t.Minus()")
g.writeln(fmt.Sprintf("t.SetSelfField(%q)", fieldName))
case token.STAREQ:
g.writeln(fmt.Sprintf("t.PushSelfField(%q)", fieldName))
g.emitExpr(a.Right)
g.writeln("t.Mult()")
g.writeln(fmt.Sprintf("t.SetSelfField(%q)", fieldName))
case token.SLASHEQ:
g.writeln(fmt.Sprintf("t.PushSelfField(%q)", fieldName))
g.emitExpr(a.Right)
g.writeln("t.Divide()")
g.writeln(fmt.Sprintf("t.SetSelfField(%q)", fieldName))
default:
g.emitExpr(a.Right)
g.writeln(fmt.Sprintf("t.SetSelfField(%q)", fieldName))
}
return
}
// Non-self: obj:field := value → obj:_FIELD(value)
if a.Op == token.ASSIGN {
g.emitExpr(send.Object)
g.emitExpr(a.Right)
g.writeln(fmt.Sprintf("t.Send(%q, 1)", "_"+strings.ToUpper(send.Method)))
g.writeln("t.Pop() // discard setter result")
return
}
}
// Check for alias->field := value (FIELD->NAME := value)
if aliasExpr, ok := a.Left.(*ast.AliasExpr); ok {
if aliasIdent, ok2 := aliasExpr.Alias.(*ast.IdentExpr); ok2 {
if fieldIdent, ok3 := aliasExpr.Field.(*ast.IdentExpr); ok3 {
upper := strings.ToUpper(aliasIdent.Name)
// `M->name := v` / `MEMVAR->name := v` are memvar writes,
// not workarea field writes.
if upper == "M" || upper == "MEMVAR" {
g.emitExpr(a.Right)
g.writeln(fmt.Sprintf(`t.PopMemvar(%q)`, fieldIdent.Name))
return
}
g.emitExpr(a.Right)
g.writeln(fmt.Sprintf(`{ _wa := t.WA.(*hbrdd.WorkAreaManager); _wa.SetAliasField(%q, %q, t.Pop2()) }`, aliasIdent.Name, fieldIdent.Name))
return
}
}
}
if ident, ok := a.Left.(*ast.IdentExpr); ok {
if idx, found := locals[strings.ToUpper(ident.Name)]; found {
switch a.Op {
case token.ASSIGN:
// Peephole: `x := x + <expr>` / `x := x - <expr>` →
// LocalAdd. Same result as `x += <expr>` but lets the
// PRG side use the explicit form without penalty.
if be, ok := a.Right.(*ast.BinaryExpr); ok &&
(be.Op == token.PLUS || be.Op == token.MINUS) {
if lid, isIdent := be.Left.(*ast.IdentExpr); isIdent {
if selfIdx, found := locals[strings.ToUpper(lid.Name)]; found && selfIdx == idx {
g.emitExpr(be.Right)
if be.Op == token.MINUS {
g.writeln("t.Negate()")
}
g.writeln(fmt.Sprintf("t.LocalAdd(%d)", idx))
return
}
}
}
g.emitExpr(a.Right)
g.writeln(fmt.Sprintf("t.PopLocalFast(%d)", idx))
case token.PLUSEQ:
g.emitExpr(a.Right)
g.writeln(fmt.Sprintf("t.LocalAdd(%d)", idx))
case token.MINUSEQ:
g.emitExpr(a.Right)
g.writeln("t.Negate()")
g.writeln(fmt.Sprintf("t.LocalAdd(%d)", idx))
default:
// General compound: push local, push right, op, pop local
g.writeln(fmt.Sprintf("t.PushLocalFast(%d)", idx))
g.emitExpr(a.Right)
g.emitBinaryOp(a.Op)
g.writeln(fmt.Sprintf("t.PopLocalFast(%d)", idx))
}
return
}
// Check module-level or function-level STATIC variable
upper := strings.ToUpper(ident.Name)
if goVar, found := g.staticVars[upper]; found {
switch a.Op {
case token.ASSIGN:
g.emitExpr(a.Right)
g.writeln(fmt.Sprintf("%s = t.Pop2()", goVar))
case token.PLUSEQ:
g.writeln(fmt.Sprintf("t.PushValue(%s)", goVar))
g.emitExpr(a.Right)
g.writeln("t.Plus()")
g.writeln(fmt.Sprintf("%s = t.Pop2()", goVar))
case token.MINUSEQ:
g.writeln(fmt.Sprintf("t.PushValue(%s)", goVar))
g.emitExpr(a.Right)
g.writeln("t.Minus()")
g.writeln(fmt.Sprintf("%s = t.Pop2()", goVar))
case token.STAREQ:
g.writeln(fmt.Sprintf("t.PushValue(%s)", goVar))
g.emitExpr(a.Right)
g.writeln("t.Mult()")
g.writeln(fmt.Sprintf("%s = t.Pop2()", goVar))
case token.SLASHEQ:
g.writeln(fmt.Sprintf("t.PushValue(%s)", goVar))
g.emitExpr(a.Right)
g.writeln("t.Divide()")
g.writeln(fmt.Sprintf("%s = t.Pop2()", goVar))
default:
g.writeln(fmt.Sprintf("t.PushValue(%s)", goVar))
g.emitExpr(a.Right)
g.emitBinaryOp(a.Op)
g.writeln(fmt.Sprintf("%s = t.Pop2()", goVar))
}
return
}
}
// Fallback: general assignment via stack
g.emitExpr(a.Right)
g.writeln("// WARN: complex assignment target — simplified")
g.writeln("t.Pop()")
}
func (g *Generator) emitCallAsStmt(call *ast.CallExpr, locals localMap) {
if ident, ok := call.Func.(*ast.IdentExpr); ok {
g.emitPushSymbol(strings.ToUpper(ident.Name))
} else {
g.emitExpr(call.Func)
}
g.writeln("t.PushNil()")
for _, arg := range call.Args {
g.emitExpr(arg)
}
g.writeln(fmt.Sprintf("t.Do(%d)", len(call.Args)))
}
// boolLiteralValue returns (value, true) if e reduces to a .T./.F.
// literal at compile time. Sees through an outer `.NOT.` so expressions
// like `!.F.` also collapse. Used by emitIf to skip dead branches and
// by the AND/OR short-circuit emitter.
func boolLiteralValue(e ast.Expr) (bool, bool) {
if u, ok := e.(*ast.UnaryExpr); ok && u.Op == token.NOT {
if v, ok := boolLiteralValue(u.X); ok {
return !v, true
}
return false, false
}
lit, ok := e.(*ast.LiteralExpr)
if !ok {
return false, false
}
switch lit.Kind {
case token.TRUE:
return true, true
case token.FALSE:
return false, true
}
return false, false
}
func (g *Generator) emitIf(s *ast.IfStmt, locals localMap) {
// Dead-branch elimination for literal conditions. An IF .T. collapses
// to its body; an IF .F. collapses to its first live ELSEIF/ELSE.
// We resolve the main Cond here and recurse on the remainder if it
// turns into a new IF chain.
if v, ok := boolLiteralValue(s.Cond); ok {
if v {
for _, stmt := range s.Body {
g.emitStmt(stmt, locals)
}
return
}
// IF .F. — scan ElseIfs for first non-.F. branch.
for i, ei := range s.ElseIfs {
if v2, ok2 := boolLiteralValue(ei.Cond); ok2 {
if v2 {
for _, stmt := range ei.Body {
g.emitStmt(stmt, locals)
}
return
}
continue // ELSEIF .F. — dead, skip
}
// Non-literal ELSEIF becomes the new IF head.
newIf := &ast.IfStmt{
IfPos: ei.ElseIfPos,
Cond: ei.Cond,
Body: ei.Body,
ElseIfs: s.ElseIfs[i+1:],
ElseBody: s.ElseBody,
}
g.emitIf(newIf, locals)
return
}
// All ElseIfs were .F. — only ELSE body remains.
for _, stmt := range s.ElseBody {
g.emitStmt(stmt, locals)
}
return
}
// Main cond is dynamic. Still filter dead ELSEIFs (.F. removed;
// an ELSEIF .T. truncates the chain and becomes the ELSE).
elseIfs := s.ElseIfs
elseBody := s.ElseBody
if len(elseIfs) > 0 {
filtered := make([]*ast.ElseIfClause, 0, len(elseIfs))
for _, ei := range elseIfs {
if v, ok := boolLiteralValue(ei.Cond); ok {
if v {
// ELSEIF .T. — chain stops here; body becomes ELSE.
elseBody = ei.Body
elseIfs = filtered
goto emit
}
continue // ELSEIF .F. — dead
}
filtered = append(filtered, ei)
}
elseIfs = filtered
}
emit:
g.emitExpr(s.Cond)
g.writeln("if t.PopLogical() {")
g.indent++
for _, stmt := range s.Body {
g.emitStmt(stmt, locals)
}
g.indent--
for _, ei := range elseIfs {
g.writeIndent()
g.write("} else {\n")
g.indent++
g.emitExpr(ei.Cond)
g.writeln("if t.PopLogical() {")
g.indent++
for _, stmt := range ei.Body {
g.emitStmt(stmt, locals)
}
g.indent--
}
if len(elseBody) > 0 {
g.writeln("} else {")
g.indent++
for _, stmt := range elseBody {
g.emitStmt(stmt, locals)
}
g.indent--
}
g.writeln("}")
// Close nested elseif braces
for range elseIfs {
g.writeln("}")
}
}
func (g *Generator) emitDoWhile(s *ast.DoWhileStmt, locals localMap) {
// DO WHILE .F. — body is unreachable; emit nothing.
if v, ok := boolLiteralValue(s.Cond); ok && !v {
return
}
// Detect RDD commands in body for WA hoisting
hasRDD := hasRDDCommands(s.Body)
safeToHoist := hasRDD && !hasWorkareaChange(s.Body)
if safeToHoist && g.hoistedFields == nil {
g.writeln("{")
g.indent++
g.writeln("_dwa := t.WA.(*hbrdd.WorkAreaManager)")
g.writeln("_darea := _dwa.Current()")
g.hoistedDW = true
}
g.writeln("for {")
g.indent++
// DO WHILE .T. — the idiomatic infinite loop. Skip the per-iteration
// PushBool/PopLogical; exit only through EXIT / LOOP / RETURN.
if v, ok := boolLiteralValue(s.Cond); !ok || !v {
g.emitExpr(s.Cond)
g.writeln("if !t.PopLogical() { break }")
}
for _, stmt := range s.Body {
g.emitStmt(stmt, locals)
}
g.indent--
g.writeln("}")
if safeToHoist && g.hoistedDW {
g.hoistedDW = false
g.indent--
g.writeln("}")
}
}
// hasRDDCommands checks if any statement is an RDD operation.
func hasRDDCommands(stmts []ast.Stmt) bool {
for _, s := range stmts {
switch s.(type) {
case *ast.SkipCmd, *ast.GoCmd, *ast.SeekCmd,
*ast.ReplaceCmd, *ast.AppendCmd, *ast.DeleteCmd:
return true
}
}
return false
}
// hasWorkareaChange checks for USE/SELECT that would invalidate cached area.
func hasWorkareaChange(stmts []ast.Stmt) bool {
for _, s := range stmts {
switch v := s.(type) {
case *ast.UseCmd, *ast.SelectCmd:
return true
case *ast.IfStmt:
if hasWorkareaChange(v.Body) || hasWorkareaChange(v.ElseBody) {
return true
}
case *ast.DoWhileStmt:
if hasWorkareaChange(v.Body) {
return true
}
}
}
return false
}
// collectSymbols scans AST for all symbol names referenced by function calls.
// Returns unique names for hoisting FindSymbol to function prologue.
func collectSymbols(stmts []ast.Stmt) []string {
seen := map[string]bool{}
var names []string
var walk func([]ast.Stmt)
var walkExpr func(ast.Expr)
walkExpr = func(e ast.Expr) {
if e == nil {
return
}
switch v := e.(type) {
case *ast.CallExpr:
if ident, ok := v.Func.(*ast.IdentExpr); ok {
name := strings.ToUpper(ident.Name)
if !seen[name] {
seen[name] = true
names = append(names, name)
}
}
for _, a := range v.Args {
walkExpr(a)
}
case *ast.BinaryExpr:
walkExpr(v.Left)
walkExpr(v.Right)
case *ast.UnaryExpr:
walkExpr(v.X)
}
}
walk = func(stmts []ast.Stmt) {
for _, s := range stmts {
switch v := s.(type) {
case *ast.ExprStmt:
walkExpr(v.X)
case *ast.ReturnStmt:
if v.Value != nil {
walkExpr(v.Value)
}
case *ast.IfStmt:
walkExpr(v.Cond)
walk(v.Body)
walk(v.ElseBody)
case *ast.ForStmt:
walk(v.Body)
case *ast.ForEachStmt:
walk(v.Body)
case *ast.DoWhileStmt:
walkExpr(v.Cond)
walk(v.Body)
case *ast.SeqStmt:
walk(v.Body)
walk(v.RecoverBody)
case *ast.SwitchStmt:
for _, c := range v.Cases {
walk(c.Body)
}
}
}
}
walk(stmts)
return names
}
// collectReplaceFields scans statements for REPLACE field names.
// Returns nil if unsafe to hoist (USE/SELECT/CLOSE found).
func collectReplaceFields(stmts []ast.Stmt) []string {
seen := map[string]bool{}
var fields []string
for _, s := range stmts {
switch v := s.(type) {
case *ast.ReplaceCmd:
for _, rf := range v.Fields {
if ident, ok := rf.Field.(*ast.IdentExpr); ok {
name := ident.Name
if !seen[name] {
seen[name] = true
fields = append(fields, name)
}
}
}
case *ast.UseCmd, *ast.SelectCmd:
return nil // workarea may change — unsafe to hoist
case *ast.IfStmt:
// Check nested blocks
if sub := collectReplaceFields(v.Body); sub == nil {
return nil
}
if sub := collectReplaceFields(v.ElseBody); sub == nil {
return nil
}
case *ast.DoWhileStmt:
if sub := collectReplaceFields(v.Body); sub == nil {
return nil
}
}
}
return fields
}
// hasAppendInBody checks if any APPEND command exists in the statements.
func hasAppendInBody(stmts []ast.Stmt) bool {
for _, s := range stmts {
if _, ok := s.(*ast.AppendCmd); ok {
return true
}
}
return false
}
func (g *Generator) emitFor(s *ast.ForStmt, locals localMap) {
idx, found := locals[strings.ToUpper(s.Var)]
if !found {
g.writeln("// ERROR: FOR variable not found in locals")
return
}
// i := start
g.emitExpr(s.Start)
g.writeln(fmt.Sprintf("t.PopLocalFast(%d)", idx))
// Detect step direction for comparison
isNegStep := false
if s.Step != nil {
if lit, ok := s.Step.(*ast.LiteralExpr); ok {
if lit.Kind == token.INT && len(lit.Value) > 0 && lit.Value[0] == '-' {
isNegStep = true
}
}
if un, ok := s.Step.(*ast.UnaryExpr); ok && un.Op == token.MINUS {
isNegStep = true
}
}
// Optimization: hoist WA/FieldIndex lookups outside FOR loop
// if body contains REPLACE and no USE/SELECT (safe to cache).
rddFields := collectReplaceFields(s.Body)
hoistRDD := len(rddFields) > 0 && hasAppendInBody(s.Body)
if hoistRDD {
g.writeln("{")
g.indent++
g.writeln("_rwa := t.WA.(*hbrdd.WorkAreaManager)")
g.writeln("_rarea := _rwa.Current()")
g.writeln("var _rdbf *dbf.DBFArea")
g.writeln("if _rarea != nil { _rdbf, _ = _rarea.(*dbf.DBFArea) }")
// Pre-compute field indexes
for i, fname := range rddFields {
g.writeln(fmt.Sprintf("var _rfi%d int = -1", i))
g.writeln(fmt.Sprintf("if _rdbf != nil { _rfi%d = _rdbf.FieldIndex(%q) }", i, fname))
}
g.hoistedFields = rddFields // store for emitReplaceCmdHoisted
}
g.writeln("for {")
g.indent++
// Comparison: fused opcode when limit is literal int (most common).
// Also see through const-propagated LOCALs: `LOCAL n := 100; FOR i := 1
// TO n` should hit the same fast path as a bare literal.
toLit, _ := s.To.(*ast.LiteralExpr)
if toLit == nil {
if id, ok := s.To.(*ast.IdentExpr); ok {
if l, ok2 := g.constLocals[strings.ToUpper(id.Name)]; ok2 {
toLit = l
}
}
}
if lit := toLit; lit != nil && lit.Kind == token.INT {
if isNegStep {
g.writeln(fmt.Sprintf("if !t.LocalGreaterEqualInt(%d, %s) { break }", idx, lit.Value))
} else {
g.writeln(fmt.Sprintf("if !t.LocalLessEqualInt(%d, %s) { break }", idx, lit.Value))
}
} else {
// General case: stack-based comparison
g.writeln(fmt.Sprintf("t.PushLocalFast(%d)", idx))
g.emitExpr(s.To)
if isNegStep {
g.writeln("t.GreaterEqual()")
} else {
g.writeln("t.LessEqual()")
}
g.writeln("if !t.PopLogical() { break }")
}
// Track FOR loop depth so LOOP can use goto instead of continue.
// Only emit label if LOOP is present in the body (Go rejects unused labels).
hasLoop := bodyHasLoop(s.Body)
forLabel := ""
prevForLabel := g.curForLabel
if hasLoop {
forLabel = fmt.Sprintf("_for_next_%d", g.forLabelSeq)
g.forLabelSeq++
g.curForLabel = forLabel
} else {
g.curForLabel = ""
}
// body
for _, stmt := range s.Body {
g.emitStmt(stmt, locals)
}
// Label for LOOP to jump to (skipping continue which would miss increment)
if hasLoop {
g.writeln(forLabel + ":")
}
// i += step (default 1)
if s.Step != nil {
g.emitExpr(s.Step)
g.writeln(fmt.Sprintf("t.LocalAdd(%d)", idx))
} else {
g.writeln(fmt.Sprintf("t.LocalAddInt(%d, 1)", idx))
}
g.curForLabel = prevForLabel
g.indent--
g.writeln("}")
// Close hoisting block
if hoistRDD {
g.hoistedFields = nil
g.indent--
g.writeln("}")
}
}
// bodyHasLoop checks if any statement in the body is a LOOP.
// Only checks the immediate level — LOOP inside nested FOR/DO WHILE is irrelevant.
func bodyHasLoop(stmts []ast.Stmt) bool {
for _, s := range stmts {
if hasLoopStmt(s) {
return true
}
}
return false
}
func hasLoopStmt(s ast.Stmt) bool {
switch s := s.(type) {
case *ast.LoopStmt:
return true
case *ast.IfStmt:
for _, st := range s.Body {
if hasLoopStmt(st) {
return true
}
}
for _, ei := range s.ElseIfs {
for _, st := range ei.Body {
if hasLoopStmt(st) {
return true
}
}
}
for _, st := range s.ElseBody {
if hasLoopStmt(st) {
return true
}
}
case *ast.SeqStmt:
for _, st := range s.Body {
if hasLoopStmt(st) {
return true
}
}
for _, st := range s.RecoverBody {
if hasLoopStmt(st) {
return true
}
}
case *ast.SwitchStmt:
for _, c := range s.Cases {
for _, st := range c.Body {
if hasLoopStmt(st) {
return true
}
}
}
for _, st := range s.Otherwise {
if hasLoopStmt(st) {
return true
}
}
// Do NOT recurse into ForStmt/DoWhileStmt — nested LOOP is for the inner loop
}
return false
}
func (g *Generator) emitSwitch(s *ast.SwitchStmt, locals localMap) {
// Wrap the whole thing in a one-iteration `for` so:
// 1. `_sw` stays scoped to the switch.
// 2. `EXIT` inside a CASE emits `break` and targets this loop,
// matching Harbour SWITCH semantics (EXIT terminates SWITCH).
// 3. Empty SWITCH (`SWITCH x ENDSWITCH`, common in conditional-
// compile test files) stays valid Go.
g.writeln("for {")
g.indent++
g.emitExpr(s.Expr)
g.writeln("_sw := t.Pop2()")
g.writeln("_ = _sw") // silence unused-var warning when no cases reference it
// Use the runtime's type-aware Equal() instead of coercing to
// NumInt. The AsNumInt() path broke every non-numeric SWITCH
// (strings, dates, logicals): "ABC".AsNumInt() returns 0, so
// `SWITCH cType CASE "C"` folded every arm to the same false.
// Each CASE emits an independent `if !_swHit { push _sw; push
// caseVal; t.Equal(); if t.PopLogical() { body; _swHit = true } }`
// block so the stack is balanced even when a body executes EXIT /
// RETURN mid-case.
first := true
for _, c := range s.Cases {
if first {
g.writeln("_swHit := false")
first = false
}
g.writeln("if !_swHit {")
g.indent++
g.writeln("t.PushValue(_sw)")
g.emitExpr(c.Value)
g.writeln("t.Equal()")
g.writeln("if t.PopLogical() {")
g.indent++
g.writeln("_swHit = true")
for _, stmt := range c.Body {
g.emitStmt(stmt, locals)
}
g.indent--
g.writeln("}")
g.indent--
g.writeln("}")
}
if len(s.Otherwise) > 0 {
if first {
// No CASE arms — emit the OTHERWISE body as-is.
for _, stmt := range s.Otherwise {
g.emitStmt(stmt, locals)
}
} else {
g.writeln("if !_swHit {")
g.indent++
for _, stmt := range s.Otherwise {
g.emitStmt(stmt, locals)
}
g.indent--
g.writeln("}")
}
}
if !first {
g.writeln("_ = _swHit") // guard against bodies that never read it
}
// Always break out of our one-iteration `for` wrapper, regardless
// of which (or no) case ran.
g.writeln("break")
g.indent--
g.writeln("}")
}
func (g *Generator) emitBeginSequence(s *ast.SeqStmt, locals localMap) {
// BEGIN SEQUENCE → Go's panic/recover.
// Catches both *HbError (runtime errors) and BreakValue (Break() calls).
// BreakValue is defined in hbrtl, but we detect it via duck typing
// to avoid import cycles.
g.writeln("{ // BEGIN SEQUENCE")
g.indent++
g.writeln("_seqErr := func() (_recoverVal interface{}) {")
g.indent++
g.writeln("defer func() {")
g.indent++
g.writeln("if r := recover(); r != nil {")
g.indent++
g.writeln("_recoverVal = r")
g.indent--
g.writeln("}")
g.indent--
g.writeln("}()")
// Body
for _, stmt := range s.Body {
g.emitStmt(stmt, locals)
}
g.writeln("return nil")
g.indent--
g.writeln("}()")
// RECOVER
if len(s.RecoverBody) > 0 {
g.writeln("if _seqErr != nil {")
g.indent++
if s.RecoverVar != "" {
if idx, found := locals[strings.ToUpper(s.RecoverVar)]; found {
// Extract the value from the recovered panic:
// *HbError → error description string
// BreakValue (has .Value field) → the Break() argument
// other → string representation
g.writeln(fmt.Sprintf(`{ // RECOVER USING %s`, s.RecoverVar))
g.indent++
g.writeln(`switch _sv := _seqErr.(type) {`)
g.writeln(`case *hbrt.HbError:`)
g.writeln(fmt.Sprintf(` t.SetLocalFast(%d, hbrt.MakeString(_sv.Error()))`, idx))
g.writeln(`default:`)
// For BreakValue, use reflection-free approach: check if
// the type has a Value field via a local interface.
g.writeln(` type hasValue interface{ GetValue() hbrt.Value }`)
g.writeln(` if bv, ok := _sv.(hasValue); ok {`)
g.writeln(fmt.Sprintf(` t.SetLocalFast(%d, bv.GetValue())`, idx))
g.writeln(` } else {`)
g.writeln(fmt.Sprintf(` t.SetLocalFast(%d, hbrt.MakeString("error"))`, idx))
g.writeln(` }`)
g.writeln(`}`)
g.indent--
g.writeln(`}`)
}
}
for _, stmt := range s.RecoverBody {
g.emitStmt(stmt, locals)
}
g.indent--
g.writeln("}")
} else {
g.writeln("_ = _seqErr")
}
g.indent--
g.writeln("} // END SEQUENCE")
}
func (g *Generator) emitForEach(s *ast.ForEachStmt, locals localMap) {
varIdx, found := locals[strings.ToUpper(s.Var)]
if !found {
g.writeln("// ERROR: FOR EACH variable not in locals")
return
}
// Evaluate collection once; the emitted loop dispatches on the
// runtime type so the same FOR EACH works for arrays, hashes, and
// strings (Harbour semantics). Hash iteration yields values — the
// user can get at keys via hb_HKeys(h) before the loop. String
// iteration walks byte-by-byte, matching Harbour's "character"
// enumeration via single-byte sub-strings.
g.emitExpr(s.Collection)
g.writeln("{ _feArr := t.Pop2()")
// Array branch — the common case, kept as-is for zero overhead
// when the collection is an array.
g.writeln("if _feArr.IsArray() {")
g.indent++
g.writeln("_feItems := _feArr.AsArray().Items")
g.writeln("for _feI := 0; _feI < len(_feItems); _feI++ {")
g.indent++
g.writeln(fmt.Sprintf("t.SetLocalFast(%d, _feItems[_feI])", varIdx))
for _, stmt := range s.Body {
g.emitStmt(stmt, locals)
}
g.indent--
g.writeln("}")
g.indent--
// Hash branch — iterate values in insertion order. HbHash keeps
// Keys/Values parallel slices in insertion order (see hash_helpers.go
// appendPair); the Order slice is only populated by a handful of
// Go-RTL builders and is empty for hash literals / hb_Hash(), so we
// walk Values directly.
g.writeln("} else if _feArr.IsHash() {")
g.indent++
g.writeln("_feH := _feArr.AsHash()")
g.writeln("for _feI := 0; _feI < len(_feH.Values); _feI++ {")
g.indent++
g.writeln(fmt.Sprintf("t.SetLocalFast(%d, _feH.Values[_feI])", varIdx))
for _, stmt := range s.Body {
g.emitStmt(stmt, locals)
}
g.indent--
g.writeln("}")
g.indent--
// String branch — iterate bytes as single-character substrings.
g.writeln("} else if _feArr.IsString() {")
g.indent++
g.writeln("_feStr := _feArr.AsString()")
g.writeln("for _feI := 0; _feI < len(_feStr); _feI++ {")
g.indent++
g.writeln(fmt.Sprintf("t.SetLocalFast(%d, hbrt.MakeString(string(_feStr[_feI])))", varIdx))
for _, stmt := range s.Body {
g.emitStmt(stmt, locals)
}
g.indent--
g.writeln("}")
g.indent--
g.writeln("} }")
}
// --- Expression emission ---
// Each emitExpr leaves one value on the stack.
// emitMultiAssign: a, b := Func() or a, b := x, y
func (g *Generator) emitMultiAssign(s *ast.MultiAssignStmt, locals localMap) {
if len(s.Values) == 1 {
// Single RHS: a, b := Func() → call function, unpack array result
g.emitExpr(s.Values[0])
g.writeln("{")
g.indent++
g.writeln("_mr := t.Pop2()")
g.writeln("if _mr.IsArray() {")
g.indent++
g.writeln("_arr := _mr.AsArray()")
for i, name := range s.Targets {
if name == "_" {
continue
}
idx := locals[strings.ToUpper(name)]
if idx > 0 {
g.writeln(fmt.Sprintf("if %d < len(_arr.Items) { t.SetLocalFast(%d, _arr.Items[%d]) }", i, idx, i))
}
}
g.indent--
g.writeln("} else {")
g.indent++
// Not array — assign first target, rest get NIL
if s.Targets[0] != "_" {
idx := locals[strings.ToUpper(s.Targets[0])]
if idx > 0 {
g.writeln(fmt.Sprintf("t.SetLocalFast(%d, _mr)", idx))
}
}
g.indent--
g.writeln("}")
g.indent--
g.writeln("}")
} else {
// Multiple RHS: a, b := x, y (parallel assign)
// Evaluate all RHS first, then assign
for i, val := range s.Values {
g.emitExpr(val)
g.writeln(fmt.Sprintf("_mv%d := t.Pop2()", i))
}
for i, name := range s.Targets {
if name == "_" || i >= len(s.Values) {
continue
}
idx := locals[strings.ToUpper(name)]
if idx > 0 {
g.writeln(fmt.Sprintf("t.SetLocalFast(%d, _mv%d)", idx, i))
}
}
}
}
// emitDefer: DEFER expr → Go defer
func (g *Generator) emitDefer(s *ast.DeferStmt, locals localMap) {
g.writeln("defer func() {")
g.indent++
g.emitExpr(s.Call)
g.writeln("t.Pop() // discard defer result")
g.indent--
g.writeln("}()")
}