Files
five/compiler/gengo/gengo.go
CharlesKWON b1024c5244 fix(gengo): hoist #pragma BEGINDUMP imports + wire HB_FUNC registration
Two bugs blocked Five's own inline-Go feature:

1. Inline Go blocks placed mid-file couldn't carry an `import` list
   because Go rejects declarations before imports in the same file.
   examples/godump_demo.prg and friends (real Five demos) hit
   "syntax error: imports must appear before other declarations"
   during compile of the generated Go.

   hoistGoImports parses the raw dump body for `import (...)` blocks
   and single-form `import "path"` lines, registers each path into
   the generator's imports map, and returns the body with those
   directives stripped. The top-of-file import block then carries
   everything the dump needs.

2. HB_FUNC() calls inside the inline block's init() enqueue
   registrations into hbrt.dynamicFuncs, but the VM only promotes
   them to its symbol table when RegisterLibModules() is called.
   gengo's generated main() skipped that step, so dispatch on the
   inline-defined names panicked with "no function symbol for call".
   Emit vm.RegisterLibModules() after RegisterModule(symbols).

Verified: examples/godump_demo.prg builds and runs; the inline
GoUpper / GoFib / GoGCD / GoSplit / GoSquare / GoTypeOf functions
all dispatch. Matches the feature's original design intent.

FiveSql2 43/43, Harbour compat 56/56, Go test ALL PASS.

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

3484 lines
97 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"
"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: "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))
}
// negateLiteral produces a new literal that represents -lit. Handles
// INT and DOUBLE (as a textual prefix). Returns (nil, false) for
// non-numeric literals or an already-negative INT whose negation would
// overflow int64.
func negateLiteral(lit *ast.LiteralExpr) (*ast.LiteralExpr, bool) {
switch lit.Kind {
case token.INT:
n, err := strconv.ParseInt(lit.Value, 10, 64)
if err != nil {
return nil, false
}
// Guard: math.MinInt64 has no positive twin — let the VM's
// runtime coerce-to-double path handle it.
if n == -1<<63 {
return nil, false
}
return &ast.LiteralExpr{
ValuePos: lit.ValuePos,
Kind: token.INT,
Value: strconv.FormatInt(-n, 10),
}, true
case token.DOUBLE:
// Syntactically prefix `-` or flip an existing leading `-`.
if strings.HasPrefix(lit.Value, "-") {
return &ast.LiteralExpr{
ValuePos: lit.ValuePos,
Kind: token.DOUBLE,
Value: lit.Value[1:],
}, true
}
return &ast.LiteralExpr{
ValuePos: lit.ValuePos,
Kind: token.DOUBLE,
Value: "-" + lit.Value,
}, true
}
return nil, false
}
// foldLiteralTree recursively folds BinaryExpr subtrees into LiteralExpr
// where both operands eventually collapse to literals. Non-foldable
// subtrees come back unchanged. Used as a preorder pre-pass so the
// caller can look at a flat LITERAL + LITERAL pair.
//
// For left-associative string-concat chains like "a" + x + "b" + "c",
// the parser builds (((("a" + x) + "b") + "c")) and no pair is
// literal+literal. We reassociate: if the LHS is `Y + strlit` and the
// RHS is a string literal, rewrite as `Y + (strlit+rhslit)` so the
// tail literals collapse. Only safe for STRING+STRING (numeric `+`
// cares about types / overflow).
func foldLiteralTree(e ast.Expr) ast.Expr {
be, ok := e.(*ast.BinaryExpr)
if !ok {
return e
}
be.Left = foldLiteralTree(be.Left)
be.Right = foldLiteralTree(be.Right)
if folded, ok := tryFoldBinary(be); ok {
return folded
}
// String-concat reassociation for left-leaning chains.
if be.Op == token.PLUS {
if rLit, ok := be.Right.(*ast.LiteralExpr); ok && rLit.Kind == token.STRING {
if lBin, ok := be.Left.(*ast.BinaryExpr); ok && lBin.Op == token.PLUS {
if mLit, ok := lBin.Right.(*ast.LiteralExpr); ok && mLit.Kind == token.STRING {
fused := &ast.LiteralExpr{
ValuePos: mLit.ValuePos,
Kind: token.STRING,
Value: mLit.Value + rLit.Value,
}
return &ast.BinaryExpr{
OpPos: be.OpPos,
Op: token.PLUS,
Left: lBin.Left,
Right: fused,
}
}
}
}
}
return be
}
// tryFoldBinary returns a synthetic LiteralExpr when both operands of a
// BinaryExpr are themselves literals and the operator is one the
// folder recognises. INT+INT stays INT (with overflow falling through
// to the VM path), mixed numeric falls to double, STRING+STRING
// concatenates. Non-literal operands or unsupported op → (nil, false).
func tryFoldBinary(e *ast.BinaryExpr) (*ast.LiteralExpr, bool) {
l, lok := e.Left.(*ast.LiteralExpr)
r, rok := e.Right.(*ast.LiteralExpr)
if !lok || !rok {
return nil, false
}
switch e.Op {
case token.PLUS, token.MINUS, token.STAR, token.SLASH:
default:
return nil, false
}
// INT + INT — keep int exact result.
if l.Kind == token.INT && r.Kind == token.INT {
li, errL := strconv.ParseInt(l.Value, 10, 64)
ri, errR := strconv.ParseInt(r.Value, 10, 64)
if errL != nil || errR != nil {
return nil, false
}
var result int64
var overflowed bool
switch e.Op {
case token.PLUS:
result = li + ri
// Harbour overflow discipline: fall through to VM on overflow
if (ri >= 0 && result < li) || (ri < 0 && result > li) {
overflowed = true
}
case token.MINUS:
result = li - ri
if (ri <= 0 && result < li) || (ri > 0 && result > li) {
overflowed = true
}
case token.STAR:
if li == 0 || ri == 0 {
result = 0
} else {
result = li * ri
if result/li != ri {
overflowed = true
}
}
case token.SLASH:
// Harbour SLASH always yields double even for int inputs.
return nil, false
}
if overflowed {
return nil, false
}
return &ast.LiteralExpr{
ValuePos: l.ValuePos,
Kind: token.INT,
Value: strconv.FormatInt(result, 10),
}, true
}
// STRING + STRING — concatenate. Preserves the quoting style of the
// left literal so DateExpr and other quoting-sensitive kinds don't
// change shape.
if e.Op == token.PLUS && l.Kind == token.STRING && r.Kind == token.STRING {
return &ast.LiteralExpr{
ValuePos: l.ValuePos,
Kind: token.STRING,
Value: l.Value + r.Value,
}, true
}
return nil, false
}
// collectConstLocals returns a map of LOCAL names (uppercase) whose
// only assignment is a literal initializer — these can be propagated
// inline. Any reassignment, ++/--, += family, @byref, MultiAssignStmt
// target, FOR/FOREACH loop var, or AtGet target disqualifies the name.
//
// The walker is bounded: if it encounters a macro expansion or any
// AST node it doesn't recognise, it aborts and returns an empty map.
// Correctness trumps coverage — an unrecognised node might hide a
// write, so we refuse to propagate.
func collectConstLocals(fn *ast.FuncDecl) map[string]*ast.LiteralExpr {
v := &constLocalVisitor{
candidates: map[string]*ast.LiteralExpr{},
}
// Seed candidates from top-level LOCAL decls with literal init.
for _, d := range fn.Decls {
vd, ok := d.(*ast.VarDecl)
if !ok || vd.Scope != ast.ScopeLocal {
continue
}
for _, vi := range vd.Vars {
if vi.Init == nil {
continue
}
if lit, ok := vi.Init.(*ast.LiteralExpr); ok {
v.candidates[strings.ToUpper(vi.Name)] = lit
}
}
}
if len(v.candidates) == 0 {
return nil
}
// Params are writable even without explicit assignment (by-value
// but reassignable) — disqualify any candidate that shadows a param.
// Params come from a separate slot but guard in case of odd decls.
for _, p := range fn.Params {
delete(v.candidates, strings.ToUpper(p.Name))
}
for _, st := range fn.Body {
v.stmt(st)
if v.aborted {
return nil
}
}
if len(v.candidates) == 0 {
return nil
}
return v.candidates
}
type constLocalVisitor struct {
candidates map[string]*ast.LiteralExpr
aborted bool
}
func (v *constLocalVisitor) abort() {
v.aborted = true
v.candidates = nil
}
func (v *constLocalVisitor) writeIdent(e ast.Expr) {
if id, ok := e.(*ast.IdentExpr); ok {
delete(v.candidates, strings.ToUpper(id.Name))
}
}
func (v *constLocalVisitor) writeName(name string) {
delete(v.candidates, strings.ToUpper(name))
}
func (v *constLocalVisitor) exprs(es []ast.Expr) {
for _, e := range es {
v.expr(e)
}
}
func (v *constLocalVisitor) stmts(ss []ast.Stmt) {
for _, s := range ss {
v.stmt(s)
}
}
func (v *constLocalVisitor) expr(e ast.Expr) {
if v.aborted || e == nil {
return
}
switch x := e.(type) {
case *ast.LiteralExpr, *ast.IdentExpr, *ast.SelfExpr:
// leaf; reads don't disqualify
case *ast.BinaryExpr:
v.expr(x.Left)
v.expr(x.Right)
case *ast.UnaryExpr:
if x.Op == token.INC || x.Op == token.DEC {
v.writeIdent(x.X)
}
v.expr(x.X)
case *ast.PostfixExpr:
v.writeIdent(x.X)
v.expr(x.X)
case *ast.AssignExpr:
// All assign ops (:= += -= *= /= %= ^=) are writes to Left's
// outer ident. Compound assigns also read, but disqualification
// is based on being written at all.
v.writeIdent(x.Left)
// Still walk Left in case of indexing: arr[i] := v — the ident
// arr is read (and we don't want to accidentally treat it as a
// write since writeIdent only triggers on a bare IdentExpr).
if _, isIdent := x.Left.(*ast.IdentExpr); !isIdent {
v.expr(x.Left)
}
v.expr(x.Right)
case *ast.CallExpr:
v.expr(x.Func)
v.exprs(x.Args)
case *ast.DotExpr:
v.expr(x.X)
case *ast.SendExpr:
v.expr(x.Object)
if x.MacroMethod != nil {
v.expr(x.MacroMethod)
}
v.exprs(x.Args)
case *ast.IndexExpr:
v.expr(x.X)
v.expr(x.Index)
case *ast.AliasExpr:
v.expr(x.Alias)
v.expr(x.Field)
case *ast.MacroExpr:
// Macros can expand to any name including writes. Bail.
v.abort()
case *ast.BlockExpr:
v.expr(x.Body)
case *ast.ArrayLitExpr:
v.exprs(x.Items)
case *ast.HashLitExpr:
v.exprs(x.Keys)
v.exprs(x.Values)
case *ast.IIfExpr:
v.expr(x.Cond)
v.expr(x.True)
v.expr(x.False)
case *ast.RefExpr:
// @ident — passes by reference; callee may mutate.
v.writeIdent(x.X)
v.expr(x.X)
case *ast.SliceExpr:
v.expr(x.X)
v.expr(x.Low)
v.expr(x.High)
case *ast.NilSafeExpr:
v.expr(x.X)
case *ast.InterpolatedString:
v.exprs(x.Parts)
default:
v.abort()
}
}
func (v *constLocalVisitor) stmt(s ast.Stmt) {
if v.aborted || s == nil {
return
}
switch x := s.(type) {
case *ast.ExprStmt:
v.expr(x.X)
case *ast.ReturnStmt:
v.expr(x.Value)
case *ast.QOutStmt:
v.exprs(x.Exprs)
case *ast.IfStmt:
v.expr(x.Cond)
v.stmts(x.Body)
for _, ei := range x.ElseIfs {
v.expr(ei.Cond)
v.stmts(ei.Body)
}
v.stmts(x.ElseBody)
case *ast.DoWhileStmt:
v.expr(x.Cond)
v.stmts(x.Body)
case *ast.ForStmt:
v.writeName(x.Var)
v.expr(x.Start)
v.expr(x.To)
v.expr(x.Step)
v.stmts(x.Body)
case *ast.ForEachStmt:
v.writeName(x.Var)
v.expr(x.Collection)
v.stmts(x.Body)
case *ast.SwitchStmt:
v.expr(x.Expr)
for _, c := range x.Cases {
v.expr(c.Value)
v.stmts(c.Body)
}
v.stmts(x.Otherwise)
case *ast.SeqStmt:
v.stmts(x.Body)
if x.RecoverVar != "" {
v.writeName(x.RecoverVar)
}
v.stmts(x.RecoverBody)
case *ast.MultiAssignStmt:
for _, t := range x.Targets {
v.writeName(t)
}
v.exprs(x.Values)
case *ast.VarDecl:
// Init exprs are reads. The LOCAL name itself was already
// collected as a candidate by collectConstLocals; we don't
// treat its own init as a reassignment.
for _, vi := range x.Vars {
v.expr(vi.Init)
}
case *ast.DeferStmt:
v.expr(x.Call)
case *ast.ExitStmt, *ast.LoopStmt:
// no expression
case *ast.SkipCmd:
v.expr(x.Count)
case *ast.GoCmd:
v.expr(x.RecNo)
case *ast.SeekCmd:
v.expr(x.Key)
case *ast.UseCmd:
v.expr(x.File)
v.expr(x.AliasExpr)
case *ast.SelectCmd:
v.expr(x.Area)
case *ast.ReplaceCmd:
for _, f := range x.Fields {
v.expr(f.Field)
v.expr(f.Value)
}
case *ast.AppendCmd, *ast.DeleteCmd, *ast.ReadCmd:
// no expressions
case *ast.IndexCmd:
v.expr(x.KeyExpr)
v.expr(x.File)
v.expr(x.ForCond)
case *ast.SetCmd:
v.expr(x.Expr)
case *ast.AtSayCmd:
v.expr(x.Row)
v.expr(x.Col)
v.expr(x.SayExpr)
v.expr(x.Picture)
case *ast.AtGetCmd:
// @ GET var writes to Var at READ time.
v.writeIdent(x.Var)
if x.VarName != "" {
v.writeName(x.VarName)
}
v.expr(x.Row)
v.expr(x.Col)
v.expr(x.Picture)
v.expr(x.Valid)
v.expr(x.When)
case *ast.AtSayGetCmd:
v.writeIdent(x.Var)
if x.VarName != "" {
v.writeName(x.VarName)
}
v.expr(x.Row)
v.expr(x.Col)
v.expr(x.SayExpr)
v.expr(x.Picture)
v.expr(x.Valid)
v.expr(x.When)
default:
v.abort()
}
}
// 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 := "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)
// 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
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 {
if _, isConst := g.constLocals[strings.ToUpper(v.Name)]; !isConst {
g.emitExpr(v.Init)
g.writeln(fmt.Sprintf("t.PopLocalFast(%d)", idx))
}
}
}
}
func (g *Generator) emitQOut(s *ast.QOutStmt, locals localMap) {
sym := "QOUT"
if s.IsQQ {
sym = "QQOUT"
}
g.emitPushSymbol(sym)
g.writeln("t.PushNil()")
for _, expr := range s.Exprs {
g.emitExpr(expr)
}
g.writeln(fmt.Sprintf("t.Function(%d)", len(s.Exprs)))
}
func (g *Generator) emitExprStmt(s *ast.ExprStmt, locals localMap) {
// Check if it's an assignment
if assign, ok := s.X.(*ast.AssignExpr); ok {
g.emitAssign(assign, locals)
return
}
// Check if it's a function call (discard result)
if call, ok := s.X.(*ast.CallExpr); ok {
g.emitCallAsStmt(call, locals)
return
}
// Bare identifier as statement (e.g., CLS, CLEAR) — treat as zero-arg function call
if ident, ok := s.X.(*ast.IdentExpr); ok {
if _, found := locals[strings.ToUpper(ident.Name)]; !found {
g.emitPushSymbol(strings.ToUpper(ident.Name))
g.writeln("t.PushNil()")
g.writeln("t.Do(0)")
return
}
}
// Postfix ++/--
if pf, ok := s.X.(*ast.PostfixExpr); ok {
// Local variable: n++
if ident, ok := pf.X.(*ast.IdentExpr); ok {
upper := strings.ToUpper(ident.Name)
if idx, found := locals[upper]; found {
if pf.Op == token.INC {
g.writeln(fmt.Sprintf("t.LocalAddInt(%d, 1)", idx))
} else {
g.writeln(fmt.Sprintf("t.LocalAddInt(%d, -1)", idx))
}
return
}
// STATIC variable: s_nPass++
if goVar, found := g.staticVars[upper]; found {
delta := "1"
if pf.Op == token.DEC {
delta = "-1"
}
g.writeln(fmt.Sprintf("{ _v := %s.AsNumInt() + %s; %s = hbrt.MakeInt(int(_v)) }", goVar, delta, goVar))
return
}
}
// Self field: ::field++
if send, ok := pf.X.(*ast.SendExpr); ok {
if _, isSelf := send.Object.(*ast.SelfExpr); isSelf {
fieldName := strings.ToUpper(send.Method)
g.writeln(fmt.Sprintf("t.PushSelfField(%q)", fieldName))
if pf.Op == token.INC {
g.writeln("t.PushInt(1)")
g.writeln("t.Plus()")
} else {
g.writeln("t.PushInt(1)")
g.writeln("t.Minus()")
}
g.writeln(fmt.Sprintf("t.SetSelfField(%q)", fieldName))
return
}
}
}
// General expression (result on stack, pop it)
g.emitExpr(s.X)
g.writeln("t.Pop()")
}
func (g *Generator) emitAssign(a *ast.AssignExpr, locals localMap) {
// Check for arr[idx] := 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 {
upper := strings.ToUpper(aliasIdent.Name)
// `M->name := v` / `MEMVAR->name := v` are memvar writes,
// not workarea field writes.
if upper == "M" || upper == "MEMVAR" {
g.emitExpr(a.Right)
g.writeln(fmt.Sprintf(`t.PopMemvar(%q)`, fieldIdent.Name))
return
}
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:
// Peephole: `x := x + <expr>` / `x := x - <expr>` →
// LocalAdd. Same result as `x += <expr>` but lets the
// PRG side use the explicit form without penalty.
if be, ok := a.Right.(*ast.BinaryExpr); ok &&
(be.Op == token.PLUS || be.Op == token.MINUS) {
if lid, isIdent := be.Left.(*ast.IdentExpr); isIdent {
if selfIdx, found := locals[strings.ToUpper(lid.Name)]; found && selfIdx == idx {
g.emitExpr(be.Right)
if be.Op == token.MINUS {
g.writeln("t.Negate()")
}
g.writeln(fmt.Sprintf("t.LocalAdd(%d)", idx))
return
}
}
}
g.emitExpr(a.Right)
g.writeln(fmt.Sprintf("t.PopLocalFast(%d)", idx))
case token.PLUSEQ:
g.emitExpr(a.Right)
g.writeln(fmt.Sprintf("t.LocalAdd(%d)", idx))
case token.MINUSEQ:
g.emitExpr(a.Right)
g.writeln("t.Negate()")
g.writeln(fmt.Sprintf("t.LocalAdd(%d)", idx))
default:
// General compound: push local, push right, op, pop local
g.writeln(fmt.Sprintf("t.PushLocalFast(%d)", idx))
g.emitExpr(a.Right)
g.emitBinaryOp(a.Op)
g.writeln(fmt.Sprintf("t.PopLocalFast(%d)", idx))
}
return
}
// Check module-level or function-level STATIC variable
upper := strings.ToUpper(ident.Name)
if goVar, found := g.staticVars[upper]; found {
switch a.Op {
case token.ASSIGN:
g.emitExpr(a.Right)
g.writeln(fmt.Sprintf("%s = t.Pop2()", goVar))
case token.PLUSEQ:
g.writeln(fmt.Sprintf("t.PushValue(%s)", goVar))
g.emitExpr(a.Right)
g.writeln("t.Plus()")
g.writeln(fmt.Sprintf("%s = t.Pop2()", goVar))
case token.MINUSEQ:
g.writeln(fmt.Sprintf("t.PushValue(%s)", goVar))
g.emitExpr(a.Right)
g.writeln("t.Minus()")
g.writeln(fmt.Sprintf("%s = t.Pop2()", goVar))
case token.STAREQ:
g.writeln(fmt.Sprintf("t.PushValue(%s)", goVar))
g.emitExpr(a.Right)
g.writeln("t.Mult()")
g.writeln(fmt.Sprintf("%s = t.Pop2()", goVar))
case token.SLASHEQ:
g.writeln(fmt.Sprintf("t.PushValue(%s)", goVar))
g.emitExpr(a.Right)
g.writeln("t.Divide()")
g.writeln(fmt.Sprintf("%s = t.Pop2()", goVar))
default:
g.writeln(fmt.Sprintf("t.PushValue(%s)", goVar))
g.emitExpr(a.Right)
g.emitBinaryOp(a.Op)
g.writeln(fmt.Sprintf("%s = t.Pop2()", goVar))
}
return
}
}
// Fallback: general assignment via stack
g.emitExpr(a.Right)
g.writeln("// WARN: complex assignment target — simplified")
g.writeln("t.Pop()")
}
func (g *Generator) emitCallAsStmt(call *ast.CallExpr, locals localMap) {
if ident, ok := call.Func.(*ast.IdentExpr); ok {
g.emitPushSymbol(strings.ToUpper(ident.Name))
} else {
g.emitExpr(call.Func)
}
g.writeln("t.PushNil()")
for _, arg := range call.Args {
g.emitExpr(arg)
}
g.writeln(fmt.Sprintf("t.Do(%d)", len(call.Args)))
}
// boolLiteralValue returns (value, true) if e reduces to a .T./.F.
// literal at compile time. Sees through an outer `.NOT.` so expressions
// like `!.F.` also collapse. Used by emitIf to skip dead branches and
// by the AND/OR short-circuit emitter.
func boolLiteralValue(e ast.Expr) (bool, bool) {
if u, ok := e.(*ast.UnaryExpr); ok && u.Op == token.NOT {
if v, ok := boolLiteralValue(u.X); ok {
return !v, true
}
return false, false
}
lit, ok := e.(*ast.LiteralExpr)
if !ok {
return false, false
}
switch lit.Kind {
case token.TRUE:
return true, true
case token.FALSE:
return false, true
}
return false, false
}
func (g *Generator) emitIf(s *ast.IfStmt, locals localMap) {
// Dead-branch elimination for literal conditions. An IF .T. collapses
// to its body; an IF .F. collapses to its first live ELSEIF/ELSE.
// We resolve the main Cond here and recurse on the remainder if it
// turns into a new IF chain.
if v, ok := boolLiteralValue(s.Cond); ok {
if v {
for _, stmt := range s.Body {
g.emitStmt(stmt, locals)
}
return
}
// IF .F. — scan ElseIfs for first non-.F. branch.
for i, ei := range s.ElseIfs {
if v2, ok2 := boolLiteralValue(ei.Cond); ok2 {
if v2 {
for _, stmt := range ei.Body {
g.emitStmt(stmt, locals)
}
return
}
continue // ELSEIF .F. — dead, skip
}
// Non-literal ELSEIF becomes the new IF head.
newIf := &ast.IfStmt{
IfPos: ei.ElseIfPos,
Cond: ei.Cond,
Body: ei.Body,
ElseIfs: s.ElseIfs[i+1:],
ElseBody: s.ElseBody,
}
g.emitIf(newIf, locals)
return
}
// All ElseIfs were .F. — only ELSE body remains.
for _, stmt := range s.ElseBody {
g.emitStmt(stmt, locals)
}
return
}
// Main cond is dynamic. Still filter dead ELSEIFs (.F. removed;
// an ELSEIF .T. truncates the chain and becomes the ELSE).
elseIfs := s.ElseIfs
elseBody := s.ElseBody
if len(elseIfs) > 0 {
filtered := make([]*ast.ElseIfClause, 0, len(elseIfs))
for _, ei := range elseIfs {
if v, ok := boolLiteralValue(ei.Cond); ok {
if v {
// ELSEIF .T. — chain stops here; body becomes ELSE.
elseBody = ei.Body
elseIfs = filtered
goto emit
}
continue // ELSEIF .F. — dead
}
filtered = append(filtered, ei)
}
elseIfs = filtered
}
emit:
g.emitExpr(s.Cond)
g.writeln("if t.PopLogical() {")
g.indent++
for _, stmt := range s.Body {
g.emitStmt(stmt, locals)
}
g.indent--
for _, ei := range elseIfs {
g.writeIndent()
g.write("} else {\n")
g.indent++
g.emitExpr(ei.Cond)
g.writeln("if t.PopLogical() {")
g.indent++
for _, stmt := range ei.Body {
g.emitStmt(stmt, locals)
}
g.indent--
}
if len(elseBody) > 0 {
g.writeln("} else {")
g.indent++
for _, stmt := range elseBody {
g.emitStmt(stmt, locals)
}
g.indent--
}
g.writeln("}")
// Close nested elseif braces
for range elseIfs {
g.writeln("}")
}
}
func (g *Generator) emitDoWhile(s *ast.DoWhileStmt, locals localMap) {
// DO WHILE .F. — body is unreachable; emit nothing.
if v, ok := boolLiteralValue(s.Cond); ok && !v {
return
}
// Detect RDD commands in body for WA hoisting
hasRDD := hasRDDCommands(s.Body)
safeToHoist := hasRDD && !hasWorkareaChange(s.Body)
if safeToHoist && g.hoistedFields == nil {
g.writeln("{")
g.indent++
g.writeln("_dwa := t.WA.(*hbrdd.WorkAreaManager)")
g.writeln("_darea := _dwa.Current()")
g.hoistedDW = true
}
g.writeln("for {")
g.indent++
// DO WHILE .T. — the idiomatic infinite loop. Skip the per-iteration
// PushBool/PopLogical; exit only through EXIT / LOOP / RETURN.
if v, ok := boolLiteralValue(s.Cond); !ok || !v {
g.emitExpr(s.Cond)
g.writeln("if !t.PopLogical() { break }")
}
for _, stmt := range s.Body {
g.emitStmt(stmt, locals)
}
g.indent--
g.writeln("}")
if safeToHoist && g.hoistedDW {
g.hoistedDW = false
g.indent--
g.writeln("}")
}
}
// hasRDDCommands checks if any statement is an RDD operation.
func hasRDDCommands(stmts []ast.Stmt) bool {
for _, s := range stmts {
switch s.(type) {
case *ast.SkipCmd, *ast.GoCmd, *ast.SeekCmd,
*ast.ReplaceCmd, *ast.AppendCmd, *ast.DeleteCmd:
return true
}
}
return false
}
// hasWorkareaChange checks for USE/SELECT that would invalidate cached area.
func hasWorkareaChange(stmts []ast.Stmt) bool {
for _, s := range stmts {
switch v := s.(type) {
case *ast.UseCmd, *ast.SelectCmd:
return true
case *ast.IfStmt:
if hasWorkareaChange(v.Body) || hasWorkareaChange(v.ElseBody) {
return true
}
case *ast.DoWhileStmt:
if hasWorkareaChange(v.Body) {
return true
}
}
}
return false
}
// collectSymbols scans AST for all symbol names referenced by function calls.
// Returns unique names for hoisting FindSymbol to function prologue.
func collectSymbols(stmts []ast.Stmt) []string {
seen := map[string]bool{}
var names []string
var walk func([]ast.Stmt)
var walkExpr func(ast.Expr)
walkExpr = func(e ast.Expr) {
if e == nil {
return
}
switch v := e.(type) {
case *ast.CallExpr:
if ident, ok := v.Func.(*ast.IdentExpr); ok {
name := strings.ToUpper(ident.Name)
if !seen[name] {
seen[name] = true
names = append(names, name)
}
}
for _, a := range v.Args {
walkExpr(a)
}
case *ast.BinaryExpr:
walkExpr(v.Left)
walkExpr(v.Right)
case *ast.UnaryExpr:
walkExpr(v.X)
}
}
walk = func(stmts []ast.Stmt) {
for _, s := range stmts {
switch v := s.(type) {
case *ast.ExprStmt:
walkExpr(v.X)
case *ast.ReturnStmt:
if v.Value != nil {
walkExpr(v.Value)
}
case *ast.IfStmt:
walkExpr(v.Cond)
walk(v.Body)
walk(v.ElseBody)
case *ast.ForStmt:
walk(v.Body)
case *ast.ForEachStmt:
walk(v.Body)
case *ast.DoWhileStmt:
walkExpr(v.Cond)
walk(v.Body)
case *ast.SeqStmt:
walk(v.Body)
walk(v.RecoverBody)
case *ast.SwitchStmt:
for _, c := range v.Cases {
walk(c.Body)
}
}
}
}
walk(stmts)
return names
}
// collectReplaceFields scans statements for REPLACE field names.
// Returns nil if unsafe to hoist (USE/SELECT/CLOSE found).
func collectReplaceFields(stmts []ast.Stmt) []string {
seen := map[string]bool{}
var fields []string
for _, s := range stmts {
switch v := s.(type) {
case *ast.ReplaceCmd:
for _, rf := range v.Fields {
if ident, ok := rf.Field.(*ast.IdentExpr); ok {
name := ident.Name
if !seen[name] {
seen[name] = true
fields = append(fields, name)
}
}
}
case *ast.UseCmd, *ast.SelectCmd:
return nil // workarea may change — unsafe to hoist
case *ast.IfStmt:
// Check nested blocks
if sub := collectReplaceFields(v.Body); sub == nil {
return nil
}
if sub := collectReplaceFields(v.ElseBody); sub == nil {
return nil
}
case *ast.DoWhileStmt:
if sub := collectReplaceFields(v.Body); sub == nil {
return nil
}
}
}
return fields
}
// hasAppendInBody checks if any APPEND command exists in the statements.
func hasAppendInBody(stmts []ast.Stmt) bool {
for _, s := range stmts {
if _, ok := s.(*ast.AppendCmd); ok {
return true
}
}
return false
}
func (g *Generator) emitFor(s *ast.ForStmt, locals localMap) {
idx, found := locals[strings.ToUpper(s.Var)]
if !found {
g.writeln("// ERROR: FOR variable not found in locals")
return
}
// i := start
g.emitExpr(s.Start)
g.writeln(fmt.Sprintf("t.PopLocalFast(%d)", idx))
// Detect step direction for comparison
isNegStep := false
if s.Step != nil {
if lit, ok := s.Step.(*ast.LiteralExpr); ok {
if lit.Kind == token.INT && len(lit.Value) > 0 && lit.Value[0] == '-' {
isNegStep = true
}
}
if un, ok := s.Step.(*ast.UnaryExpr); ok && un.Op == token.MINUS {
isNegStep = true
}
}
// Optimization: hoist WA/FieldIndex lookups outside FOR loop
// if body contains REPLACE and no USE/SELECT (safe to cache).
rddFields := collectReplaceFields(s.Body)
hoistRDD := len(rddFields) > 0 && hasAppendInBody(s.Body)
if hoistRDD {
g.writeln("{")
g.indent++
g.writeln("_rwa := t.WA.(*hbrdd.WorkAreaManager)")
g.writeln("_rarea := _rwa.Current()")
g.writeln("var _rdbf *dbf.DBFArea")
g.writeln("if _rarea != nil { _rdbf, _ = _rarea.(*dbf.DBFArea) }")
// Pre-compute field indexes
for i, fname := range rddFields {
g.writeln(fmt.Sprintf("var _rfi%d int = -1", i))
g.writeln(fmt.Sprintf("if _rdbf != nil { _rfi%d = _rdbf.FieldIndex(%q) }", i, fname))
}
g.hoistedFields = rddFields // store for emitReplaceCmdHoisted
}
g.writeln("for {")
g.indent++
// Comparison: fused opcode when limit is literal int (most common).
// Also see through const-propagated LOCALs: `LOCAL n := 100; FOR i := 1
// TO n` should hit the same fast path as a bare literal.
toLit, _ := s.To.(*ast.LiteralExpr)
if toLit == nil {
if id, ok := s.To.(*ast.IdentExpr); ok {
if l, ok2 := g.constLocals[strings.ToUpper(id.Name)]; ok2 {
toLit = l
}
}
}
if lit := toLit; lit != nil && lit.Kind == token.INT {
if isNegStep {
g.writeln(fmt.Sprintf("if !t.LocalGreaterEqualInt(%d, %s) { break }", idx, lit.Value))
} else {
g.writeln(fmt.Sprintf("if !t.LocalLessEqualInt(%d, %s) { break }", idx, lit.Value))
}
} else {
// General case: stack-based comparison
g.writeln(fmt.Sprintf("t.PushLocalFast(%d)", idx))
g.emitExpr(s.To)
if isNegStep {
g.writeln("t.GreaterEqual()")
} else {
g.writeln("t.LessEqual()")
}
g.writeln("if !t.PopLogical() { break }")
}
// Track FOR loop depth so LOOP can use goto instead of continue.
// Only emit label if LOOP is present in the body (Go rejects unused labels).
hasLoop := bodyHasLoop(s.Body)
forLabel := ""
prevForLabel := g.curForLabel
if hasLoop {
forLabel = fmt.Sprintf("_for_next_%d", g.forLabelSeq)
g.forLabelSeq++
g.curForLabel = forLabel
} else {
g.curForLabel = ""
}
// body
for _, stmt := range s.Body {
g.emitStmt(stmt, locals)
}
// Label for LOOP to jump to (skipping continue which would miss increment)
if hasLoop {
g.writeln(forLabel + ":")
}
// i += step (default 1)
if s.Step != nil {
g.emitExpr(s.Step)
g.writeln(fmt.Sprintf("t.LocalAdd(%d)", idx))
} else {
g.writeln(fmt.Sprintf("t.LocalAddInt(%d, 1)", idx))
}
g.curForLabel = prevForLabel
g.indent--
g.writeln("}")
// Close hoisting block
if hoistRDD {
g.hoistedFields = nil
g.indent--
g.writeln("}")
}
}
// bodyHasLoop checks if any statement in the body is a LOOP.
// Only checks the immediate level — LOOP inside nested FOR/DO WHILE is irrelevant.
func bodyHasLoop(stmts []ast.Stmt) bool {
for _, s := range stmts {
if hasLoopStmt(s) {
return true
}
}
return false
}
func hasLoopStmt(s ast.Stmt) bool {
switch s := s.(type) {
case *ast.LoopStmt:
return true
case *ast.IfStmt:
for _, st := range s.Body {
if hasLoopStmt(st) {
return true
}
}
for _, ei := range s.ElseIfs {
for _, st := range ei.Body {
if hasLoopStmt(st) {
return true
}
}
}
for _, st := range s.ElseBody {
if hasLoopStmt(st) {
return true
}
}
case *ast.SeqStmt:
for _, st := range s.Body {
if hasLoopStmt(st) {
return true
}
}
for _, st := range s.RecoverBody {
if hasLoopStmt(st) {
return true
}
}
case *ast.SwitchStmt:
for _, c := range s.Cases {
for _, st := range c.Body {
if hasLoopStmt(st) {
return true
}
}
}
for _, st := range s.Otherwise {
if hasLoopStmt(st) {
return true
}
}
// Do NOT recurse into ForStmt/DoWhileStmt — nested LOOP is for the inner loop
}
return false
}
func (g *Generator) emitSwitch(s *ast.SwitchStmt, locals localMap) {
// Wrap the whole thing in a one-iteration `for` so:
// 1. `_sw` stays scoped to the switch.
// 2. `EXIT` inside a CASE emits `break` and targets this loop,
// matching Harbour SWITCH semantics (EXIT terminates SWITCH).
// 3. Empty SWITCH (`SWITCH x ENDSWITCH`, common in conditional-
// compile test files) stays valid Go.
g.writeln("for {")
g.indent++
g.emitExpr(s.Expr)
g.writeln("_sw := t.Pop2()")
g.writeln("_ = _sw") // silence unused-var warning when no cases reference it
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 {
if first {
// No CASE arms — emit the OTHERWISE body as-is, no if/else.
for _, stmt := range s.Otherwise {
g.emitStmt(stmt, locals)
}
} else {
g.writeln("} else {")
g.indent++
for _, stmt := range s.Otherwise {
g.emitStmt(stmt, locals)
}
g.indent--
g.writeln("}")
}
} else if !first {
// Had CASE arms, no OTHERWISE — close the if/else-if chain.
g.writeln("}")
}
// Always break out of our one-iteration `for` wrapper, regardless
// of which (or no) case ran.
g.writeln("break")
g.indent--
g.writeln("}")
}
func (g *Generator) emitBeginSequence(s *ast.SeqStmt, locals localMap) {
// BEGIN SEQUENCE → Go's panic/recover.
// Catches both *HbError (runtime errors) and BreakValue (Break() calls).
// BreakValue is defined in hbrtl, but we detect it via duck typing
// to avoid import cycles.
g.writeln("{ // BEGIN SEQUENCE")
g.indent++
g.writeln("_seqErr := func() (_recoverVal interface{}) {")
g.indent++
g.writeln("defer func() {")
g.indent++
g.writeln("if r := recover(); r != nil {")
g.indent++
g.writeln("_recoverVal = r")
g.indent--
g.writeln("}")
g.indent--
g.writeln("}()")
// Body
for _, stmt := range s.Body {
g.emitStmt(stmt, locals)
}
g.writeln("return nil")
g.indent--
g.writeln("}()")
// RECOVER
if len(s.RecoverBody) > 0 {
g.writeln("if _seqErr != nil {")
g.indent++
if s.RecoverVar != "" {
if idx, found := locals[strings.ToUpper(s.RecoverVar)]; found {
// Extract the value from the recovered panic:
// *HbError → error description string
// BreakValue (has .Value field) → the Break() argument
// other → string representation
g.writeln(fmt.Sprintf(`{ // RECOVER USING %s`, s.RecoverVar))
g.indent++
g.writeln(`switch _sv := _seqErr.(type) {`)
g.writeln(`case *hbrt.HbError:`)
g.writeln(fmt.Sprintf(` t.SetLocalFast(%d, hbrt.MakeString(_sv.Error()))`, idx))
g.writeln(`default:`)
// For BreakValue, use reflection-free approach: check if
// the type has a Value field via a local interface.
g.writeln(` type hasValue interface{ GetValue() hbrt.Value }`)
g.writeln(` if bv, ok := _sv.(hasValue); ok {`)
g.writeln(fmt.Sprintf(` t.SetLocalFast(%d, bv.GetValue())`, idx))
g.writeln(` } else {`)
g.writeln(fmt.Sprintf(` t.SetLocalFast(%d, hbrt.MakeString("error"))`, idx))
g.writeln(` }`)
g.writeln(`}`)
g.indent--
g.writeln(`}`)
}
}
for _, stmt := range s.RecoverBody {
g.emitStmt(stmt, locals)
}
g.indent--
g.writeln("}")
} else {
g.writeln("_ = _seqErr")
}
g.indent--
g.writeln("} // END SEQUENCE")
}
func (g *Generator) emitForEach(s *ast.ForEachStmt, locals localMap) {
varIdx, found := locals[strings.ToUpper(s.Var)]
if !found {
g.writeln("// ERROR: FOR EACH variable not in locals")
return
}
// Evaluate collection
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:
// 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:
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
}
// 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) 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 {
upper := strings.ToUpper(ident.Name)
// `M->name` / `MEMVAR->name` access the memvar namespace, not
// a database workarea. Harbour reserves both aliases for this.
if upper == "M" || upper == "MEMVAR" {
g.writeln(fmt.Sprintf(`t.PushMemvar(%q)`, fieldIdent.Name))
return
}
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) {
// ::super:Method(args) — dispatch to parent class. The parse tree
// is nested: outer SendExpr.Object is itself a SendExpr whose
// Object is ::SELF and Method is "super". Detect that shape and
// route through SendSuper, which keeps Self bound to the child
// instance but looks the method up on Parent.
if sup, ok := e.Object.(*ast.SendExpr); ok {
if _, isSelf := sup.Object.(*ast.SelfExpr); isSelf &&
strings.EqualFold(sup.Method, "super") {
for _, arg := range e.Args {
g.emitExpr(arg)
}
// Emit defining-class name so runtime walks the right Parent
// chain — Self's class alone would infinite-loop on 3+ level
// hierarchies (Grand→Child→Base). See SendSuper comment.
g.writeln(fmt.Sprintf("t.SendSuper(%q, %q, %d)",
g.curMethodClass, e.Method, len(e.Args)))
return
}
}
// 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))
}
}