Files
five/compiler/gengo/emit_stmt.go
CharlesKWON 6a30c4e50e fix(gengo): compound assign for non-LOCAL LHS
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>
2026-05-03 05:14:28 +09:00

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("}()")
}