Files
five/compiler/gengo/gengo.go
CharlesKWON 3adc9d7d59 fix: PCount, Break/RECOVER, SET INDEX TO — 3 Harbour compat fixes
Release-blocking compatibility issues discovered during the 258-test
pre-release validation suite (100 syntax + 44 RDD + 114 RTL).

1. PCount() always returned 0 in PRG code

   Root cause: ParamCount() returned t.pendingParams, which is
   overwritten by every nested Function() call. By the time the
   PCount() RTL's Frame() executes, pendingParams is already 0.

   Fix: Frame() now stores pendingParams in frame.paramCount.
   PCount() RTL uses CallerParamCount() which reads callSP-2
   (the PRG caller's frame), while RTL functions still use
   ParamCount() (reads pendingParams before their own Frame).

   Verified: PCount(1,2,3)=3, PCount(1)=1, PCount()=0

2. Break("string") panicked instead of being caught by RECOVER USING

   Root cause: Generated SEQUENCE code only caught *HbError panics.
   Break() panics with BreakValue (a different type), which fell
   through to EndProc's "runtime error" message and re-panic.

   Fix (two parts):
   a) gengo emitBeginSequence: recover closure now catches any
      panic (interface{}), then dispatches via type switch:
      - *HbError → extract .Error() string
      - hasValue interface (BreakValue) → extract .GetValue()
      - other → static "error" string
   b) hbrtl/error.go: BreakValue gets GetValue() method for
      duck-type detection without import cycles
   c) hbrt/thread.go EndProc: BreakValue type name check added
      so it re-panics silently (no stderr noise)

3. SET INDEX TO a, b, c only opened the last file

   Root cause: Parser's parseSet() called parseExpr() once for
   INDEX setting, stopping at the first comma. Remaining file
   names were consumed by the "eat rest of line" loop.

   Fix: Parser now collects comma-separated identifiers into a
   single string literal "a,b,c". gengo splits on comma and
   calls OrderListAdd() for each file.

   Verified: SET INDEX TO si_name, si_city → OrdCount=2

All tests pass:
  go test ./...          14 packages OK
  FiveSql2               43/43  100%
  compat_harbour         51/51
  Syntax test           100/100
  RDD test               44/44
  RTL test              114/114
  Windows cross-compile  OK
  Linux cross-compile    OK

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 18:06:28 +09:00

2449 lines
67 KiB
Go

// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
// All rights reserved.
// Go code generator for the Five language.
// Converts Five AST into Go source code that calls hbrt runtime functions.
//
// Design references:
// - Harbour: gencc.c — pcode → hb_xvm*() C function calls
// - tsgo: internal/printer/printer.go — AST → text via Writer interface
// - Pattern: AST node → Thread method call (t.PushInt, t.Plus, etc.)
//
// Generated code structure:
// package main
// import ("five/hbrt"; "five/hbrtl")
// var symbols = hbrt.NewModule(...)
// func HB_MAIN(t *hbrt.Thread) { ... }
// func main() { vm := hbrt.NewVM(); ... vm.Run("MAIN") }
package gengo
import (
"five/compiler/ast"
"five/compiler/token"
"fmt"
"path/filepath"
"strings"
)
// Generator produces Go source code from a Five AST.
type Generator struct {
buf strings.Builder
indent int
file *ast.File
symbols []symbolEntry
imports map[string]bool
importAlias map[string]string // path → alias ("_", "name", or "")
curLocals localMap // current function's local variable map
goFastFuncs []goFastEntry // Go functions to register as FastFunc
staticVars map[string]string // top-level STATIC: upper name → Go var name
IsLibrary bool // if true, no main() generated, symbols use unique name
hoistedFields []string // field names hoisted outside FOR loop (nil = not hoisting)
hoistedDW bool // DO WHILE has hoisted _dwa/_darea
symCache map[string]string // symbol name → cached variable name (nil = not caching)
Debug bool // if true, emit t.DebugLine() calls
forLabelSeq int // monotonic counter for FOR..NEXT LOOP labels
curForLabel string // current FOR loop's LOOP goto label ("" if not in FOR)
blockSeq int // monotonic counter for unique closure capture names
}
type symbolEntry struct {
name string
scope string // "hbrt.FsPublic|hbrt.FsLocal" etc.
fn string // Go function name: "HB_MAIN"
}
// Generate converts an AST File into Go source code.
func Generate(file *ast.File) string {
return doGenerate(file, false, false)
}
// GenerateWithDebug is like Generate but includes DebugLine calls.
func GenerateWithDebug(file *ast.File) string {
return doGenerate(file, true, false)
}
// GenerateLibrary generates Go code without main() — for multi-PRG builds.
func GenerateLibrary(file *ast.File) string {
return doGenerate(file, false, true)
}
// generate is the unified internal implementation.
// doGenerate is the unified internal implementation for all Generate* variants.
func doGenerate(file *ast.File, debug, library bool) string {
g := &Generator{
file: file,
imports: map[string]bool{"five/hbrt": true, "five/hbrtl": true},
Debug: debug,
IsLibrary: library,
}
// Collect symbols from declarations
for _, d := range file.Decls {
switch decl := d.(type) {
case *ast.FuncDecl:
scope := "hbrt.FsPublic|hbrt.FsLocal"
if !library && (decl.Name == "Main" || decl.Name == "MAIN") {
scope += "|hbrt.FsFirst"
}
g.symbols = append(g.symbols, symbolEntry{
name: strings.ToUpper(decl.Name),
scope: scope,
fn: "HB_" + strings.ToUpper(decl.Name),
})
case *ast.ClassDecl:
className := strings.ToUpper(decl.Name)
g.symbols = append(g.symbols, symbolEntry{
name: className,
scope: "hbrt.FsPublic|hbrt.FsLocal",
fn: "HB_" + className + "_CTOR",
})
}
}
if hasXBaseCommands(file) {
g.imports["five/hbrdd"] = true
g.imports["five/hbrdd/dbf"] = true
}
g.importAlias = make(map[string]string)
for _, imp := range file.Imports {
g.imports[imp.Path] = true
if imp.Alias != "" {
g.importAlias[imp.Path] = imp.Alias
}
}
g.emitHeader()
g.emitSymbols()
for _, d := range file.Decls {
g.emitDecl(d)
}
g.emitFastFuncRegistrations()
if library {
g.emitInitModule()
} else {
g.emitMain()
}
// Patch deferred imports: inline RTL may add "fmt"/"strings" after header was emitted.
result := g.buf.String()
var deferred string
if g.imports["fmt"] && !strings.Contains(result, "\"fmt\"") {
deferred += "\t\"fmt\"\n"
}
if g.imports["strings"] && !strings.Contains(result, "\"strings\"") {
deferred += "\t\"strings\"\n"
}
result = strings.Replace(result, "\t/*DEFERRED_IMPORTS*/\n", deferred, 1)
// Patch guards
if g.imports["strings"] {
result = strings.Replace(result, "/*GUARD_STRINGS*/", "var _ = strings.TrimLeft", 1)
} else {
result = strings.Replace(result, "/*GUARD_STRINGS*/\n", "", 1)
}
if g.imports["fmt"] {
result = strings.Replace(result, "/*GUARD_FMT*/", "var _ = fmt.Sprintf", 1)
} else {
result = strings.Replace(result, "/*GUARD_FMT*/\n", "", 1)
}
return result
}
// --- Emit infrastructure ---
func (g *Generator) write(s string) {
g.buf.WriteString(s)
}
func (g *Generator) writef(format string, args ...interface{}) {
fmt.Fprintf(&g.buf, format, args...)
}
func (g *Generator) writeln(s string) {
g.writeIndent()
g.buf.WriteString(s)
g.buf.WriteByte('\n')
}
func (g *Generator) writeIndent() {
for i := 0; i < g.indent; i++ {
g.buf.WriteByte('\t')
}
}
// --- File structure ---
func (g *Generator) emitHeader() {
g.writeln("// Code generated by Five compiler. DO NOT EDIT.")
g.writeln(fmt.Sprintf("// Source: %s", g.file.Name))
g.writeln("")
g.writeln("package main")
g.writeln("")
// Imports (deferred placeholder for imports discovered during body emission)
g.writeln("import (")
g.indent++
for imp := range g.imports {
if alias, ok := g.importAlias[imp]; ok {
g.writeln(fmt.Sprintf("%s %q", alias, imp))
} else {
g.writeln(fmt.Sprintf("%q", imp))
}
}
g.writeln("/*DEFERRED_IMPORTS*/")
g.indent--
g.writeln(")")
g.writeln("")
// Ensure imports are used
g.writeln("var _ = hbrtl.RegisterRTL")
if g.imports["five/hbrdd"] {
g.writeln("var _ = hbrdd.NewWorkAreaManager")
g.writeln("var _ dbf.DBFDriver")
}
// Always emit — deferred inline RTL may add these imports after header.
// If not actually imported, the DEFERRED_IMPORTS patch won't add them and
// Go compiler will error. But we also strip unused guards in the patch step.
g.writeln("/*GUARD_STRINGS*/")
g.writeln("/*GUARD_FMT*/")
g.writeln("")
}
func (g *Generator) emitSymbols() {
varName := "symbols"
if g.IsLibrary {
// Unique variable name for library mode
safeName := strings.TrimSuffix(filepath.Base(g.file.Name), ".prg")
safeName = strings.Map(func(r rune) rune {
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') {
return r
}
return '_'
}, safeName)
varName = "symbols_" + safeName
}
g.writeln(fmt.Sprintf("var %s = hbrt.NewModule(%q,", varName, strings.TrimSuffix(g.file.Name, ".prg")))
g.indent++
for _, sym := range g.symbols {
g.writeln(fmt.Sprintf("hbrt.Sym(%q, %s, %s),", sym.name, sym.scope, sym.fn))
}
g.indent--
g.writeln(")")
g.writeln("")
}
// emitFastFuncRegistrations emits var declarations for Go FastFunc registrations.
// These are pre-registered at package init time for 3-11x faster calls.
func (g *Generator) emitFastFuncRegistrations() {
if len(g.goFastFuncs) == 0 {
return
}
// Deduplicate
seen := map[string]bool{}
g.writeln("// Go FastFunc registrations (type-specialized, bypass reflect)")
g.writeln("var (")
g.indent++
for _, ff := range g.goFastFuncs {
if seen[ff.regName] {
continue
}
seen[ff.regName] = true
g.writeln(fmt.Sprintf("_ff_%s = hbrt.RegisterFastFunc(%q, %s)", ff.regName, ff.qualName, ff.qualName))
}
g.indent--
g.writeln(")")
g.writeln("")
}
func (g *Generator) emitMain() {
// init() runs before main() — set raw mode before ANY Go runtime I/O
g.writeln("func init() {")
g.indent++
g.writeln("hbrtl.InitRawTerminal()")
g.indent--
g.writeln("}")
g.writeln("")
g.writeln("func main() {")
g.indent++
g.writeln("vm := hbrt.NewVM()")
g.writeln("hbrtl.RegisterRTL(vm)")
g.writeln("vm.RegisterModule(symbols)")
// Find main function
mainName := "MAIN"
for _, sym := range g.symbols {
if strings.Contains(sym.scope, "FsFirst") {
mainName = sym.name
break
}
}
g.writeln(fmt.Sprintf("vm.Run(%q)", mainName))
g.indent--
g.writeln("}")
}
func (g *Generator) emitInitModule() {
safeName := strings.TrimSuffix(filepath.Base(g.file.Name), ".prg")
safeName = strings.Map(func(r rune) rune {
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') {
return r
}
return '_'
}, safeName)
varName := "symbols_" + safeName
// Register this module's symbols into a global registry
// that main()'s vm.RegisterModule will pick up
g.writeln(fmt.Sprintf("func init() {"))
g.indent++
g.writeln(fmt.Sprintf("hbrt.RegisterLibModule(%s)", varName))
g.indent--
g.writeln("}")
}
// --- Declaration emission ---
func (g *Generator) emitDecl(d ast.Decl) {
switch decl := d.(type) {
case *ast.FuncDecl:
g.emitFuncDecl(decl)
case *ast.ClassDecl:
g.emitClassDecl(decl)
case *ast.MethodDecl:
g.emitMethodDeclStandalone(decl)
case *ast.VarDecl:
// Top-level STATIC → package-level var
if decl.Scope == ast.ScopeStatic {
g.emitTopLevelStatic(decl)
}
case *ast.GoDumpDecl:
// Inline Go code from #pragma BEGINDUMP ... ENDDUMP
if decl.Code != "" {
g.writeln("\n// --- Inline Go code (#pragma BEGINDUMP) ---")
g.write(decl.Code)
g.writeln("\n// --- End inline Go code ---\n")
}
}
}
// emitTopLevelStatic emits module-level STATIC variables as package-level Go vars.
func (g *Generator) emitTopLevelStatic(vd *ast.VarDecl) {
for _, v := range vd.Vars {
varName := "static_" + strings.ToUpper(v.Name)
initVal := "hbrt.MakeNil()"
if v.Init != nil {
initVal = g.exprToGoLiteral(v.Init)
}
g.writeln(fmt.Sprintf("var %s = %s", varName, initVal))
// Register in staticMap for lookup
if g.staticVars == nil {
g.staticVars = make(map[string]string)
}
g.staticVars[strings.ToUpper(v.Name)] = varName
}
g.writeln("")
}
func (g *Generator) emitFuncDecl(fn *ast.FuncDecl) {
goName := "HB_" + strings.ToUpper(fn.Name)
g.writeln(fmt.Sprintf("func %s(t *hbrt.Thread) {", goName))
g.indent++
// Count params and locals (including mid-function LOCALs in Body)
nParams := len(fn.Params)
nLocals := 0
for _, d := range fn.Decls {
if vd, ok := d.(*ast.VarDecl); ok && vd.Scope == ast.ScopeLocal {
nLocals += len(vd.Vars)
}
}
// Count mid-function LOCAL declarations in Body
for _, s := range fn.Body {
if vd, ok := s.(*ast.VarDecl); ok && vd.Scope == ast.ScopeLocal {
nLocals += len(vd.Vars)
}
}
g.writeln(fmt.Sprintf("t.Frame(%d, %d)", nParams, nLocals))
g.writeln("defer t.EndProc()")
g.writeln("")
// Build local map FIRST (needed for init expressions that reference params)
g.curLocals = g.buildLocalMap(fn)
// Emit LOCAL initializers
localIdx := nParams + 1 // 1-based, params come first
for _, d := range fn.Decls {
vd, ok := d.(*ast.VarDecl)
if !ok || vd.Scope != ast.ScopeLocal {
continue
}
for _, v := range vd.Vars {
if v.Init != nil {
g.emitExpr(v.Init)
g.writeln(fmt.Sprintf("t.PopLocalFast(%d)", localIdx))
}
localIdx++
}
}
// Emit body statements
for _, stmt := range fn.Body {
g.emitStmt(stmt, g.curLocals)
}
g.indent--
g.writeln("}")
g.writeln("")
}
type localMap map[string]int
func (g *Generator) buildLocalMap(fn *ast.FuncDecl) localMap {
m := make(localMap)
idx := 1
for _, p := range fn.Params {
m[strings.ToUpper(p.Name)] = idx
idx++
}
for _, d := range fn.Decls {
if vd, ok := d.(*ast.VarDecl); ok && vd.Scope == ast.ScopeLocal {
for _, v := range vd.Vars {
m[strings.ToUpper(v.Name)] = idx
idx++
}
}
}
return m
}
// scanBodyLocals recursively scans statements for LOCAL declarations,
// adding them to the local map with pre-assigned indices.
// This ensures mid-function LOCALs are known at compile time.
func scanBodyLocals(stmts []ast.Stmt, m localMap, idx *int) {
for _, s := range stmts {
switch st := s.(type) {
case *ast.VarDecl:
if st.Scope == ast.ScopeLocal {
for _, v := range st.Vars {
name := strings.ToUpper(v.Name)
if _, exists := m[name]; !exists {
m[name] = *idx
(*idx)++
}
}
}
case *ast.IfStmt:
scanBodyLocals(st.Body, m, idx)
for _, ei := range st.ElseIfs {
scanBodyLocals(ei.Body, m, idx)
}
scanBodyLocals(st.ElseBody, m, idx)
case *ast.DoWhileStmt:
scanBodyLocals(st.Body, m, idx)
case *ast.ForStmt:
scanBodyLocals(st.Body, m, idx)
case *ast.ForEachStmt:
scanBodyLocals(st.Body, m, idx)
case *ast.SwitchStmt:
for _, c := range st.Cases {
scanBodyLocals(c.Body, m, idx)
}
scanBodyLocals(st.Otherwise, m, idx)
case *ast.SeqStmt:
scanBodyLocals(st.Body, m, idx)
scanBodyLocals(st.RecoverBody, m, idx)
}
}
}
// --- Statement emission ---
func (g *Generator) emitStmt(stmt ast.Stmt, locals localMap) {
// Emit debug line hook
if g.Debug && stmt.Pos().Line > 0 {
g.writeln(fmt.Sprintf("t.DebugLine(%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++
// Key expression: stringify ident (field name) or use string literal
keyStr := exprToString(s.KeyExpr)
g.writeln(fmt.Sprintf("_keyExpr := %q", keyStr))
fileStr := exprToString(s.File)
g.writeln(fmt.Sprintf("_file := %q", fileStr))
forExpr := `""`
if s.ForCond != nil {
forExpr = fmt.Sprintf("%q", exprToString(s.ForCond))
}
// Set VM callback for UDF evaluation during index build
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, Unique: %v, Descending: %v})",
forExpr, s.Unique, s.Descending))
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.writeln(fmt.Sprintf(`t.PushSymbol(t.VM().FindSymbol(%q))`, 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.writeln(fmt.Sprintf(`t.PushSymbol(t.VM().FindSymbol(%q))`, 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 {
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.writeln(fmt.Sprintf("t.PushSymbol(t.VM().FindSymbol(%q))", 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.writeln(fmt.Sprintf("t.PushSymbol(t.VM().FindSymbol(%q))", 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 {
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:
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 STATIC variable
upper := strings.ToUpper(ident.Name)
if goVar, found := g.staticVars[upper]; found {
g.emitExpr(a.Right)
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.writeln(fmt.Sprintf("t.PushSymbol(t.VM().FindSymbol(%q))", 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)))
}
func (g *Generator) emitIf(s *ast.IfStmt, locals localMap) {
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 s.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(s.ElseBody) > 0 {
g.writeln("} else {")
g.indent++
for _, stmt := range s.ElseBody {
g.emitStmt(stmt, locals)
}
g.indent--
}
g.writeln("}")
// Close nested elseif braces
for range s.ElseIfs {
g.writeln("}")
}
}
func (g *Generator) emitDoWhile(s *ast.DoWhileStmt, locals localMap) {
// 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++
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)
if lit, ok := s.To.(*ast.LiteralExpr); ok && 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) {
g.emitExpr(s.Expr)
g.writeln("_sw := t.Pop2()")
first := true
for _, c := range s.Cases {
if first {
g.emitExpr(c.Value)
g.writeln("if _sw.AsNumInt() == t.Pop2().AsNumInt() {")
first = false
} else {
g.emitExpr(c.Value)
g.writeln("} else if _sw.AsNumInt() == t.Pop2().AsNumInt() {")
}
g.indent++
for _, stmt := range c.Body {
g.emitStmt(stmt, locals)
}
g.indent--
}
if len(s.Otherwise) > 0 {
g.writeln("} else {")
g.indent++
for _, stmt := range s.Otherwise {
g.emitStmt(stmt, locals)
}
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
g.emitExpr(s.Collection)
g.writeln("{ _feArr := t.Pop2()")
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--
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("}()")
}
func (g *Generator) emitExpr(expr ast.Expr) {
switch e := expr.(type) {
case *ast.LiteralExpr:
g.emitLiteral(e)
case *ast.IdentExpr:
g.emitIdent(e)
case *ast.BinaryExpr:
// Short-circuit AND/OR: Harbour evaluates right operand only if needed
if e.Op == token.AND {
g.emitExpr(e.Left)
g.writeln("if !t.PopLogical() {")
g.writeln("t.PushBool(false)")
g.writeln("} else {")
g.emitExpr(e.Right)
g.writeln("}")
} else if e.Op == token.OR {
g.emitExpr(e.Left)
g.writeln("if t.PopLogical() {")
g.writeln("t.PushBool(true)")
g.writeln("} else {")
g.emitExpr(e.Right)
g.writeln("}")
} else {
g.emitExpr(e.Left)
g.emitExpr(e.Right)
g.emitBinaryOp(e.Op)
}
case *ast.UnaryExpr:
g.emitExpr(e.X)
g.emitUnaryOp(e.Op)
case *ast.AssignExpr:
// Handle compound assignment (+=, -=, :=) in expression context.
// Needed for code block bodies like {|x| nSum += x}.
g.emitAssignExpr(e)
case *ast.CallExpr:
g.emitCall(e)
case *ast.DotExpr:
// pkg.Member as value (rare — usually inside CallExpr)
g.writeln(fmt.Sprintf("t.PushValue(hbrt.WrapGo(%s.%s))", g.dotPkgName(e), e.Member))
case *ast.SendExpr:
g.emitSendExpr(e)
case *ast.IndexExpr:
g.emitExpr(e.X)
g.emitExpr(e.Index)
g.writeln("t.ArrayPush()")
case *ast.SelfExpr:
g.writeln("t.PushSelf()") // :: alone, rare
case *ast.ArrayLitExpr:
for _, item := range e.Items {
g.emitExpr(item)
}
g.writeln(fmt.Sprintf("t.ArrayGen(%d)", len(e.Items)))
case *ast.HashLitExpr:
for i := range e.Keys {
g.emitExpr(e.Keys[i])
g.emitExpr(e.Values[i])
}
g.writeln(fmt.Sprintf("t.HashGen(%d)", len(e.Keys)))
case *ast.BlockExpr:
g.emitBlock(e)
case *ast.SliceExpr:
// a[low:high] → hbrt.ArraySlice(array, low, high)
g.emitExpr(e.X)
if e.Low != nil {
g.emitExpr(e.Low)
} else {
g.writeln("t.PushInt(1)") // default: from start (1-based)
}
if e.High != nil {
g.emitExpr(e.High)
} else {
g.writeln("t.PushInt(-1)") // default: to end (-1 = all)
}
g.writeln("t.ArraySlice()")
case *ast.NilSafeExpr:
// obj?:Method() → if not nil, call; else push NIL
g.emitExpr(e.X)
g.writeln("{")
g.indent++
g.writeln("_ns := t.Pop2()")
g.writeln("if _ns.IsNil() {")
g.indent++
g.writeln("t.PushNil()")
g.indent--
g.writeln("} else {")
g.indent++
g.writeln("t.PushValue(_ns)")
for _, arg := range e.Args {
g.emitExpr(arg)
}
g.writeln(fmt.Sprintf("t.Send(%q, %d)", e.Method, len(e.Args)))
g.indent--
g.writeln("}")
g.indent--
g.writeln("}")
case *ast.InterpolatedString:
// Already converted to fmt.Sprintf CallExpr by parser
g.emitExpr(e.Parts[0]) // shouldn't reach here normally
case *ast.MacroExpr:
g.writeln("t.MacroPush() // runtime macro compilation")
g.writeln("t.PushNil()")
case *ast.AliasExpr:
g.emitAliasExpr(e)
case *ast.RefExpr:
// @variable — pass by reference
// In Five, we push a ByRef wrapper that holds the local index
if ident, ok := e.X.(*ast.IdentExpr); ok {
if idx, found := g.curLocals[strings.ToUpper(ident.Name)]; found {
g.writeln(fmt.Sprintf("t.PushLocalRef(%d)", idx))
} else {
g.emitExpr(e.X) // fallback: push value
}
} else {
g.emitExpr(e.X)
}
case *ast.IIfExpr:
g.emitExpr(e.Cond)
g.writeln("if t.PopLogical() {")
g.indent++
g.emitExpr(e.True)
g.indent--
g.writeln("} else {")
g.indent++
g.emitExpr(e.False)
g.indent--
g.writeln("}")
case *ast.PostfixExpr:
g.emitExpr(e.X)
g.writeln("t.Dup()")
if e.Op == token.INC {
g.writeln("t.Inc()")
} else {
g.writeln("t.Dec()")
}
g.writeln("t.Pop() // keep original for postfix")
default:
g.writeln(fmt.Sprintf("t.PushNil() // WARN: unhandled expr %T", expr))
}
}
// exprToString extracts a string representation from an AST expression.
// Used for INDEX ON key and filename, where idents are field/file names, not variables.
func exprToString(expr ast.Expr) string {
switch e := expr.(type) {
case *ast.IdentExpr:
return e.Name
case *ast.LiteralExpr:
if e.Kind == token.STRING {
return `"` + e.Value + `"`
}
return e.Value
case *ast.BinaryExpr:
left := exprToString(e.Left)
right := exprToString(e.Right)
opStr := ""
switch e.Op {
case token.PLUS:
opStr = "+"
case token.MINUS:
opStr = "-"
case token.EQ:
opStr = "="
case token.EXEQ:
opStr = "=="
case token.NEQ:
opStr = "!="
case token.LT:
opStr = "<"
case token.GT:
opStr = ">"
case token.LTE:
opStr = "<="
case token.GTE:
opStr = ">="
case token.AND:
opStr = ".AND."
case token.OR:
opStr = ".OR."
}
if opStr != "" {
return left + " " + opStr + " " + right
}
case *ast.UnaryExpr:
if e.Op == token.NOT {
return "!" + exprToString(e.X)
}
case *ast.CallExpr:
if ident, ok := e.Func.(*ast.IdentExpr); ok {
args := ""
for i, a := range e.Args {
if i > 0 {
args += ","
}
args += exprToString(a)
}
return ident.Name + "(" + args + ")"
}
case *ast.AliasExpr:
// FIELD->NAME, M->VAR, ALIAS->FIELD
alias := exprToString(e.Alias)
field := exprToString(e.Field)
return alias + "->" + field
}
return ""
}
func (g *Generator) emitLiteral(e *ast.LiteralExpr) {
switch e.Kind {
case token.INT:
g.writeln(fmt.Sprintf("t.PushInt(%s)", e.Value))
case token.DOUBLE:
g.writeln(fmt.Sprintf("t.PushDouble(%s, 255, 255)", e.Value))
case token.STRING:
g.writeln(fmt.Sprintf("t.PushString(%q)", e.Value))
case token.TRUE:
g.writeln("t.PushBool(true)")
case token.FALSE:
g.writeln("t.PushBool(false)")
case token.NIL_LIT:
g.writeln("t.PushNil()")
default:
g.writeln(fmt.Sprintf("t.PushNil() // WARN: unknown literal kind %v", e.Kind))
}
}
func (g *Generator) emitIdent(e *ast.IdentExpr) {
upper := strings.ToUpper(e.Name)
// Special: Self keyword → PushSelf
if upper == "SELF" {
g.writeln("t.PushSelf()")
return
}
if idx, found := g.curLocals[upper]; found {
g.writeln(fmt.Sprintf("t.PushLocalFast(%d)", idx))
} else if goVar, found := g.staticVars[upper]; found {
// Module-level STATIC variable
g.writeln(fmt.Sprintf("t.PushValue(%s)", goVar))
} else {
// Not a local — could be unresolved global variable or function ref
g.writeln(fmt.Sprintf("t.PushLocal(0) // UNRESOLVED: %q", e.Name))
}
}
func (g *Generator) emitCall(e *ast.CallExpr) {
// Check for Go package call: pkg.Func(args)
if dot, ok := e.Func.(*ast.DotExpr); ok {
if g.isGoPackage(dot) {
g.emitGoPackageCall(dot, e.Args)
return
}
}
// Inline hot-path RTL functions — skip Frame/EndProc/VM dispatch entirely
if ident, ok := e.Func.(*ast.IdentExpr); ok {
if g.tryEmitInlineRTL(ident.Name, e.Args) {
return
}
upper := strings.ToUpper(ident.Name)
// Guard: reserved words must never be emitted as function calls.
// This catches PRG bugs like stray ENDIF/ENDDO/NEXT from bad IF nesting.
if isReservedWord(upper) {
g.writeln(fmt.Sprintf("// WARN: reserved word %q used as function call — skipped", upper))
g.writeln("t.PushNil()")
return
}
if g.symCache != nil {
if varName, ok := g.symCache[upper]; ok {
g.writeln(fmt.Sprintf("t.PushSymbol(%s)", varName))
} else {
g.writeln(fmt.Sprintf("t.PushSymbol(t.VM().FindSymbol(%q))", upper))
}
} else {
g.writeln(fmt.Sprintf("t.PushSymbol(t.VM().FindSymbol(%q))", upper))
}
} else {
g.emitExpr(e.Func)
}
g.writeln("t.PushNil()")
for _, arg := range e.Args {
g.emitExpr(arg)
}
g.writeln(fmt.Sprintf("t.Function(%d)", len(e.Args)))
}
// emitAssignExpr handles := / += / -= in expression context (e.g. code block body).
func (g *Generator) emitAssignExpr(e *ast.AssignExpr) {
// alias->field := value in expression context
if aliasExpr, ok := e.Left.(*ast.AliasExpr); ok {
if aliasIdent, ok2 := aliasExpr.Alias.(*ast.IdentExpr); ok2 {
if fieldIdent, ok3 := aliasExpr.Field.(*ast.IdentExpr); ok3 {
g.emitExpr(e.Right)
g.writeln("t.Dup()")
g.writeln(fmt.Sprintf(`{ _wa := t.WA.(*hbrdd.WorkAreaManager); _wa.SetAliasField(%q, %q, t.Pop2()) }`, aliasIdent.Name, fieldIdent.Name))
return
}
}
}
if ident, ok := e.Left.(*ast.IdentExpr); ok {
if idx, found := g.curLocals[strings.ToUpper(ident.Name)]; found {
switch e.Op {
case token.ASSIGN:
g.emitExpr(e.Right)
g.writeln("t.Dup()")
g.writeln(fmt.Sprintf("t.PopLocalFast(%d)", idx))
case token.PLUSEQ:
g.emitExpr(e.Right)
g.writeln(fmt.Sprintf("t.LocalAdd(%d)", idx))
g.writeln(fmt.Sprintf("t.PushLocalFast(%d)", idx))
case token.MINUSEQ:
g.emitExpr(e.Right)
g.writeln("t.Negate()")
g.writeln(fmt.Sprintf("t.LocalAdd(%d)", idx))
g.writeln(fmt.Sprintf("t.PushLocalFast(%d)", idx))
default:
g.writeln(fmt.Sprintf("t.PushLocalFast(%d)", idx))
g.emitExpr(e.Right)
g.emitBinaryOp(e.Op)
g.writeln("t.Dup()")
g.writeln(fmt.Sprintf("t.PopLocalFast(%d)", idx))
}
return
}
}
// Fallback: unknown target
g.emitExpr(e.Right)
g.writeln("t.Dup()")
g.writeln("// WARN: compound assignment — unknown target")
}
// isReservedWord returns true if the name is a Harbour reserved keyword
// that must never be emitted as a function call.
func isReservedWord(name string) bool {
switch name {
case "IF", "ELSE", "ELSEIF", "ENDIF",
"DO", "WHILE", "ENDDO",
"FOR", "NEXT", "TO", "STEP",
"RETURN", "FUNCTION", "PROCEDURE",
"LOCAL", "STATIC", "PRIVATE", "PUBLIC",
"BEGIN", "SEQUENCE", "RECOVER", "END",
"SWITCH", "CASE", "OTHERWISE", "ENDCASE",
"EXIT", "LOOP",
"CLASS", "ENDCLASS", "METHOD", "DATA",
"WITH", "OBJECT",
"NIL", "TRUE", "FALSE",
"AND", "OR", "NOT", "IN":
return true
}
return false
}
// tryEmitInlineRTL emits direct Go code for known RTL functions.
// Returns true if handled (no VM dispatch needed).
// This eliminates Frame/EndProc/symbol lookup for hot-path functions.
func (g *Generator) tryEmitInlineRTL(name string, args []ast.Expr) bool {
upper := strings.ToUpper(name)
switch upper {
case "LTRIM":
if len(args) == 1 {
g.emitExpr(args[0])
g.writeln("{ _s := t.Pop2().AsString(); t.PushString(strings.TrimLeft(_s, \" \")) }")
g.imports["strings"] = true
return true
}
case "RTRIM", "TRIM":
if len(args) == 1 {
g.emitExpr(args[0])
g.writeln("{ _s := t.Pop2().AsString(); t.PushString(strings.TrimRight(_s, \" \")) }")
g.imports["strings"] = true
return true
}
case "ALLTRIM":
if len(args) == 1 {
g.emitExpr(args[0])
g.writeln("{ _s := t.Pop2().AsString(); t.PushString(strings.TrimSpace(_s)) }")
g.imports["strings"] = true
return true
}
case "UPPER":
if len(args) == 1 {
g.emitExpr(args[0])
g.writeln("{ _s := t.Pop2().AsString(); t.PushString(strings.ToUpper(_s)) }")
g.imports["strings"] = true
return true
}
case "LOWER":
if len(args) == 1 {
g.emitExpr(args[0])
g.writeln("{ _s := t.Pop2().AsString(); t.PushString(strings.ToLower(_s)) }")
g.imports["strings"] = true
return true
}
case "LEN":
if len(args) == 1 {
g.emitExpr(args[0])
g.writeln("{ _v := t.Pop2(); if _v.IsString() { t.PushInt(len(_v.AsString())) } else if _v.IsArray() { t.PushInt(len(_v.AsArray().Items)) } else if _v.IsHash() { t.PushInt(len(_v.AsHash().Keys)) } else { t.PushInt(0) } }")
return true
}
case "EMPTY":
if len(args) == 1 {
g.emitExpr(args[0])
g.writeln("{ _v := t.Pop2(); t.PushBool(_v.IsNil() || (_v.IsString() && len(strings.TrimSpace(_v.AsString())) == 0) || (_v.IsNumeric() && _v.AsNumDouble() == 0) || (_v.IsLogical() && !_v.AsBool()) || (_v.IsDate() && _v.AsJulian() == 0)) }")
g.imports["strings"] = true
return true
}
case "CHR":
if len(args) == 1 {
g.emitExpr(args[0])
g.writeln("t.PushString(string(byte(t.Pop2().AsNumInt())))")
return true
}
case "ASC":
if len(args) == 1 {
g.emitExpr(args[0])
g.writeln("{ _s := t.Pop2().AsString(); if len(_s)>0 { t.PushInt(int(_s[0])) } else { t.PushInt(0) } }")
return true
}
case "EOF":
if len(args) == 0 {
if g.hoistedDW {
g.writeln(fmt.Sprintf("t.PushBool(%s != nil && %s.EOF())", g.hoistedAreaVar(), g.hoistedAreaVar()))
} else {
g.writeln("{ _wa := t.WA.(*hbrdd.WorkAreaManager); if _a := _wa.Current(); _a != nil { t.PushBool(_a.EOF()) } else { t.PushBool(true) } }")
}
return true
}
case "BOF":
if len(args) == 0 {
if g.hoistedDW {
g.writeln(fmt.Sprintf("t.PushBool(%s != nil && %s.BOF())", g.hoistedAreaVar(), g.hoistedAreaVar()))
} else {
g.writeln("{ _wa := t.WA.(*hbrdd.WorkAreaManager); if _a := _wa.Current(); _a != nil { t.PushBool(_a.BOF()) } else { t.PushBool(true) } }")
}
return true
}
case "FOUND":
if len(args) == 0 {
if g.hoistedDW {
g.writeln(fmt.Sprintf("t.PushBool(%s != nil && %s.Found())", g.hoistedAreaVar(), g.hoistedAreaVar()))
} else {
g.writeln("{ _wa := t.WA.(*hbrdd.WorkAreaManager); if _a := _wa.Current(); _a != nil { t.PushBool(_a.Found()) } else { t.PushBool(false) } }")
}
return true
}
case "RECNO":
if len(args) == 0 {
if g.hoistedDW {
g.writeln(fmt.Sprintf("if %s != nil { t.PushInt(int(%s.RecNo())) } else { t.PushInt(0) }", g.hoistedAreaVar(), g.hoistedAreaVar()))
} else {
g.writeln("{ _wa := t.WA.(*hbrdd.WorkAreaManager); if _a := _wa.Current(); _a != nil { t.PushInt(int(_a.RecNo())) } else { t.PushInt(0) } }")
}
return true
}
case "DELETED":
if len(args) == 0 {
if g.hoistedDW {
g.writeln(fmt.Sprintf("t.PushBool(%s != nil && %s.Deleted())", g.hoistedAreaVar(), g.hoistedAreaVar()))
} else {
g.writeln("{ _wa := t.WA.(*hbrdd.WorkAreaManager); if _a := _wa.Current(); _a != nil { t.PushBool(_a.Deleted()) } else { t.PushBool(false) } }")
}
return true
}
case "RECCOUNT":
if len(args) == 0 {
g.writeln("{ _wa := t.WA.(*hbrdd.WorkAreaManager); if _a := _wa.Current(); _a != nil { _rc, _ := _a.RecCount(); t.PushInt(int(_rc)) } else { t.PushInt(0) } }")
return true
}
case "STR":
if len(args) >= 1 && len(args) <= 3 {
g.emitExpr(args[0])
g.writeln("{ _sv := t.Pop2()")
if len(args) >= 2 {
g.emitExpr(args[1])
g.writeln("_sw := int(t.Pop2().AsNumInt())")
} else {
g.writeln("_sw := 10")
}
if len(args) >= 3 {
g.emitExpr(args[2])
g.writeln("_sd := int(t.Pop2().AsNumInt())")
} else {
g.writeln("_sd := 0")
}
g.writeln("_ss := fmt.Sprintf(\"%*.*f\", _sw, _sd, _sv.AsNumDouble())")
g.writeln("if len(_ss) > _sw && _sw > 0 { _ss = strings.Repeat(\"*\", _sw) }")
g.writeln("t.PushString(_ss) }")
g.imports["fmt"] = true
g.imports["strings"] = true
return true
}
case "PADR":
if len(args) >= 2 {
g.emitExpr(args[0])
g.emitExpr(args[1])
g.writeln("{ _pn := int(t.Pop2().AsNumInt()); _ps := t.Pop2().AsString()")
g.writeln("if len(_ps) >= _pn { t.PushString(_ps[:_pn])")
g.writeln("} else { t.PushString(_ps + hbrtl.Spaces(_pn - len(_ps))) } }")
return true
}
case "PADL":
if len(args) >= 2 && len(args) <= 3 {
g.emitExpr(args[0])
g.emitExpr(args[1])
if len(args) == 3 {
g.emitExpr(args[2])
g.writeln("{ _pf := t.Pop2().AsString(); _pn := int(t.Pop2().AsNumInt()); _ps := t.Pop2().AsString()")
g.writeln("if len(_ps) >= _pn { t.PushString(_ps[len(_ps)-_pn:])")
g.writeln("} else { t.PushString(strings.Repeat(_pf[:1], _pn-len(_ps)) + _ps) } }")
g.imports["strings"] = true
} else {
g.writeln("{ _pn := int(t.Pop2().AsNumInt()); _ps := t.Pop2().AsString()")
g.writeln("if len(_ps) >= _pn { t.PushString(_ps[len(_ps)-_pn:])")
g.writeln("} else { t.PushString(hbrtl.Spaces(_pn - len(_ps)) + _ps) } }")
}
return true
}
case "SUBSTR", "SUBSTRING":
if len(args) >= 2 && len(args) <= 3 {
g.emitExpr(args[0])
g.emitExpr(args[1])
if len(args) == 3 {
g.emitExpr(args[2])
g.writeln("{ _sl := int(t.Pop2().AsNumInt()); _sp := int(t.Pop2().AsNumInt())-1; _ss := t.Pop2().AsString()")
} else {
g.writeln("{ _sl := 0; _sp := int(t.Pop2().AsNumInt())-1; _ss := t.Pop2().AsString(); _sl = len(_ss) - _sp")
}
g.writeln("if _sp < 0 { _sp = 0 }; if _sp > len(_ss) { _sp = len(_ss) }")
g.writeln("if _sp+_sl > len(_ss) { _sl = len(_ss) - _sp }")
g.writeln("t.PushString(_ss[_sp:_sp+_sl]) }")
return true
}
case "LEFT":
if len(args) == 2 {
g.emitExpr(args[0])
g.emitExpr(args[1])
g.writeln("{ _ln := int(t.Pop2().AsNumInt()); _ls := t.Pop2().AsString()")
g.writeln("if _ln >= len(_ls) { t.PushString(_ls) } else if _ln <= 0 { t.PushString(\"\") } else { t.PushString(_ls[:_ln]) } }")
return true
}
case "RIGHT":
if len(args) == 2 {
g.emitExpr(args[0])
g.emitExpr(args[1])
g.writeln("{ _rn := int(t.Pop2().AsNumInt()); _rs := t.Pop2().AsString()")
g.writeln("if _rn >= len(_rs) { t.PushString(_rs) } else if _rn <= 0 { t.PushString(\"\") } else { t.PushString(_rs[len(_rs)-_rn:]) } }")
return true
}
case "AT":
if len(args) == 2 {
g.emitExpr(args[0])
g.emitExpr(args[1])
g.writeln("{ _as := t.Pop2().AsString(); _ak := t.Pop2().AsString()")
g.writeln("_ai := strings.Index(_as, _ak)")
g.writeln("if _ai >= 0 { t.PushInt(_ai+1) } else { t.PushInt(0) } }")
g.imports["strings"] = true
return true
}
case "IIF":
if len(args) == 3 {
g.emitExpr(args[0])
g.writeln("if t.Pop2().AsBool() {")
g.indent++
g.emitExpr(args[1])
g.indent--
g.writeln("} else {")
g.indent++
g.emitExpr(args[2])
g.indent--
g.writeln("}")
return true
}
}
return false
}
// Spaces is exported for use by generated code.
func init() {
// hbrtl.Spaces is available to generated code via import
}
// isGoPackage checks if a DotExpr refers to an imported Go package.
func (g *Generator) isGoPackage(dot *ast.DotExpr) bool {
if ident, ok := dot.X.(*ast.IdentExpr); ok {
pkgName := ident.Name
// Check against imported package names
for path := range g.imports {
// "database/sql" → last segment "sql"
parts := strings.Split(path, "/")
name := parts[len(parts)-1]
if alias, ok := g.importAlias[path]; ok && alias != "_" && alias != "" {
name = alias
}
if name == pkgName {
return true
}
}
}
return false
}
// dotPkgName extracts the package identifier from a DotExpr.
func (g *Generator) dotPkgName(dot *ast.DotExpr) string {
if ident, ok := dot.X.(*ast.IdentExpr); ok {
return ident.Name
}
return "unknown"
}
// emitGoPackageCall generates direct Go function call with auto type bridging.
// PRG: result := sql.Open("sqlite", ":memory:")
// Go: { _r := hbrt.GoCallFunc(sql.Open, args...); t.PushValue(_r[0]) }
func (g *Generator) emitGoPackageCall(dot *ast.DotExpr, args []ast.Expr) {
pkg := g.dotPkgName(dot)
fn := dot.Member
qualName := pkg + "." + fn
regName := pkg + "_" + fn // safe Go variable name
// Register FastFunc in init block
g.goFastFuncs = append(g.goFastFuncs, goFastEntry{regName: regName, qualName: qualName})
// Build arg list
g.writeln("{")
g.indent++
argNames := make([]string, len(args))
for i, arg := range args {
argName := fmt.Sprintf("_a%d", i)
argNames[i] = argName
g.emitExpr(arg)
g.writeln(fmt.Sprintf("%s := t.Pop2()", argName))
}
argsStr := ""
for i, name := range argNames {
if i > 0 {
argsStr += ", "
}
argsStr += name
}
// Use FastPath (type-specialized, 3-11x faster than reflect)
g.writeln(fmt.Sprintf("_results := hbrt.GoCallFast(_ff_%s, %s)", regName, argsStr))
g.writeln("if len(_results) > 0 { t.PushValue(_results[0]) } else { t.PushNil() }")
g.indent--
g.writeln("}")
}
type goFastEntry struct {
regName string // Go variable: strings_ToUpper
qualName string // Go call: strings.ToUpper
}
func (g *Generator) emitAliasExpr(e *ast.AliasExpr) {
fieldIdent, isFieldIdent := e.Field.(*ast.IdentExpr)
// Case 1: alias->field (static alias, simple field name)
if ident, ok := e.Alias.(*ast.IdentExpr); ok && isFieldIdent {
g.writeln(fmt.Sprintf(`t.PushAliasField(%q, %q)`, ident.Name, fieldIdent.Name))
return
}
// Case 2: (expr)->field (dynamic alias, simple field name)
if isFieldIdent {
g.emitExpr(e.Alias)
g.writeln(fmt.Sprintf(`t.PushDynAliasField(t.Pop2().AsString(), %q)`, fieldIdent.Name))
return
}
// Case 3: alias->(expr) or (expr)->(expr) — workarea context expression
// Harbour: save current WA, select new WA, evaluate expr, restore WA
// Example: (nArea)->(Used()) → evaluate Used() in workarea nArea
// Example: CUSTOMERS->(RecCount()) → evaluate RecCount() in CUSTOMERS workarea
if ident, ok := e.Alias.(*ast.IdentExpr); ok {
_, isLocal := g.curLocals[strings.ToUpper(ident.Name)]
if isLocal {
// Local variable: emit value (numeric area number)
g.emitExpr(e.Alias)
g.writeln(`t.WASaveAndSelect(int(t.Pop2().AsNumInt()))`)
} else {
// Static alias name: resolve by alias string
g.writeln(fmt.Sprintf(`t.WASaveAndSelectAlias(%q)`, ident.Name))
}
} else {
// Dynamic: numeric area from expression
g.emitExpr(e.Alias)
g.writeln(`t.WASaveAndSelect(int(t.Pop2().AsNumInt()))`)
}
g.emitExpr(e.Field)
g.writeln(`t.WARestore()`)
}
func (g *Generator) fieldName(expr ast.Expr) string {
if ident, ok := expr.(*ast.IdentExpr); ok {
return ident.Name
}
return ""
}
func (g *Generator) emitSendExpr(e *ast.SendExpr) {
// Self access: ::field (no parens) → PushSelfField
// Self method: ::method() (has parens) → Send on Self
if _, isSelf := e.Object.(*ast.SelfExpr); isSelf {
if !e.HasParens && len(e.Args) == 0 {
// ::field (getter, no parentheses)
g.writeln(fmt.Sprintf("t.PushSelfField(%q)", strings.ToUpper(e.Method)))
return
}
// ::method() or ::method(args) — method call on Self
g.writeln("t.PushSelf()")
for _, arg := range e.Args {
g.emitExpr(arg)
}
g.writeln(fmt.Sprintf("t.Send(%q, %d)", e.Method, len(e.Args)))
return
}
// General: obj:method(args) or obj:field
// Check at runtime: if Go object → GoCall, else Harbour Send
g.emitExpr(e.Object)
g.writeln("{")
g.indent++
g.writeln("_obj := t.Pop2()")
// Push args and capture them
argNames := make([]string, len(e.Args))
for i, arg := range e.Args {
argNames[i] = fmt.Sprintf("_sa%d", i)
g.emitExpr(arg)
g.writeln(fmt.Sprintf("%s := t.Pop2()", argNames[i]))
}
g.writeln("if hbrt.IsGoObject(_obj) {")
g.indent++
// Go object: use reflect bridge
argsStr := ""
for i, name := range argNames {
if i > 0 {
argsStr += ", "
}
argsStr += name
}
g.writeln(fmt.Sprintf("_gr := hbrt.GoCallCached(_obj, %q, %s)", e.Method, argsStr))
g.writeln("if len(_gr) > 0 { t.PushValue(_gr[0]) } else { t.PushNil() }")
g.indent--
g.writeln("} else {")
g.indent++
// Harbour object: use Send
g.writeln("t.PushValue(_obj)")
for _, name := range argNames {
g.writeln(fmt.Sprintf("t.PushValue(%s)", name))
}
g.writeln(fmt.Sprintf("t.Send(%q, %d)", e.Method, len(e.Args)))
g.indent--
g.writeln("}")
g.indent--
g.writeln("}")
}
func (g *Generator) emitBlock(e *ast.BlockExpr) {
// Code block: {|params| body}
// Block params are passed via Frame() from Eval/AEval.
nParams := len(e.Params)
// Collect free variables in the block body that reference outer locals.
// These need to be captured via Go closure variables.
outerLocals := g.curLocals
blockLocals := make(localMap)
for i, p := range e.Params {
blockLocals[strings.ToUpper(p)] = i + 1
}
// Find all idents in block body that are in outerLocals but NOT in blockLocals
freeVars := g.collectFreeVars(e.Body, blockLocals, outerLocals)
// Harbour: closures share outer locals via RefCell (mutable capture).
// Convert each captured outer local to a RefCell, then pass the RefCell
// into the block. Both outer function and block read/write through it.
for _, fv := range freeVars {
outerIdx := outerLocals[fv]
// Ensure outer local is a RefCell (PushLocalRef creates one if needed,
// but we do it inline to avoid stack ops).
g.writeln(fmt.Sprintf("t.EnsureLocalRef(%d) // share %s via RefCell", outerIdx, fv))
}
// Capture the RefCell values with unique names to avoid Go scope issues.
capSeq := g.blockSeq
g.blockSeq++
capNames := make(map[string]string) // fv → Go var name
for _, fv := range freeVars {
outerIdx := outerLocals[fv]
capName := fmt.Sprintf("_cap_%s_%d", fv, capSeq)
g.writeln(fmt.Sprintf("%s := t.LocalRaw(%d) // capture RefCell %s", capName, outerIdx, fv))
capNames[fv] = capName
}
g.writeln(fmt.Sprintf("t.PushBlock(func(t *hbrt.Thread) {"))
g.indent++
nLocals := len(freeVars)
g.writeln(fmt.Sprintf("t.Frame(%d, %d)", nParams, nLocals))
g.writeln("defer t.EndProc()")
// Inject RefCell values directly into block locals — reads/writes go through RefCell
for i, fv := range freeVars {
localIdx := nParams + i + 1
blockLocals[fv] = localIdx
g.writeln(fmt.Sprintf("t.SetLocalRaw(%d, %s) // inject shared RefCell %s", localIdx, capNames[fv], fv))
}
g.curLocals = blockLocals
g.emitExpr(e.Body)
g.writeln("t.RetValue()")
g.curLocals = outerLocals
g.indent--
g.writeln(fmt.Sprintf("}, %d)", 0))
}
// collectFreeVars finds identifier names in expr that exist in outerLocals but not blockLocals.
func (g *Generator) collectFreeVars(expr ast.Expr, blockLocals, outerLocals localMap) []string {
var result []string
seen := map[string]bool{}
g.walkExprIdents(expr, func(name string) {
upper := strings.ToUpper(name)
if seen[upper] {
return
}
if _, inBlock := blockLocals[upper]; inBlock {
return
}
if _, inOuter := outerLocals[upper]; inOuter {
seen[upper] = true
result = append(result, upper)
}
})
return result
}
// walkExprIdents calls fn for each IdentExpr in the expression tree.
func (g *Generator) walkExprIdents(expr ast.Expr, fn func(string)) {
if expr == nil {
return
}
switch e := expr.(type) {
case *ast.IdentExpr:
fn(e.Name)
case *ast.BinaryExpr:
g.walkExprIdents(e.Left, fn)
g.walkExprIdents(e.Right, fn)
case *ast.UnaryExpr:
g.walkExprIdents(e.X, fn)
case *ast.PostfixExpr:
g.walkExprIdents(e.X, fn)
case *ast.CallExpr:
g.walkExprIdents(e.Func, fn)
for _, a := range e.Args {
g.walkExprIdents(a, fn)
}
case *ast.IndexExpr:
g.walkExprIdents(e.X, fn)
g.walkExprIdents(e.Index, fn)
case *ast.DotExpr:
g.walkExprIdents(e.X, fn)
case *ast.AssignExpr:
g.walkExprIdents(e.Left, fn)
g.walkExprIdents(e.Right, fn)
case *ast.ArrayLitExpr:
for _, item := range e.Items {
g.walkExprIdents(item, fn)
}
case *ast.IIfExpr:
g.walkExprIdents(e.Cond, fn)
g.walkExprIdents(e.True, fn)
g.walkExprIdents(e.False, fn)
case *ast.SendExpr:
g.walkExprIdents(e.Object, fn)
for _, a := range e.Args {
g.walkExprIdents(a, fn)
}
case *ast.AliasExpr:
g.walkExprIdents(e.Alias, fn)
g.walkExprIdents(e.Field, fn)
case *ast.BlockExpr:
g.walkExprIdents(e.Body, fn)
}
}
func (g *Generator) emitBinaryOp(op token.Kind) {
switch op {
case token.PLUS:
g.writeln("t.Plus()")
case token.MINUS:
g.writeln("t.Minus()")
case token.STAR:
g.writeln("t.Mult()")
case token.SLASH:
g.writeln("t.Divide()")
case token.PERCENT:
g.writeln("t.Modulus()")
case token.POWER:
g.writeln("t.Power()")
case token.EQ, token.EXEQ:
g.writeln("t.Equal()")
case token.NEQ:
g.writeln("t.NotEqual()")
case token.LT:
g.writeln("t.Less()")
case token.GT:
g.writeln("t.Greater()")
case token.LTE:
g.writeln("t.LessEqual()")
case token.GTE:
g.writeln("t.GreaterEqual()")
case token.AND:
g.writeln("t.And()")
case token.OR:
g.writeln("t.Or()")
case token.DOLLAR:
g.writeln("t.InString()") // $ operator
// Compound assign ops (shouldn't reach here normally)
case token.PLUSEQ:
g.writeln("t.Plus()")
case token.MINUSEQ:
g.writeln("t.Minus()")
case token.STAREQ:
g.writeln("t.Mult()")
case token.SLASHEQ:
g.writeln("t.Divide()")
default:
g.writeln(fmt.Sprintf("// WARN: unhandled binary op %v", op))
}
}
func (g *Generator) emitUnaryOp(op token.Kind) {
switch op {
case token.MINUS:
g.writeln("t.Negate()")
case token.NOT:
g.writeln("t.Not()")
case token.INC:
g.writeln("t.Inc()")
case token.DEC:
g.writeln("t.Dec()")
default:
g.writeln(fmt.Sprintf("// WARN: unhandled unary op %v", op))
}
}