Files
five/compiler/gengo/gengo.go
CharlesKWON f4ed42556b checkpoint: season-wide bug fix campaign + infra
Cumulative season's silent-bug hunting (~62 fixes) across the FiveSql2
SQL engine, the Five compiler/runtime, and the hbrdd RDD layer. Saved
as a single checkpoint before refactoring the parser to delegate xBase
command translation to the preprocessor.

Highlights:

FiveSql2 engine (_FiveSql2/src/)
- prefix-glob index attach -> explicit convention (<table>_pk.ntx,
  <table>_uq.ntx, <table>.cdx) — fixes silent multi-row INSERT row-drop
- DROP/CREATE TABLE FErase chain extended (.cdx, .fsc, .fsv, .dbt, .fpt)
- COUNT(DISTINCT col) parsed + aggregated via hSeen hash
- UNION column-count mismatch returns SQL_ERR_GRAMMAR (was silent)
- DISTINCT + ORDER BY hidden-col leak fixed (trim before DISTINCT)
- Derived table FROM (SELECT...) + JOIN right-side derived
- Self-FK CASCADE depth 2+ via SqlGetSingleColPK pre-collect
- LAG/LEAD default arg uses SqlEvalRowExpr (handles -N const exprs)
- DATE literal round-trip validation (Feb 29 non-leap rejected)
- CREATE OR REPLACE VIEW; CREATE VIEW errors on already-exists
- AlterTable type dispatcher comma-wrapped (1-char type "A" no longer
  matches CHARACTER)

Compiler / runtime
- gengo: HB_ -> FV_ prefix on emitted Go function names (Five identity)
- gengo split: emit_block.go, emit_stmt.go, folding.go extracted
- parser/stmtreg.go nudges
- hbrt: debug TUI/CLI restructure (debugcmd, debugkey, termios_*),
  windows debug stubs collapsed
- thread/vm/value/class/pcinterp tightening from panic traces

RDD layer (hbrdd/)
- dbf: null bitmap support (null.go + null_test.go), mmap split
  (mmap_posix.go / mmap_windows.go), byte-level numeric parse
- ntx/cdx: windows mmap parity
- workarea + mem RDD: cross-area state-bleed fixes

RTL (hbrtl/)
- errorlog rewrite with platform-specific FD (errorlog_fd_unix /
  errorlog_fd_other)
- sqlscan, sqlhelpers, indexrtl, datetime extensions

Gates green at checkpoint:
- go test ./...        : PASS
- FiveSql2 SQL:1999    : 43/43
- Harbour compat       : 56/56

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 09:26:25 +09:00

1630 lines
48 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 FV_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"
"strconv"
"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
// Per-function constant locals: LOCAL names (uppercase) whose sole
// assignment is a literal initializer. Reads get substituted inline.
constLocals map[string]*ast.LiteralExpr
// Class name of the currently-emitting method body, used to resolve
// ::super: at compile time against the defining class's Parent.
curMethodClass string
}
type symbolEntry struct {
name string
scope string // "hbrt.FsPublic|hbrt.FsLocal" etc.
fn string // Go function name: "FV_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: "FV_" + 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: "FV_" + 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)")
// Pick up any HB_FUNC() registrations from #pragma BEGINDUMP blocks
// — their package init() queues them into hbrt.dynamicFuncs, and
// RegisterLibModules pulls them into this VM.
g.writeln("vm.RegisterLibModules()")
// 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. Extract
// any `import` directives inside the block and fold them into
// the generator's own imports map — Go insists imports live at
// the top of the file, so we can't just splat the raw dump in
// the middle of the declarations.
if decl.Code != "" {
body := hoistGoImports(decl.Code, g.imports)
g.writeln("\n// --- Inline Go code (#pragma BEGINDUMP) ---")
g.write(body)
g.writeln("\n// --- End inline Go code ---\n")
}
}
}
// hoistGoImports scans a raw Go source block for top-level `import`
// statements, registers each imported path in `imports`, and returns
// the body with those directives stripped. Handles both forms:
//
// import "path"
// import (
// "path1"
// alias "path2"
// _ "path3"
// )
//
// Imports in the middle of the dump would violate Go's "imports first"
// rule. Five's own top-of-file import block will already include
// whatever the hoisted set adds, so the end result links correctly.
func hoistGoImports(code string, imports map[string]bool) string {
lines := strings.Split(code, "\n")
var out []string
i := 0
for i < len(lines) {
trim := strings.TrimSpace(lines[i])
// Block form: `import (`
if trim == "import (" || trim == "import(" {
i++
for i < len(lines) {
t := strings.TrimSpace(lines[i])
if t == ")" {
i++
break
}
// Parse `path`, `alias path`, `_ path` — each with
// quoted path as the final token.
if start := strings.Index(t, `"`); start >= 0 {
if end := strings.LastIndex(t, `"`); end > start {
imports[t[start+1:end]] = true
}
}
i++
}
continue
}
// Single form: `import "path"` or `import alias "path"`.
if strings.HasPrefix(trim, "import ") || strings.HasPrefix(trim, "import\t") {
if start := strings.Index(trim, `"`); start >= 0 {
if end := strings.LastIndex(trim, `"`); end > start {
imports[trim[start+1:end]] = true
}
}
i++
continue
}
out = append(out, lines[i])
i++
}
return strings.Join(out, "\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 := "FV_" + 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)
// Also include mid-function LOCALs so their names show up in the
// debugger / error.log for any frame that has executed past the decl.
{
idx := len(g.curLocals) + 1
scanBodyLocals(fn.Body, g.curLocals, &idx)
}
g.emitLocalNames(fn.Name, g.curLocals)
// Scan for LOCALs that are literal-initialised and never reassigned
// so reads can be constant-propagated at emit time.
g.constLocals = collectConstLocals(fn)
// Emit LOCAL initializers. LOCALs that were const-propagated have
// their reads substituted inline, so the init store has no live
// reader — skip it (dead-store elimination).
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 {
if _, isConst := g.constLocals[strings.ToUpper(v.Name)]; !isConst {
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("")
// Drop per-function constant map so it doesn't leak to siblings.
g.constLocals = nil
// 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
// emitLocalNames tells the runtime the PRG-level names of this frame's
// params+locals, so the debugger and error.log can show "i, nSum"
// instead of "_1, _2". Emitted as an inline slice literal — one
// allocation per call (24-byte slice header); string literals are
// interned so the underlying data sits in the binary's data segment.
func (g *Generator) emitLocalNames(funcName string, locals localMap) {
if len(locals) == 0 {
return
}
// Reverse the map: slot index → PRG name.
names := make([]string, len(locals))
for name, idx := range locals {
if idx >= 1 && idx <= len(names) {
names[idx-1] = name
}
}
// Drop trailing empty slots (defensive — map may have holes).
for len(names) > 0 && names[len(names)-1] == "" {
names = names[:len(names)-1]
}
if len(names) == 0 {
return
}
var sb strings.Builder
sb.WriteString("t.SetLocalNames([]string{")
for i, n := range names {
if i > 0 {
sb.WriteString(", ")
}
sb.WriteString(strconv.Quote(n))
}
sb.WriteString("})")
g.writeln(sb.String())
}
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)
}
}
}
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:
// Compile-time constant folding. Fold the whole subtree so
// `(2*3) + 5` collapses to PushInt(11) and so string-concat
// tails like `x + "b" + "c"` reassociate into `x + "bc"`.
// Mutates the AST in place — safe because the generator owns
// the tree post-parse.
switch folded := foldLiteralTree(e).(type) {
case *ast.LiteralExpr:
g.emitLiteral(folded)
return
case *ast.BinaryExpr:
e = folded
}
// Short-circuit AND/OR: Harbour evaluates right operand only if needed.
// With a literal LHS we can skip the PushBool/PopLogical roundtrip
// entirely — .T. .AND. B folds to B, .F. .AND. B folds to false,
// and symmetrically for OR.
if e.Op == token.AND {
if v, ok := boolLiteralValue(e.Left); ok {
if v {
g.emitExpr(e.Right)
} else {
g.writeln("t.PushBool(false)")
}
break
}
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 {
if v, ok := boolLiteralValue(e.Left); ok {
if v {
g.writeln("t.PushBool(true)")
} else {
g.emitExpr(e.Right)
}
break
}
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:
// Fold `-<literal>` to a negative literal at compile time so
// `-42` becomes PushInt(-42) instead of PushInt(42) + Negate().
if e.Op == token.MINUS {
if lit, ok := e.X.(*ast.LiteralExpr); ok {
if neg, ok := negateLiteral(lit); ok {
g.emitLiteral(neg)
break
}
}
}
// Fold `.NOT. .T.` → .F. and `.NOT. .F.` → .T. at compile time.
if e.Op == token.NOT {
if v, ok := boolLiteralValue(e.X); ok {
if v {
g.writeln("t.PushBool(false)")
} else {
g.writeln("t.PushBool(true)")
}
break
}
}
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:
// &ident / &(expr) — evaluate the inner expression to get the
// source string, then MacroPush compiles and runs it via the
// hbrtl-installed evaluator hook.
g.emitExpr(e.Expr)
g.writeln("t.MacroPush()")
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:
// `x++` / `x--` as a sub-expression (e.g. `y := x++`): push the
// ORIGINAL value for the surrounding expression to consume, then
// mutate the underlying storage so the next read sees the new
// value. The earlier Dup+Inc+Pop form only incremented a copy
// on the stack — the variable itself was never updated, so
// `y := x++` silently left x unchanged and `x++ + x++` read the
// same value twice. Matches Clipper / C post-increment
// semantics.
delta := "1"
if e.Op == token.DEC {
delta = "-1"
}
if ident, ok := e.X.(*ast.IdentExpr); ok {
upper := strings.ToUpper(ident.Name)
if idx, found := g.curLocals[upper]; found {
g.writeln(fmt.Sprintf("t.PushLocalFast(%d)", idx))
g.writeln(fmt.Sprintf("t.LocalAddInt(%d, %s)", idx, delta))
return
}
if goVar, found := g.staticVars[upper]; found {
g.writeln(fmt.Sprintf("t.PushValue(%s)", goVar))
g.writeln(fmt.Sprintf("{ _v := %s.AsNumInt() + %s; %s = hbrt.MakeInt(int(_v)) }", goVar, delta, goVar))
return
}
}
if send, ok := e.X.(*ast.SendExpr); ok {
if _, isSelf := send.Object.(*ast.SelfExpr); isSelf {
fieldName := strings.ToUpper(send.Method)
// Push old value first (for outer expression),
// then read-modify-write the field.
g.writeln(fmt.Sprintf("t.PushSelfField(%q)", fieldName))
g.writeln(fmt.Sprintf("t.PushSelfField(%q)", fieldName))
if e.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
}
}
// Fallback: expression has no assignable target (e.g. a
// parenthesised constant) — emit a no-op modification so the
// surrounding expression still sees the value.
g.emitExpr(e.X)
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.
// The emitted code uses strings.ToUpper, so flag the import here
// — without this the deferred-import patcher leaves a stub.
g.imports["strings"] = true
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
}
// Constant-propagate literal-init LOCALs that collectConstLocals
// proved are never reassigned or passed by reference. Emitting the
// literal inline lets downstream folding (dead IF, AND/OR, FOR
// fused ops) fire on what was a variable reference.
if lit, ok := g.constLocals[upper]; ok {
g.emitLiteral(lit)
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 {
upper := strings.ToUpper(aliasIdent.Name)
if upper == "M" || upper == "MEMVAR" {
// Memvar write — leaves a copy on the stack for the
// enclosing expression to pick up (matches Dup+SetAlias
// path).
g.emitExpr(e.Right)
g.writeln("t.Dup()")
g.writeln(fmt.Sprintf(`t.PopMemvar(%q)`, fieldIdent.Name))
return
}
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",
"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) 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))
}
}