Audit follow-up after Wave 1's pcode `+=` fix surfaced a parallel
class of silent miscompiles in the *gengo* (native-Go) emit path.
Three real bugs hiding behind happy-path test coverage:
* `arr[i] += x` was ASSIGN-only — the IndexExpr branch returned
after emitting `arr[i] := x`, dropping the original element.
Now: PushArray + Push index, ArrayPush to read, fold with RHS,
re-do PushArray + index, ArrayPop to store.
* `alias->field += x` (and the M-> / MEMVAR-> namespace variants)
were ASSIGN-only too. Same shape of bug — `x->v += 7` compiled
as `x->v := 7`. Compound branch reads via PushAliasField (or
PushMemvar for M->), folds, stores via SetAliasField (or
PopMemvar).
* PRIVATE / PUBLIC mid-function declarations were treated as
extra LOCAL slots. emitMidVarDecl extended `locals` past the
function's declared count and emitted `PopLocalFast(idx)` for
the init. The slot didn't exist at runtime, so the init either
silently scribbled past the frame (small N) or panicked with
"local variable index out of range" once exercised. New logic:
PRIVATE/PUBLIC declarations bypass the locals table and emit
`PopMemvar(name)` for the init expression. The runtime auto-
creates the memvar.
* Memvar assignment fallback. After the LOCAL/STATIC checks miss
in emitAssign, the bottom path used to be a one-line WARN that
emitted RHS + `Pop()` — silently discarding the value. PRIVATE
pSum stayed at its initial value forever. Now: ASSIGN goes
through PopMemvar; compound forms read via PushMemvar, fold,
write back via PopMemvar.
Test fixture (tests/std_ch/test_compound_lhs.prg) covers all four
shapes. The std.ch runner picks it up so the regression suite now
stands at 15/15.
Other gates green:
go test ./... : PASS
FiveSql2 SQL:1999 : 43/43
Harbour compat : 56/56
std.ch suite : 15/15
FRB suite : 5/5
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1421 lines
39 KiB
Go
1421 lines
39 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) {
|
|
// PRIVATE / PUBLIC live in the runtime memvar namespace, NOT in
|
|
// the function's local slot table. Without this distinction the
|
|
// declaration was silently registered as a phantom LOCAL slot
|
|
// past the function's declared LOCAL count: subsequent reads/
|
|
// writes via LocalAdd / PopLocalFast then panicked with "local
|
|
// variable index out of range" (or, worse, silently scribbled
|
|
// past the allocated frame).
|
|
if s.Scope == ast.ScopePrivate || s.Scope == ast.ScopePublic {
|
|
for _, v := range s.Vars {
|
|
if v.Init == nil {
|
|
// Bare PRIVATE/PUBLIC declaration without init —
|
|
// runtime auto-creates the memvar on first read/write.
|
|
continue
|
|
}
|
|
g.emitExpr(v.Init)
|
|
g.writeln(fmt.Sprintf(`t.PopMemvar(%q)`, v.Name))
|
|
}
|
|
return
|
|
}
|
|
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] := / += / -= / etc. (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
|
|
}
|
|
// Compound: read current arr[idx], fold with RHS, store back.
|
|
// Without this, `arr[i] += x` was silently compiled as
|
|
// `arr[i] := x` — the original element discarded. Same fix
|
|
// pattern as the LOCAL / STATIC compound branches below.
|
|
g.emitExpr(idx.X)
|
|
g.emitExpr(idx.Index)
|
|
g.writeln("t.ArrayPush()") // push arr[index] for read
|
|
g.emitExpr(a.Right)
|
|
g.emitBinaryOp(a.Op)
|
|
// Stack now: [folded value]. Re-push X/index to set.
|
|
g.writeln("_v := t.Pop2()")
|
|
g.emitExpr(idx.X)
|
|
g.emitExpr(idx.Index)
|
|
g.writeln("t.PushValue(_v)")
|
|
g.writeln("t.ArrayPop()")
|
|
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 := / += / etc. (workarea field assign)
|
|
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" {
|
|
if a.Op == token.ASSIGN {
|
|
g.emitExpr(a.Right)
|
|
g.writeln(fmt.Sprintf(`t.PopMemvar(%q)`, fieldIdent.Name))
|
|
return
|
|
}
|
|
// Compound: M-> is the memvar namespace.
|
|
g.writeln(fmt.Sprintf(`t.PushMemvar(%q)`, fieldIdent.Name))
|
|
g.emitExpr(a.Right)
|
|
g.emitBinaryOp(a.Op)
|
|
g.writeln(fmt.Sprintf(`t.PopMemvar(%q)`, fieldIdent.Name))
|
|
return
|
|
}
|
|
if a.Op == token.ASSIGN {
|
|
g.emitExpr(a.Right)
|
|
g.writeln(fmt.Sprintf(`{ _wa := t.WA.(*hbrdd.WorkAreaManager); _wa.SetAliasField(%q, %q, t.Pop2()) }`, aliasIdent.Name, fieldIdent.Name))
|
|
return
|
|
}
|
|
// Compound: read current alias->field, fold with RHS,
|
|
// write back. `x->v += 7` used to compile as `x->v := 7`
|
|
// (silent miscompile) — the original field value got
|
|
// discarded.
|
|
g.writeln(fmt.Sprintf(`t.PushAliasField(%q, %q)`, aliasIdent.Name, fieldIdent.Name))
|
|
g.emitExpr(a.Right)
|
|
g.emitBinaryOp(a.Op)
|
|
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
|
|
}
|
|
// Memvar fallback (PRIVATE / PUBLIC / unresolved IDENT) —
|
|
// runtime auto-creates the memvar on assign. Without this
|
|
// the bottom WARN fallback dropped the RHS and silently
|
|
// did nothing, so `PRIVATE pSum := 50 ; pSum += 25` left
|
|
// pSum at 50.
|
|
if a.Op == token.ASSIGN {
|
|
g.emitExpr(a.Right)
|
|
g.writeln(fmt.Sprintf(`t.PopMemvar(%q)`, ident.Name))
|
|
return
|
|
}
|
|
g.writeln(fmt.Sprintf(`t.PushMemvar(%q)`, ident.Name))
|
|
g.emitExpr(a.Right)
|
|
g.emitBinaryOp(a.Op)
|
|
g.writeln(fmt.Sprintf(`t.PopMemvar(%q)`, ident.Name))
|
|
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("}()")
|
|
}
|