Files
five/compiler/gengo/gengo.go
CharlesKWON 1f63c7fe63 perf(vm): symbol hoist + Function() stack shift — global 3-15%
The VM call path (PushSymbol → Function → Frame) is traversed by every
PRG function call. Three changes together cut per-call overhead across
the entire bench suite.

Changes
 - hbrt/call.go Function(): replace pop-push dance with a single slice
   shift (N+2 pops + N pushes → 1 copy of N slots + sp adjust). Kills
   the per-call `make([]Value, nArgs)` heap alloc. Resolved function
   pointer is cached back into sym.Func so subsequent calls on the
   same Symbol skip the VM lookup entirely.
 - hbrt/vm.go GetSym(): new helper. Generated code calls it with a
   pointer to a package-level `*Symbol` slot so FindSymbol (which takes
   the VM RWMutex + map lookup) runs at most once per symbol per
   process. Nil results are intentionally NOT cached — an init-order
   miss becomes a retry on the next call instead of a permanent sticky
   failure.
 - hbrt/thread.go pushPendingSym(): scalar fast slot for depth=1 call
   nesting (common case). Nil syms still go through the slice so the
   "empty vs stored nil" ambiguity can't produce a false pop.
 - compiler/gengo/gengo.go: emit `t.PushSymbol(t.GetSym(&_sym_<file>_<NAME>, "NAME"))`
   for every function call site, with a per-file prefix so multi-PRG
   builds don't collide on identical symbol names.

Bugs fixed during bring-up
 - pendingSymFast == nil was ambiguous ("unused" vs "nil stored"). Nil
   syms now spill to the slice, preserving distinguishability.
 - The old varName-reuse branch at the PushSymbol emit site skipped
   the GetSym wrapper, emitting a raw `t.PushSymbol(varName)` against
   an uninitialized package-level *Symbol. Every call path now funnels
   through emitPushSymbol.

bench_sql deltas vs prior build
 - B1  SELECT *          114 →  97 µs   (15%)
 - B4  GROUP_HAVING      584 → 554 µs   (5%)
 - B8  RECURSIVE CTE     150 → 141 µs   (6%)
 - B10 RANK PARTITION    310 → 296 µs   (5%)
 - B11 SUM OVER          335 → 320 µs   (4%)
 - B14 COUNT             295 → 281 µs   (5%)
 - B15 CTE+WIN+JOIN     1891 → 1826 µs  (3%)

Verification
 - go test ./...               ALL PASS
 - FiveSql2 test_sql1999       43/43
 - tests/compat_harbour        56/56

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 20:41:48 +09:00

2715 lines
75 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"
"sort"
"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
}
}
if hasXBaseCommands(file) {
// Blank-import the in-memory RDD so MEMRDD / "mem:" paths work
// from PRG (the driver registers itself in its init).
g.imports["five/hbrdd/mem"] = true
g.importAlias["five/hbrdd/mem"] = "_"
}
g.symCache = map[string]string{}
g.emitHeader()
g.emitSymbols()
for _, d := range file.Decls {
g.emitDecl(d)
}
g.emitFastFuncRegistrations()
if library {
g.emitInitModule()
} else {
g.emitMain()
}
g.emitSymCache()
// 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...)
}
// symVar returns the package-level cache variable name for the given
// symbol, registering it if first seen. The name embeds a per-file
// prefix so multi-PRG builds don't collide on identical symbol names
// across files. Characters that can't appear in a Go identifier are
// replaced with underscores.
func (g *Generator) symVar(name string) string {
if g.symCache == nil {
g.symCache = map[string]string{}
}
if v, ok := g.symCache[name]; ok {
return v
}
var sb strings.Builder
sb.WriteString("_sym_")
sb.WriteString(g.fileKey())
sb.WriteByte('_')
for i := 0; i < len(name); i++ {
c := name[i]
switch {
case c >= 'A' && c <= 'Z', c >= 'a' && c <= 'z', c >= '0' && c <= '9', c == '_':
sb.WriteByte(c)
default:
sb.WriteByte('_')
}
}
v := sb.String()
g.symCache[name] = v
return v
}
// fileKey derives a short identifier-safe prefix from g.file.Name so
// package-level symbol caches don't collide across PRG files.
func (g *Generator) fileKey() string {
base := g.file.Name
if idx := strings.LastIndex(base, "/"); idx >= 0 {
base = base[idx+1:]
}
if idx := strings.LastIndexByte(base, '.'); idx >= 0 {
base = base[:idx]
}
var sb strings.Builder
for i := 0; i < len(base); i++ {
c := base[i]
switch {
case c >= 'A' && c <= 'Z', c >= 'a' && c <= 'z', c >= '0' && c <= '9':
sb.WriteByte(c)
default:
sb.WriteByte('_')
}
}
return sb.String()
}
// emitPushSymbol writes PushSymbol using the lazy-cached package-level
// variable. First call resolves via VM; subsequent calls skip the
// RWMutex + map lookup.
func (g *Generator) emitPushSymbol(name string) {
v := g.symVar(name)
g.writeln(fmt.Sprintf("t.PushSymbol(t.GetSym(&%s, %q))", v, name))
}
// emitSymCache writes the package-level `var _sym_NAME *hbrt.Symbol`
// declarations discovered during body emission. Called after all
// function bodies are emitted so every PushSymbol call site has had
// a chance to register its target name.
func (g *Generator) emitSymCache() {
if len(g.symCache) == 0 {
return
}
// Deterministic order so diffs are stable.
names := make([]string, 0, len(g.symCache))
for k := range g.symCache {
names = append(names, k)
}
sort.Strings(names)
g.writeln("")
g.writeln("// Cached symbol pointers — populated lazily on first use.")
for _, n := range names {
g.writeln(fmt.Sprintf("var %s *hbrt.Symbol", g.symCache[n]))
}
g.writeln("")
}
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)
// Emit function-level STATIC variables as package-level Go vars.
// Harbour: STATIC inside FUNCTION persists across calls but is
// scoped to the function. We prefix with funcname to avoid clashes.
for _, d := range fn.Decls {
if vd, ok := d.(*ast.VarDecl); ok && vd.Scope == ast.ScopeStatic {
for _, v := range vd.Vars {
varName := "static_" + strings.ToUpper(fn.Name) + "_" + 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))
if g.staticVars == nil {
g.staticVars = make(map[string]string)
}
g.staticVars[strings.ToUpper(v.Name)] = varName
}
}
}
// Also scan body for mid-function STATIC declarations
for _, s := range fn.Body {
if vd, ok := s.(*ast.VarDecl); ok && vd.Scope == ast.ScopeStatic {
for _, v := range vd.Vars {
varName := "static_" + strings.ToUpper(fn.Name) + "_" + 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))
if g.staticVars == nil {
g.staticVars = make(map[string]string)
}
g.staticVars[strings.ToUpper(v.Name)] = varName
}
}
}
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("")
// Remove function-scoped STATIC vars from the map so they don't
// leak into other functions that happen to share a variable name.
for _, d := range fn.Decls {
if vd, ok := d.(*ast.VarDecl); ok && vd.Scope == ast.ScopeStatic {
for _, v := range vd.Vars {
delete(g.staticVars, strings.ToUpper(v.Name))
}
}
}
for _, s := range fn.Body {
if vd, ok := s.(*ast.VarDecl); ok && vd.Scope == ast.ScopeStatic {
for _, v := range vd.Vars {
delete(g.staticVars, strings.ToUpper(v.Name))
}
}
}
}
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++
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("}")
// 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})",
forExpr, s.TagName, 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.emitPushSymbol(funcName)
g.writeln("t.PushNil()")
g.writeln(fmt.Sprintf("t.PushBool(%s)", val))
g.writeln("t.Do(1)")
}
break
}
// Value SET commands — SET DATE/DECIMALS/EPOCH TO expr
valueFuncMap := map[string]string{
"DATE": "__SETDATEFORMAT",
"DECIMALS": "SETDECIMALS",
"EPOCH": "SETEPOCH",
}
if funcName, ok := valueFuncMap[upper]; ok && s.Expr != nil {
g.emitPushSymbol(funcName)
g.writeln("t.PushNil()")
g.emitExpr(s.Expr)
g.writeln("t.Do(1)")
break
}
// Workarea-specific SET commands
g.writeln("{")
g.indent++
g.writeln("wa := t.WA.(*hbrdd.WorkAreaManager)")
g.writeln("if area := wa.Current(); area != nil {")
g.indent++
switch upper {
case "FILTER":
if s.Expr != nil {
g.emitExpr(s.Expr)
g.writeln(`area.SetFilter(t.Pop2().AsString(), nil)`)
} else {
g.writeln("area.ClearFilter()")
}
case "ORDER":
if s.Expr != nil {
g.writeln("if idx, ok := area.(hbrdd.Indexer); ok {")
g.indent++
g.emitExpr(s.Expr)
g.writeln(`{ _ov := t.Pop2(); var _os string; if _ov.IsNumeric() { _os = hbrt.NtoS(_ov.AsNumInt()) } else { _os = _ov.AsString() }; idx.OrderListFocus(_os) }`)
g.indent--
g.writeln("}")
}
case "INDEX":
if s.Expr != nil {
fileStr := exprToString(s.Expr)
g.writeln("if idx, ok := area.(hbrdd.Indexer); ok {")
g.indent++
if fileStr != "" {
// SET INDEX TO a, b, c — split comma-separated file names
// and call OrderListAdd for each. Harbour loads all NTX
// files into the active index list.
clean := fileStr
if len(clean) >= 2 && clean[0] == '"' && clean[len(clean)-1] == '"' {
clean = clean[1 : len(clean)-1]
}
parts := strings.Split(clean, ",")
for _, p := range parts {
p = strings.TrimSpace(p)
if p != "" {
g.writeln(fmt.Sprintf(`idx.OrderListAdd(%q)`, p))
}
}
} else {
g.emitExpr(s.Expr)
g.writeln(`idx.OrderListAdd(t.Pop2().AsString())`)
}
g.indent--
g.writeln("}")
} else {
g.writeln("if idx, ok := area.(hbrdd.Indexer); ok { idx.OrderListClear() }")
}
default:
g.writeln(fmt.Sprintf("// SET %s: not yet implemented", upper))
}
g.indent--
g.writeln("}")
g.indent--
g.writeln("}")
case *ast.SeqStmt:
g.emitBeginSequence(s, locals)
case *ast.AtSayCmd:
g.emitAtSayCmd(s)
case *ast.AtGetCmd:
g.emitAtGetCmd(s, locals)
case *ast.AtSayGetCmd:
g.emitAtSayGetCmd(s, locals)
case *ast.ReadCmd:
g.emitReadCmd(s, locals)
default:
g.writeln(fmt.Sprintf("// WARN: unhandled statement type %T — skipped", stmt))
}
}
func (g *Generator) emitMidVarDecl(s *ast.VarDecl, locals localMap) {
for _, v := range s.Vars {
idx, found := locals[strings.ToUpper(v.Name)]
if !found {
maxIdx := 0
for _, i := range locals {
if i > maxIdx {
maxIdx = i
}
}
idx = maxIdx + 1
locals[strings.ToUpper(v.Name)] = idx
}
if v.Init != nil {
g.emitExpr(v.Init)
g.writeln(fmt.Sprintf("t.PopLocalFast(%d)", idx))
}
}
}
func (g *Generator) emitQOut(s *ast.QOutStmt, locals localMap) {
sym := "QOUT"
if s.IsQQ {
sym = "QQOUT"
}
g.emitPushSymbol(sym)
g.writeln("t.PushNil()")
for _, expr := range s.Exprs {
g.emitExpr(expr)
}
g.writeln(fmt.Sprintf("t.Function(%d)", len(s.Exprs)))
}
func (g *Generator) emitExprStmt(s *ast.ExprStmt, locals localMap) {
// Check if it's an assignment
if assign, ok := s.X.(*ast.AssignExpr); ok {
g.emitAssign(assign, locals)
return
}
// Check if it's a function call (discard result)
if call, ok := s.X.(*ast.CallExpr); ok {
g.emitCallAsStmt(call, locals)
return
}
// Bare identifier as statement (e.g., CLS, CLEAR) — treat as zero-arg function call
if ident, ok := s.X.(*ast.IdentExpr); ok {
if _, found := locals[strings.ToUpper(ident.Name)]; !found {
g.emitPushSymbol(strings.ToUpper(ident.Name))
g.writeln("t.PushNil()")
g.writeln("t.Do(0)")
return
}
}
// Postfix ++/--
if pf, ok := s.X.(*ast.PostfixExpr); ok {
// Local variable: n++
if ident, ok := pf.X.(*ast.IdentExpr); ok {
upper := strings.ToUpper(ident.Name)
if idx, found := locals[upper]; found {
if pf.Op == token.INC {
g.writeln(fmt.Sprintf("t.LocalAddInt(%d, 1)", idx))
} else {
g.writeln(fmt.Sprintf("t.LocalAddInt(%d, -1)", idx))
}
return
}
// STATIC variable: s_nPass++
if goVar, found := g.staticVars[upper]; found {
delta := "1"
if pf.Op == token.DEC {
delta = "-1"
}
g.writeln(fmt.Sprintf("{ _v := %s.AsNumInt() + %s; %s = hbrt.MakeInt(int(_v)) }", goVar, delta, goVar))
return
}
}
// Self field: ::field++
if send, ok := pf.X.(*ast.SendExpr); ok {
if _, isSelf := send.Object.(*ast.SelfExpr); isSelf {
fieldName := strings.ToUpper(send.Method)
g.writeln(fmt.Sprintf("t.PushSelfField(%q)", fieldName))
if pf.Op == token.INC {
g.writeln("t.PushInt(1)")
g.writeln("t.Plus()")
} else {
g.writeln("t.PushInt(1)")
g.writeln("t.Minus()")
}
g.writeln(fmt.Sprintf("t.SetSelfField(%q)", fieldName))
return
}
}
}
// General expression (result on stack, pop it)
g.emitExpr(s.X)
g.writeln("t.Pop()")
}
func (g *Generator) emitAssign(a *ast.AssignExpr, locals localMap) {
// Check for arr[idx] := value (array index assignment)
if idx, ok := a.Left.(*ast.IndexExpr); ok {
if a.Op == token.ASSIGN {
g.emitExpr(idx.X) // array
g.emitExpr(idx.Index) // index
g.emitExpr(a.Right) // value
g.writeln("t.ArrayPop()") // set array[index] = value
return
}
}
// Check for obj:field := value (object field assignment)
if send, ok := a.Left.(*ast.SendExpr); ok {
_, isSelf := send.Object.(*ast.SelfExpr)
if isSelf {
fieldName := strings.ToUpper(send.Method)
switch a.Op {
case token.ASSIGN:
g.emitExpr(a.Right)
g.writeln(fmt.Sprintf("t.SetSelfField(%q)", fieldName))
case token.PLUSEQ:
g.writeln(fmt.Sprintf("t.PushSelfField(%q)", fieldName))
g.emitExpr(a.Right)
g.writeln("t.Plus()")
g.writeln(fmt.Sprintf("t.SetSelfField(%q)", fieldName))
case token.MINUSEQ:
g.writeln(fmt.Sprintf("t.PushSelfField(%q)", fieldName))
g.emitExpr(a.Right)
g.writeln("t.Minus()")
g.writeln(fmt.Sprintf("t.SetSelfField(%q)", fieldName))
case token.STAREQ:
g.writeln(fmt.Sprintf("t.PushSelfField(%q)", fieldName))
g.emitExpr(a.Right)
g.writeln("t.Mult()")
g.writeln(fmt.Sprintf("t.SetSelfField(%q)", fieldName))
case token.SLASHEQ:
g.writeln(fmt.Sprintf("t.PushSelfField(%q)", fieldName))
g.emitExpr(a.Right)
g.writeln("t.Divide()")
g.writeln(fmt.Sprintf("t.SetSelfField(%q)", fieldName))
default:
g.emitExpr(a.Right)
g.writeln(fmt.Sprintf("t.SetSelfField(%q)", fieldName))
}
return
}
// Non-self: obj:field := value → obj:_FIELD(value)
if a.Op == token.ASSIGN {
g.emitExpr(send.Object)
g.emitExpr(a.Right)
g.writeln(fmt.Sprintf("t.Send(%q, 1)", "_"+strings.ToUpper(send.Method)))
g.writeln("t.Pop() // discard setter result")
return
}
}
// Check for alias->field := value (FIELD->NAME := value)
if aliasExpr, ok := a.Left.(*ast.AliasExpr); ok {
if aliasIdent, ok2 := aliasExpr.Alias.(*ast.IdentExpr); ok2 {
if fieldIdent, ok3 := aliasExpr.Field.(*ast.IdentExpr); ok3 {
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 or function-level STATIC variable
upper := strings.ToUpper(ident.Name)
if goVar, found := g.staticVars[upper]; found {
switch a.Op {
case token.ASSIGN:
g.emitExpr(a.Right)
g.writeln(fmt.Sprintf("%s = t.Pop2()", goVar))
case token.PLUSEQ:
g.writeln(fmt.Sprintf("t.PushValue(%s)", goVar))
g.emitExpr(a.Right)
g.writeln("t.Plus()")
g.writeln(fmt.Sprintf("%s = t.Pop2()", goVar))
case token.MINUSEQ:
g.writeln(fmt.Sprintf("t.PushValue(%s)", goVar))
g.emitExpr(a.Right)
g.writeln("t.Minus()")
g.writeln(fmt.Sprintf("%s = t.Pop2()", goVar))
case token.STAREQ:
g.writeln(fmt.Sprintf("t.PushValue(%s)", goVar))
g.emitExpr(a.Right)
g.writeln("t.Mult()")
g.writeln(fmt.Sprintf("%s = t.Pop2()", goVar))
case token.SLASHEQ:
g.writeln(fmt.Sprintf("t.PushValue(%s)", goVar))
g.emitExpr(a.Right)
g.writeln("t.Divide()")
g.writeln(fmt.Sprintf("%s = t.Pop2()", goVar))
default:
g.writeln(fmt.Sprintf("t.PushValue(%s)", goVar))
g.emitExpr(a.Right)
g.emitBinaryOp(a.Op)
g.writeln(fmt.Sprintf("%s = t.Pop2()", goVar))
}
return
}
}
// Fallback: general assignment via stack
g.emitExpr(a.Right)
g.writeln("// WARN: complex assignment target — simplified")
g.writeln("t.Pop()")
}
func (g *Generator) emitCallAsStmt(call *ast.CallExpr, locals localMap) {
if ident, ok := call.Func.(*ast.IdentExpr); ok {
g.emitPushSymbol(strings.ToUpper(ident.Name))
} else {
g.emitExpr(call.Func)
}
g.writeln("t.PushNil()")
for _, arg := range call.Args {
g.emitExpr(arg)
}
g.writeln(fmt.Sprintf("t.Do(%d)", len(call.Args)))
}
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))
}
}
// emitIndexKeyExpr emits Go code that evaluates an INDEX ON key expression.
// Unlike emitExpr, bare identifiers (IdentExpr) are treated as DBF FIELD
// names — not local variables — because INDEX ON operates in field context.
// Function calls, UPPER/LOWER wrappers, and binary ops delegate to emitExpr
// (which handles them identically regardless of context).
func (g *Generator) emitIndexKeyExpr(expr ast.Expr) {
switch e := expr.(type) {
case *ast.IdentExpr:
// Bare identifier in INDEX = field name → runtime FieldGet by name
fieldName := strings.ToUpper(e.Name)
g.writeln(fmt.Sprintf(`{ _wa := t.WA.(*hbrdd.WorkAreaManager); if _a := _wa.Current(); _a != nil { for _fi := 0; _fi < _a.FieldCount(); _fi++ { if strings.ToUpper(_a.GetFieldInfo(_fi).Name) == %q { _v, _ := _a.GetValue(_fi); t.PushValue(_v); break } } } }`, fieldName))
case *ast.BinaryExpr:
// Recurse with field-aware emitter for both sides
g.emitIndexKeyExpr(e.Left)
g.emitIndexKeyExpr(e.Right)
g.emitBinaryOp(e.Op)
case *ast.CallExpr:
// Function call: emit normally (symbol + args + Function)
// But args might contain field refs, so use indexKeyExpr for args
if ident, ok := e.Func.(*ast.IdentExpr); ok {
upper := strings.ToUpper(ident.Name)
// Inline UPPER/LOWER for single-arg calls on fields
if (upper == "UPPER" || upper == "LOWER") && len(e.Args) == 1 {
g.emitIndexKeyExpr(e.Args[0])
if upper == "UPPER" {
g.writeln("{ _s := t.Pop2().AsString(); t.PushString(strings.ToUpper(_s)) }")
} else {
g.writeln("{ _s := t.Pop2().AsString(); t.PushString(strings.ToLower(_s)) }")
}
return
}
g.emitPushSymbol(upper)
} else {
g.emitExpr(e.Func)
}
g.writeln("t.PushNil()")
for _, arg := range e.Args {
g.emitIndexKeyExpr(arg)
}
g.writeln(fmt.Sprintf("t.Function(%d)", len(e.Args)))
case *ast.AliasExpr:
// FIELD->NAME or alias->field — delegate to standard emitter
g.emitExpr(expr)
default:
// Literals, etc. — standard emitter works fine
g.emitExpr(expr)
}
}
// exprToString extracts a string representation from an AST expression.
// containsCall reports whether an expression contains any function call.
// Used to distinguish runtime-evaluated INDEX ON filenames
// (`TO ( Lower(cTable) + "_pk.ntx" )`) from static ones (`TO test.ntx`).
func containsCall(expr ast.Expr) bool {
if expr == nil {
return false
}
switch e := expr.(type) {
case *ast.CallExpr:
return true
case *ast.BinaryExpr:
return containsCall(e.Left) || containsCall(e.Right)
case *ast.UnaryExpr:
return containsCall(e.X)
case *ast.SendExpr:
return true
}
return false
}
// 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 {
// Unresolved at compile time — fall back to runtime memvar lookup.
// Harbour semantics: undefined identifiers try PRIVATE/PUBLIC memvar
// tables at runtime; missing → NIL. Prior behavior was PushLocal(0)
// which crashed any code path that actually reached the reference
// (e.g. missing #include, typo'd constant). PushMemvar returns NIL
// for missing names, matching Harbour's forgiving semantics.
g.writeln(fmt.Sprintf("t.PushMemvar(%q) // unresolved: fall through to memvar", 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
}
g.emitPushSymbol(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))
}
}