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>
1352 lines
37 KiB
Go
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("}()")
|
|
}
|