Release-blocking compatibility issues discovered during the 258-test
pre-release validation suite (100 syntax + 44 RDD + 114 RTL).
1. PCount() always returned 0 in PRG code
Root cause: ParamCount() returned t.pendingParams, which is
overwritten by every nested Function() call. By the time the
PCount() RTL's Frame() executes, pendingParams is already 0.
Fix: Frame() now stores pendingParams in frame.paramCount.
PCount() RTL uses CallerParamCount() which reads callSP-2
(the PRG caller's frame), while RTL functions still use
ParamCount() (reads pendingParams before their own Frame).
Verified: PCount(1,2,3)=3, PCount(1)=1, PCount()=0
2. Break("string") panicked instead of being caught by RECOVER USING
Root cause: Generated SEQUENCE code only caught *HbError panics.
Break() panics with BreakValue (a different type), which fell
through to EndProc's "runtime error" message and re-panic.
Fix (two parts):
a) gengo emitBeginSequence: recover closure now catches any
panic (interface{}), then dispatches via type switch:
- *HbError → extract .Error() string
- hasValue interface (BreakValue) → extract .GetValue()
- other → static "error" string
b) hbrtl/error.go: BreakValue gets GetValue() method for
duck-type detection without import cycles
c) hbrt/thread.go EndProc: BreakValue type name check added
so it re-panics silently (no stderr noise)
3. SET INDEX TO a, b, c only opened the last file
Root cause: Parser's parseSet() called parseExpr() once for
INDEX setting, stopping at the first comma. Remaining file
names were consumed by the "eat rest of line" loop.
Fix: Parser now collects comma-separated identifiers into a
single string literal "a,b,c". gengo splits on comma and
calls OrderListAdd() for each file.
Verified: SET INDEX TO si_name, si_city → OrdCount=2
All tests pass:
go test ./... 14 packages OK
FiveSql2 43/43 100%
compat_harbour 51/51
Syntax test 100/100
RDD test 44/44
RTL test 114/114
Windows cross-compile OK
Linux cross-compile OK
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2449 lines
67 KiB
Go
2449 lines
67 KiB
Go
// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
|
|
// All rights reserved.
|
|
|
|
// Go code generator for the Five language.
|
|
// Converts Five AST into Go source code that calls hbrt runtime functions.
|
|
//
|
|
// Design references:
|
|
// - Harbour: gencc.c — pcode → hb_xvm*() C function calls
|
|
// - tsgo: internal/printer/printer.go — AST → text via Writer interface
|
|
// - Pattern: AST node → Thread method call (t.PushInt, t.Plus, etc.)
|
|
//
|
|
// Generated code structure:
|
|
// package main
|
|
// import ("five/hbrt"; "five/hbrtl")
|
|
// var symbols = hbrt.NewModule(...)
|
|
// func HB_MAIN(t *hbrt.Thread) { ... }
|
|
// func main() { vm := hbrt.NewVM(); ... vm.Run("MAIN") }
|
|
package gengo
|
|
|
|
import (
|
|
"five/compiler/ast"
|
|
"five/compiler/token"
|
|
"fmt"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// Generator produces Go source code from a Five AST.
|
|
type Generator struct {
|
|
buf strings.Builder
|
|
indent int
|
|
file *ast.File
|
|
symbols []symbolEntry
|
|
imports map[string]bool
|
|
importAlias map[string]string // path → alias ("_", "name", or "")
|
|
curLocals localMap // current function's local variable map
|
|
goFastFuncs []goFastEntry // Go functions to register as FastFunc
|
|
staticVars map[string]string // top-level STATIC: upper name → Go var name
|
|
IsLibrary bool // if true, no main() generated, symbols use unique name
|
|
hoistedFields []string // field names hoisted outside FOR loop (nil = not hoisting)
|
|
hoistedDW bool // DO WHILE has hoisted _dwa/_darea
|
|
symCache map[string]string // symbol name → cached variable name (nil = not caching)
|
|
Debug bool // if true, emit t.DebugLine() calls
|
|
forLabelSeq int // monotonic counter for FOR..NEXT LOOP labels
|
|
curForLabel string // current FOR loop's LOOP goto label ("" if not in FOR)
|
|
blockSeq int // monotonic counter for unique closure capture names
|
|
}
|
|
|
|
type symbolEntry struct {
|
|
name string
|
|
scope string // "hbrt.FsPublic|hbrt.FsLocal" etc.
|
|
fn string // Go function name: "HB_MAIN"
|
|
}
|
|
|
|
// Generate converts an AST File into Go source code.
|
|
func Generate(file *ast.File) string {
|
|
return doGenerate(file, false, false)
|
|
}
|
|
|
|
// GenerateWithDebug is like Generate but includes DebugLine calls.
|
|
func GenerateWithDebug(file *ast.File) string {
|
|
return doGenerate(file, true, false)
|
|
}
|
|
|
|
// GenerateLibrary generates Go code without main() — for multi-PRG builds.
|
|
func GenerateLibrary(file *ast.File) string {
|
|
return doGenerate(file, false, true)
|
|
}
|
|
|
|
// generate is the unified internal implementation.
|
|
// doGenerate is the unified internal implementation for all Generate* variants.
|
|
func doGenerate(file *ast.File, debug, library bool) string {
|
|
g := &Generator{
|
|
file: file,
|
|
imports: map[string]bool{"five/hbrt": true, "five/hbrtl": true},
|
|
Debug: debug,
|
|
IsLibrary: library,
|
|
}
|
|
|
|
// Collect symbols from declarations
|
|
for _, d := range file.Decls {
|
|
switch decl := d.(type) {
|
|
case *ast.FuncDecl:
|
|
scope := "hbrt.FsPublic|hbrt.FsLocal"
|
|
if !library && (decl.Name == "Main" || decl.Name == "MAIN") {
|
|
scope += "|hbrt.FsFirst"
|
|
}
|
|
g.symbols = append(g.symbols, symbolEntry{
|
|
name: strings.ToUpper(decl.Name),
|
|
scope: scope,
|
|
fn: "HB_" + strings.ToUpper(decl.Name),
|
|
})
|
|
case *ast.ClassDecl:
|
|
className := strings.ToUpper(decl.Name)
|
|
g.symbols = append(g.symbols, symbolEntry{
|
|
name: className,
|
|
scope: "hbrt.FsPublic|hbrt.FsLocal",
|
|
fn: "HB_" + className + "_CTOR",
|
|
})
|
|
}
|
|
}
|
|
|
|
if hasXBaseCommands(file) {
|
|
g.imports["five/hbrdd"] = true
|
|
g.imports["five/hbrdd/dbf"] = true
|
|
}
|
|
|
|
g.importAlias = make(map[string]string)
|
|
for _, imp := range file.Imports {
|
|
g.imports[imp.Path] = true
|
|
if imp.Alias != "" {
|
|
g.importAlias[imp.Path] = imp.Alias
|
|
}
|
|
}
|
|
|
|
g.emitHeader()
|
|
g.emitSymbols()
|
|
for _, d := range file.Decls {
|
|
g.emitDecl(d)
|
|
}
|
|
g.emitFastFuncRegistrations()
|
|
if library {
|
|
g.emitInitModule()
|
|
} else {
|
|
g.emitMain()
|
|
}
|
|
|
|
// Patch deferred imports: inline RTL may add "fmt"/"strings" after header was emitted.
|
|
result := g.buf.String()
|
|
var deferred string
|
|
if g.imports["fmt"] && !strings.Contains(result, "\"fmt\"") {
|
|
deferred += "\t\"fmt\"\n"
|
|
}
|
|
if g.imports["strings"] && !strings.Contains(result, "\"strings\"") {
|
|
deferred += "\t\"strings\"\n"
|
|
}
|
|
result = strings.Replace(result, "\t/*DEFERRED_IMPORTS*/\n", deferred, 1)
|
|
// Patch guards
|
|
if g.imports["strings"] {
|
|
result = strings.Replace(result, "/*GUARD_STRINGS*/", "var _ = strings.TrimLeft", 1)
|
|
} else {
|
|
result = strings.Replace(result, "/*GUARD_STRINGS*/\n", "", 1)
|
|
}
|
|
if g.imports["fmt"] {
|
|
result = strings.Replace(result, "/*GUARD_FMT*/", "var _ = fmt.Sprintf", 1)
|
|
} else {
|
|
result = strings.Replace(result, "/*GUARD_FMT*/\n", "", 1)
|
|
}
|
|
return result
|
|
}
|
|
|
|
// --- Emit infrastructure ---
|
|
|
|
func (g *Generator) write(s string) {
|
|
g.buf.WriteString(s)
|
|
}
|
|
|
|
func (g *Generator) writef(format string, args ...interface{}) {
|
|
fmt.Fprintf(&g.buf, format, args...)
|
|
}
|
|
|
|
func (g *Generator) writeln(s string) {
|
|
g.writeIndent()
|
|
g.buf.WriteString(s)
|
|
g.buf.WriteByte('\n')
|
|
}
|
|
|
|
func (g *Generator) writeIndent() {
|
|
for i := 0; i < g.indent; i++ {
|
|
g.buf.WriteByte('\t')
|
|
}
|
|
}
|
|
|
|
// --- File structure ---
|
|
|
|
func (g *Generator) emitHeader() {
|
|
g.writeln("// Code generated by Five compiler. DO NOT EDIT.")
|
|
g.writeln(fmt.Sprintf("// Source: %s", g.file.Name))
|
|
g.writeln("")
|
|
g.writeln("package main")
|
|
g.writeln("")
|
|
|
|
// Imports (deferred placeholder for imports discovered during body emission)
|
|
g.writeln("import (")
|
|
g.indent++
|
|
for imp := range g.imports {
|
|
if alias, ok := g.importAlias[imp]; ok {
|
|
g.writeln(fmt.Sprintf("%s %q", alias, imp))
|
|
} else {
|
|
g.writeln(fmt.Sprintf("%q", imp))
|
|
}
|
|
}
|
|
g.writeln("/*DEFERRED_IMPORTS*/")
|
|
g.indent--
|
|
g.writeln(")")
|
|
g.writeln("")
|
|
|
|
// Ensure imports are used
|
|
g.writeln("var _ = hbrtl.RegisterRTL")
|
|
if g.imports["five/hbrdd"] {
|
|
g.writeln("var _ = hbrdd.NewWorkAreaManager")
|
|
g.writeln("var _ dbf.DBFDriver")
|
|
}
|
|
// Always emit — deferred inline RTL may add these imports after header.
|
|
// If not actually imported, the DEFERRED_IMPORTS patch won't add them and
|
|
// Go compiler will error. But we also strip unused guards in the patch step.
|
|
g.writeln("/*GUARD_STRINGS*/")
|
|
g.writeln("/*GUARD_FMT*/")
|
|
g.writeln("")
|
|
}
|
|
|
|
func (g *Generator) emitSymbols() {
|
|
varName := "symbols"
|
|
if g.IsLibrary {
|
|
// Unique variable name for library mode
|
|
safeName := strings.TrimSuffix(filepath.Base(g.file.Name), ".prg")
|
|
safeName = strings.Map(func(r rune) rune {
|
|
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') {
|
|
return r
|
|
}
|
|
return '_'
|
|
}, safeName)
|
|
varName = "symbols_" + safeName
|
|
}
|
|
g.writeln(fmt.Sprintf("var %s = hbrt.NewModule(%q,", varName, strings.TrimSuffix(g.file.Name, ".prg")))
|
|
g.indent++
|
|
for _, sym := range g.symbols {
|
|
g.writeln(fmt.Sprintf("hbrt.Sym(%q, %s, %s),", sym.name, sym.scope, sym.fn))
|
|
}
|
|
g.indent--
|
|
g.writeln(")")
|
|
g.writeln("")
|
|
}
|
|
|
|
// emitFastFuncRegistrations emits var declarations for Go FastFunc registrations.
|
|
// These are pre-registered at package init time for 3-11x faster calls.
|
|
func (g *Generator) emitFastFuncRegistrations() {
|
|
if len(g.goFastFuncs) == 0 {
|
|
return
|
|
}
|
|
// Deduplicate
|
|
seen := map[string]bool{}
|
|
g.writeln("// Go FastFunc registrations (type-specialized, bypass reflect)")
|
|
g.writeln("var (")
|
|
g.indent++
|
|
for _, ff := range g.goFastFuncs {
|
|
if seen[ff.regName] {
|
|
continue
|
|
}
|
|
seen[ff.regName] = true
|
|
g.writeln(fmt.Sprintf("_ff_%s = hbrt.RegisterFastFunc(%q, %s)", ff.regName, ff.qualName, ff.qualName))
|
|
}
|
|
g.indent--
|
|
g.writeln(")")
|
|
g.writeln("")
|
|
}
|
|
|
|
func (g *Generator) emitMain() {
|
|
// init() runs before main() — set raw mode before ANY Go runtime I/O
|
|
g.writeln("func init() {")
|
|
g.indent++
|
|
g.writeln("hbrtl.InitRawTerminal()")
|
|
g.indent--
|
|
g.writeln("}")
|
|
g.writeln("")
|
|
g.writeln("func main() {")
|
|
g.indent++
|
|
g.writeln("vm := hbrt.NewVM()")
|
|
g.writeln("hbrtl.RegisterRTL(vm)")
|
|
g.writeln("vm.RegisterModule(symbols)")
|
|
// Find main function
|
|
mainName := "MAIN"
|
|
for _, sym := range g.symbols {
|
|
if strings.Contains(sym.scope, "FsFirst") {
|
|
mainName = sym.name
|
|
break
|
|
}
|
|
}
|
|
g.writeln(fmt.Sprintf("vm.Run(%q)", mainName))
|
|
g.indent--
|
|
g.writeln("}")
|
|
}
|
|
|
|
func (g *Generator) emitInitModule() {
|
|
safeName := strings.TrimSuffix(filepath.Base(g.file.Name), ".prg")
|
|
safeName = strings.Map(func(r rune) rune {
|
|
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') {
|
|
return r
|
|
}
|
|
return '_'
|
|
}, safeName)
|
|
varName := "symbols_" + safeName
|
|
|
|
// Register this module's symbols into a global registry
|
|
// that main()'s vm.RegisterModule will pick up
|
|
g.writeln(fmt.Sprintf("func init() {"))
|
|
g.indent++
|
|
g.writeln(fmt.Sprintf("hbrt.RegisterLibModule(%s)", varName))
|
|
g.indent--
|
|
g.writeln("}")
|
|
}
|
|
|
|
// --- Declaration emission ---
|
|
|
|
func (g *Generator) emitDecl(d ast.Decl) {
|
|
switch decl := d.(type) {
|
|
case *ast.FuncDecl:
|
|
g.emitFuncDecl(decl)
|
|
case *ast.ClassDecl:
|
|
g.emitClassDecl(decl)
|
|
case *ast.MethodDecl:
|
|
g.emitMethodDeclStandalone(decl)
|
|
case *ast.VarDecl:
|
|
// Top-level STATIC → package-level var
|
|
if decl.Scope == ast.ScopeStatic {
|
|
g.emitTopLevelStatic(decl)
|
|
}
|
|
case *ast.GoDumpDecl:
|
|
// Inline Go code from #pragma BEGINDUMP ... ENDDUMP
|
|
if decl.Code != "" {
|
|
g.writeln("\n// --- Inline Go code (#pragma BEGINDUMP) ---")
|
|
g.write(decl.Code)
|
|
g.writeln("\n// --- End inline Go code ---\n")
|
|
}
|
|
}
|
|
}
|
|
|
|
// emitTopLevelStatic emits module-level STATIC variables as package-level Go vars.
|
|
func (g *Generator) emitTopLevelStatic(vd *ast.VarDecl) {
|
|
for _, v := range vd.Vars {
|
|
varName := "static_" + strings.ToUpper(v.Name)
|
|
initVal := "hbrt.MakeNil()"
|
|
if v.Init != nil {
|
|
initVal = g.exprToGoLiteral(v.Init)
|
|
}
|
|
g.writeln(fmt.Sprintf("var %s = %s", varName, initVal))
|
|
// Register in staticMap for lookup
|
|
if g.staticVars == nil {
|
|
g.staticVars = make(map[string]string)
|
|
}
|
|
g.staticVars[strings.ToUpper(v.Name)] = varName
|
|
}
|
|
g.writeln("")
|
|
}
|
|
|
|
func (g *Generator) emitFuncDecl(fn *ast.FuncDecl) {
|
|
goName := "HB_" + strings.ToUpper(fn.Name)
|
|
g.writeln(fmt.Sprintf("func %s(t *hbrt.Thread) {", goName))
|
|
g.indent++
|
|
|
|
// Count params and locals (including mid-function LOCALs in Body)
|
|
nParams := len(fn.Params)
|
|
nLocals := 0
|
|
for _, d := range fn.Decls {
|
|
if vd, ok := d.(*ast.VarDecl); ok && vd.Scope == ast.ScopeLocal {
|
|
nLocals += len(vd.Vars)
|
|
}
|
|
}
|
|
// Count mid-function LOCAL declarations in Body
|
|
for _, s := range fn.Body {
|
|
if vd, ok := s.(*ast.VarDecl); ok && vd.Scope == ast.ScopeLocal {
|
|
nLocals += len(vd.Vars)
|
|
}
|
|
}
|
|
g.writeln(fmt.Sprintf("t.Frame(%d, %d)", nParams, nLocals))
|
|
g.writeln("defer t.EndProc()")
|
|
g.writeln("")
|
|
|
|
// Build local map FIRST (needed for init expressions that reference params)
|
|
g.curLocals = g.buildLocalMap(fn)
|
|
|
|
// Emit LOCAL initializers
|
|
localIdx := nParams + 1 // 1-based, params come first
|
|
for _, d := range fn.Decls {
|
|
vd, ok := d.(*ast.VarDecl)
|
|
if !ok || vd.Scope != ast.ScopeLocal {
|
|
continue
|
|
}
|
|
for _, v := range vd.Vars {
|
|
if v.Init != nil {
|
|
g.emitExpr(v.Init)
|
|
g.writeln(fmt.Sprintf("t.PopLocalFast(%d)", localIdx))
|
|
}
|
|
localIdx++
|
|
}
|
|
}
|
|
|
|
// Emit body statements
|
|
for _, stmt := range fn.Body {
|
|
g.emitStmt(stmt, g.curLocals)
|
|
}
|
|
|
|
g.indent--
|
|
g.writeln("}")
|
|
g.writeln("")
|
|
}
|
|
|
|
type localMap map[string]int
|
|
|
|
func (g *Generator) buildLocalMap(fn *ast.FuncDecl) localMap {
|
|
m := make(localMap)
|
|
idx := 1
|
|
for _, p := range fn.Params {
|
|
m[strings.ToUpper(p.Name)] = idx
|
|
idx++
|
|
}
|
|
for _, d := range fn.Decls {
|
|
if vd, ok := d.(*ast.VarDecl); ok && vd.Scope == ast.ScopeLocal {
|
|
for _, v := range vd.Vars {
|
|
m[strings.ToUpper(v.Name)] = idx
|
|
idx++
|
|
}
|
|
}
|
|
}
|
|
return m
|
|
}
|
|
|
|
// scanBodyLocals recursively scans statements for LOCAL declarations,
|
|
// adding them to the local map with pre-assigned indices.
|
|
// This ensures mid-function LOCALs are known at compile time.
|
|
func scanBodyLocals(stmts []ast.Stmt, m localMap, idx *int) {
|
|
for _, s := range stmts {
|
|
switch st := s.(type) {
|
|
case *ast.VarDecl:
|
|
if st.Scope == ast.ScopeLocal {
|
|
for _, v := range st.Vars {
|
|
name := strings.ToUpper(v.Name)
|
|
if _, exists := m[name]; !exists {
|
|
m[name] = *idx
|
|
(*idx)++
|
|
}
|
|
}
|
|
}
|
|
case *ast.IfStmt:
|
|
scanBodyLocals(st.Body, m, idx)
|
|
for _, ei := range st.ElseIfs {
|
|
scanBodyLocals(ei.Body, m, idx)
|
|
}
|
|
scanBodyLocals(st.ElseBody, m, idx)
|
|
case *ast.DoWhileStmt:
|
|
scanBodyLocals(st.Body, m, idx)
|
|
case *ast.ForStmt:
|
|
scanBodyLocals(st.Body, m, idx)
|
|
case *ast.ForEachStmt:
|
|
scanBodyLocals(st.Body, m, idx)
|
|
case *ast.SwitchStmt:
|
|
for _, c := range st.Cases {
|
|
scanBodyLocals(c.Body, m, idx)
|
|
}
|
|
scanBodyLocals(st.Otherwise, m, idx)
|
|
case *ast.SeqStmt:
|
|
scanBodyLocals(st.Body, m, idx)
|
|
scanBodyLocals(st.RecoverBody, m, idx)
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Statement emission ---
|
|
|
|
func (g *Generator) emitStmt(stmt ast.Stmt, locals localMap) {
|
|
// Emit debug line hook
|
|
if g.Debug && stmt.Pos().Line > 0 {
|
|
g.writeln(fmt.Sprintf("t.DebugLine(%q, %d)", g.file.Name, stmt.Pos().Line))
|
|
}
|
|
|
|
switch s := stmt.(type) {
|
|
case *ast.ReturnStmt:
|
|
if len(s.Values) > 1 {
|
|
// Multi-return: RETURN a, b, c → push array of values
|
|
for _, v := range s.Values {
|
|
g.emitExpr(v)
|
|
}
|
|
g.writeln(fmt.Sprintf("t.ArrayGen(%d)", len(s.Values)))
|
|
g.writeln("t.RetValue()")
|
|
} else if s.Value != nil {
|
|
g.emitExpr(s.Value)
|
|
g.writeln("t.RetValue()")
|
|
} else {
|
|
g.writeln("t.RetNil()")
|
|
}
|
|
g.writeln("return") // Go return to exit function immediately
|
|
|
|
case *ast.QOutStmt:
|
|
g.emitQOut(s, locals)
|
|
|
|
case *ast.ExprStmt:
|
|
g.emitExprStmt(s, locals)
|
|
|
|
case *ast.IfStmt:
|
|
g.emitIf(s, locals)
|
|
|
|
case *ast.SwitchStmt:
|
|
g.emitSwitch(s, locals)
|
|
|
|
case *ast.DoWhileStmt:
|
|
g.emitDoWhile(s, locals)
|
|
|
|
case *ast.ForStmt:
|
|
g.emitFor(s, locals)
|
|
|
|
case *ast.ForEachStmt:
|
|
g.emitForEach(s, locals)
|
|
|
|
case *ast.ExitStmt:
|
|
g.writeln("break")
|
|
|
|
case *ast.LoopStmt:
|
|
if g.curForLabel != "" {
|
|
// Inside FOR..NEXT: goto label before increment (continue would skip it)
|
|
g.writeln("goto " + g.curForLabel)
|
|
} else {
|
|
g.writeln("continue")
|
|
}
|
|
|
|
case *ast.MultiAssignStmt:
|
|
g.emitMultiAssign(s, locals)
|
|
|
|
case *ast.DeferStmt:
|
|
g.emitDefer(s, locals)
|
|
|
|
case *ast.VarDecl:
|
|
// LOCAL in mid-function or PRIVATE/PUBLIC
|
|
g.emitMidVarDecl(s, locals)
|
|
|
|
// xBase commands — generate calls to hbrdd WorkAreaManager
|
|
case *ast.UseCmd:
|
|
g.emitUseCmd(s, locals)
|
|
case *ast.GoCmd:
|
|
g.emitGoCmd(s)
|
|
case *ast.SkipCmd:
|
|
g.emitSkipCmd(s, locals)
|
|
case *ast.SeekCmd:
|
|
g.emitSeekCmd(s, locals)
|
|
case *ast.ReplaceCmd:
|
|
g.emitReplaceCmd(s, locals)
|
|
case *ast.AppendCmd:
|
|
if g.hoistedFields != nil {
|
|
// Use hoisted area variable
|
|
g.writeln("if _rarea != nil { _rarea.Append() }")
|
|
} else {
|
|
g.writeln("{ _wa := t.WA.(*hbrdd.WorkAreaManager)")
|
|
g.writeln("if _area := _wa.Current(); _area != nil { _area.Append() } }")
|
|
}
|
|
case *ast.DeleteCmd:
|
|
if g.hoistedDW || g.hoistedFields != nil {
|
|
g.writeln(fmt.Sprintf("if %s != nil { %s.Delete() }", g.hoistedAreaVar(), g.hoistedAreaVar()))
|
|
} else {
|
|
g.writeln("{ _wa := t.WA.(*hbrdd.WorkAreaManager)")
|
|
g.writeln("if _area := _wa.Current(); _area != nil { _area.Delete() } }")
|
|
}
|
|
case *ast.SelectCmd:
|
|
g.emitExpr(s.Area)
|
|
g.writeln("{ _wa := t.WA.(*hbrdd.WorkAreaManager); _v := t.Pop2()")
|
|
g.writeln("if _v.IsNumeric() { _wa.Select(int(_v.AsNumInt())) } else { _wa.Select(_v.AsString()) } }")
|
|
case *ast.IndexCmd:
|
|
g.writeln("{")
|
|
g.indent++
|
|
g.writeln("wa := t.WA.(*hbrdd.WorkAreaManager)")
|
|
g.writeln("if area := wa.Current(); area != nil {")
|
|
g.indent++
|
|
g.writeln("if idx, ok := area.(hbrdd.Indexer); ok {")
|
|
g.indent++
|
|
// Key expression: stringify ident (field name) or use string literal
|
|
keyStr := exprToString(s.KeyExpr)
|
|
g.writeln(fmt.Sprintf("_keyExpr := %q", keyStr))
|
|
fileStr := exprToString(s.File)
|
|
g.writeln(fmt.Sprintf("_file := %q", fileStr))
|
|
forExpr := `""`
|
|
if s.ForCond != nil {
|
|
forExpr = fmt.Sprintf("%q", exprToString(s.ForCond))
|
|
}
|
|
// Set VM callback for UDF evaluation during index build
|
|
g.writeln("dbf.KeyEvalFunc = func(expr string) hbrt.Value { return t.MacroEval(expr) }")
|
|
g.writeln(fmt.Sprintf("idx.OrderCreate(hbrdd.OrderCreateParams{KeyExpr: _keyExpr, FilePath: _file, ForExpr: %s, Unique: %v, Descending: %v})",
|
|
forExpr, s.Unique, s.Descending))
|
|
g.writeln("dbf.KeyEvalFunc = nil")
|
|
g.indent--
|
|
g.writeln("}")
|
|
g.indent--
|
|
g.writeln("}")
|
|
g.indent--
|
|
g.writeln("}")
|
|
case *ast.SetCmd:
|
|
upper := strings.ToUpper(s.Setting)
|
|
|
|
// Boolean SET toggles — call RTL Set function, no workarea needed
|
|
setFuncMap := map[string]string{
|
|
"DELETED": "SETDELETED",
|
|
"EXACT": "SETEXACT",
|
|
"SOFTSEEK": "SETSOFTSEEK",
|
|
"EXCLUSIVE": "SETEXCLUSIVE",
|
|
"FIXED": "SETFIXED",
|
|
"CANCEL": "SETCANCEL",
|
|
"BELL": "SETBELL",
|
|
"CONFIRM": "SETCONFIRM",
|
|
"INSERT": "SETINSERT",
|
|
"ESCAPE": "SETESCAPE",
|
|
"WRAP": "SETWRAP",
|
|
}
|
|
if funcName, ok := setFuncMap[upper]; ok {
|
|
onOff := strings.ToUpper(s.Extra)
|
|
if onOff == "ON" || onOff == "OFF" {
|
|
val := "true"
|
|
if onOff == "OFF" {
|
|
val = "false"
|
|
}
|
|
g.writeln(fmt.Sprintf(`t.PushSymbol(t.VM().FindSymbol(%q))`, funcName))
|
|
g.writeln("t.PushNil()")
|
|
g.writeln(fmt.Sprintf("t.PushBool(%s)", val))
|
|
g.writeln("t.Do(1)")
|
|
}
|
|
break
|
|
}
|
|
|
|
// Value SET commands — SET DATE/DECIMALS/EPOCH TO expr
|
|
valueFuncMap := map[string]string{
|
|
"DATE": "__SETDATEFORMAT",
|
|
"DECIMALS": "SETDECIMALS",
|
|
"EPOCH": "SETEPOCH",
|
|
}
|
|
if funcName, ok := valueFuncMap[upper]; ok && s.Expr != nil {
|
|
g.writeln(fmt.Sprintf(`t.PushSymbol(t.VM().FindSymbol(%q))`, funcName))
|
|
g.writeln("t.PushNil()")
|
|
g.emitExpr(s.Expr)
|
|
g.writeln("t.Do(1)")
|
|
break
|
|
}
|
|
|
|
// Workarea-specific SET commands
|
|
g.writeln("{")
|
|
g.indent++
|
|
g.writeln("wa := t.WA.(*hbrdd.WorkAreaManager)")
|
|
g.writeln("if area := wa.Current(); area != nil {")
|
|
g.indent++
|
|
switch upper {
|
|
case "FILTER":
|
|
if s.Expr != nil {
|
|
g.emitExpr(s.Expr)
|
|
g.writeln(`area.SetFilter(t.Pop2().AsString(), nil)`)
|
|
} else {
|
|
g.writeln("area.ClearFilter()")
|
|
}
|
|
case "ORDER":
|
|
if s.Expr != nil {
|
|
g.writeln("if idx, ok := area.(hbrdd.Indexer); ok {")
|
|
g.indent++
|
|
g.emitExpr(s.Expr)
|
|
g.writeln(`{ _ov := t.Pop2(); var _os string; if _ov.IsNumeric() { _os = hbrt.NtoS(_ov.AsNumInt()) } else { _os = _ov.AsString() }; idx.OrderListFocus(_os) }`)
|
|
g.indent--
|
|
g.writeln("}")
|
|
}
|
|
case "INDEX":
|
|
if s.Expr != nil {
|
|
fileStr := exprToString(s.Expr)
|
|
g.writeln("if idx, ok := area.(hbrdd.Indexer); ok {")
|
|
g.indent++
|
|
if fileStr != "" {
|
|
// SET INDEX TO a, b, c — split comma-separated file names
|
|
// and call OrderListAdd for each. Harbour loads all NTX
|
|
// files into the active index list.
|
|
clean := fileStr
|
|
if len(clean) >= 2 && clean[0] == '"' && clean[len(clean)-1] == '"' {
|
|
clean = clean[1 : len(clean)-1]
|
|
}
|
|
parts := strings.Split(clean, ",")
|
|
for _, p := range parts {
|
|
p = strings.TrimSpace(p)
|
|
if p != "" {
|
|
g.writeln(fmt.Sprintf(`idx.OrderListAdd(%q)`, p))
|
|
}
|
|
}
|
|
} else {
|
|
g.emitExpr(s.Expr)
|
|
g.writeln(`idx.OrderListAdd(t.Pop2().AsString())`)
|
|
}
|
|
g.indent--
|
|
g.writeln("}")
|
|
} else {
|
|
g.writeln("if idx, ok := area.(hbrdd.Indexer); ok { idx.OrderListClear() }")
|
|
}
|
|
default:
|
|
g.writeln(fmt.Sprintf("// SET %s: not yet implemented", upper))
|
|
}
|
|
g.indent--
|
|
g.writeln("}")
|
|
g.indent--
|
|
g.writeln("}")
|
|
|
|
case *ast.SeqStmt:
|
|
g.emitBeginSequence(s, locals)
|
|
|
|
case *ast.AtSayCmd:
|
|
g.emitAtSayCmd(s)
|
|
case *ast.AtGetCmd:
|
|
g.emitAtGetCmd(s, locals)
|
|
case *ast.AtSayGetCmd:
|
|
g.emitAtSayGetCmd(s, locals)
|
|
case *ast.ReadCmd:
|
|
g.emitReadCmd(s, locals)
|
|
|
|
default:
|
|
g.writeln(fmt.Sprintf("// WARN: unhandled statement type %T — skipped", stmt))
|
|
}
|
|
}
|
|
|
|
func (g *Generator) emitMidVarDecl(s *ast.VarDecl, locals localMap) {
|
|
for _, v := range s.Vars {
|
|
idx, found := locals[strings.ToUpper(v.Name)]
|
|
if !found {
|
|
maxIdx := 0
|
|
for _, i := range locals {
|
|
if i > maxIdx {
|
|
maxIdx = i
|
|
}
|
|
}
|
|
idx = maxIdx + 1
|
|
locals[strings.ToUpper(v.Name)] = idx
|
|
}
|
|
if v.Init != nil {
|
|
g.emitExpr(v.Init)
|
|
g.writeln(fmt.Sprintf("t.PopLocalFast(%d)", idx))
|
|
}
|
|
}
|
|
}
|
|
|
|
func (g *Generator) emitQOut(s *ast.QOutStmt, locals localMap) {
|
|
sym := "QOUT"
|
|
if s.IsQQ {
|
|
sym = "QQOUT"
|
|
}
|
|
g.writeln(fmt.Sprintf("t.PushSymbol(t.VM().FindSymbol(%q))", sym))
|
|
g.writeln("t.PushNil()")
|
|
for _, expr := range s.Exprs {
|
|
g.emitExpr(expr)
|
|
}
|
|
g.writeln(fmt.Sprintf("t.Function(%d)", len(s.Exprs)))
|
|
}
|
|
|
|
func (g *Generator) emitExprStmt(s *ast.ExprStmt, locals localMap) {
|
|
// Check if it's an assignment
|
|
if assign, ok := s.X.(*ast.AssignExpr); ok {
|
|
g.emitAssign(assign, locals)
|
|
return
|
|
}
|
|
// Check if it's a function call (discard result)
|
|
if call, ok := s.X.(*ast.CallExpr); ok {
|
|
g.emitCallAsStmt(call, locals)
|
|
return
|
|
}
|
|
// Bare identifier as statement (e.g., CLS, CLEAR) — treat as zero-arg function call
|
|
if ident, ok := s.X.(*ast.IdentExpr); ok {
|
|
if _, found := locals[strings.ToUpper(ident.Name)]; !found {
|
|
g.writeln(fmt.Sprintf("t.PushSymbol(t.VM().FindSymbol(%q))", strings.ToUpper(ident.Name)))
|
|
g.writeln("t.PushNil()")
|
|
g.writeln("t.Do(0)")
|
|
return
|
|
}
|
|
}
|
|
// Postfix ++/--
|
|
if pf, ok := s.X.(*ast.PostfixExpr); ok {
|
|
// Local variable: n++
|
|
if ident, ok := pf.X.(*ast.IdentExpr); ok {
|
|
upper := strings.ToUpper(ident.Name)
|
|
if idx, found := locals[upper]; found {
|
|
if pf.Op == token.INC {
|
|
g.writeln(fmt.Sprintf("t.LocalAddInt(%d, 1)", idx))
|
|
} else {
|
|
g.writeln(fmt.Sprintf("t.LocalAddInt(%d, -1)", idx))
|
|
}
|
|
return
|
|
}
|
|
// STATIC variable: s_nPass++
|
|
if goVar, found := g.staticVars[upper]; found {
|
|
delta := "1"
|
|
if pf.Op == token.DEC {
|
|
delta = "-1"
|
|
}
|
|
g.writeln(fmt.Sprintf("{ _v := %s.AsNumInt() + %s; %s = hbrt.MakeInt(int(_v)) }", goVar, delta, goVar))
|
|
return
|
|
}
|
|
}
|
|
// Self field: ::field++
|
|
if send, ok := pf.X.(*ast.SendExpr); ok {
|
|
if _, isSelf := send.Object.(*ast.SelfExpr); isSelf {
|
|
fieldName := strings.ToUpper(send.Method)
|
|
g.writeln(fmt.Sprintf("t.PushSelfField(%q)", fieldName))
|
|
if pf.Op == token.INC {
|
|
g.writeln("t.PushInt(1)")
|
|
g.writeln("t.Plus()")
|
|
} else {
|
|
g.writeln("t.PushInt(1)")
|
|
g.writeln("t.Minus()")
|
|
}
|
|
g.writeln(fmt.Sprintf("t.SetSelfField(%q)", fieldName))
|
|
return
|
|
}
|
|
}
|
|
}
|
|
// General expression (result on stack, pop it)
|
|
g.emitExpr(s.X)
|
|
g.writeln("t.Pop()")
|
|
}
|
|
|
|
func (g *Generator) emitAssign(a *ast.AssignExpr, locals localMap) {
|
|
// Check for arr[idx] := value (array index assignment)
|
|
if idx, ok := a.Left.(*ast.IndexExpr); ok {
|
|
if a.Op == token.ASSIGN {
|
|
g.emitExpr(idx.X) // array
|
|
g.emitExpr(idx.Index) // index
|
|
g.emitExpr(a.Right) // value
|
|
g.writeln("t.ArrayPop()") // set array[index] = value
|
|
return
|
|
}
|
|
}
|
|
|
|
// Check for obj:field := value (object field assignment)
|
|
if send, ok := a.Left.(*ast.SendExpr); ok {
|
|
_, isSelf := send.Object.(*ast.SelfExpr)
|
|
|
|
if isSelf {
|
|
fieldName := strings.ToUpper(send.Method)
|
|
switch a.Op {
|
|
case token.ASSIGN:
|
|
g.emitExpr(a.Right)
|
|
g.writeln(fmt.Sprintf("t.SetSelfField(%q)", fieldName))
|
|
case token.PLUSEQ:
|
|
g.writeln(fmt.Sprintf("t.PushSelfField(%q)", fieldName))
|
|
g.emitExpr(a.Right)
|
|
g.writeln("t.Plus()")
|
|
g.writeln(fmt.Sprintf("t.SetSelfField(%q)", fieldName))
|
|
case token.MINUSEQ:
|
|
g.writeln(fmt.Sprintf("t.PushSelfField(%q)", fieldName))
|
|
g.emitExpr(a.Right)
|
|
g.writeln("t.Minus()")
|
|
g.writeln(fmt.Sprintf("t.SetSelfField(%q)", fieldName))
|
|
case token.STAREQ:
|
|
g.writeln(fmt.Sprintf("t.PushSelfField(%q)", fieldName))
|
|
g.emitExpr(a.Right)
|
|
g.writeln("t.Mult()")
|
|
g.writeln(fmt.Sprintf("t.SetSelfField(%q)", fieldName))
|
|
case token.SLASHEQ:
|
|
g.writeln(fmt.Sprintf("t.PushSelfField(%q)", fieldName))
|
|
g.emitExpr(a.Right)
|
|
g.writeln("t.Divide()")
|
|
g.writeln(fmt.Sprintf("t.SetSelfField(%q)", fieldName))
|
|
default:
|
|
g.emitExpr(a.Right)
|
|
g.writeln(fmt.Sprintf("t.SetSelfField(%q)", fieldName))
|
|
}
|
|
return
|
|
}
|
|
|
|
// Non-self: obj:field := value → obj:_FIELD(value)
|
|
if a.Op == token.ASSIGN {
|
|
g.emitExpr(send.Object)
|
|
g.emitExpr(a.Right)
|
|
g.writeln(fmt.Sprintf("t.Send(%q, 1)", "_"+strings.ToUpper(send.Method)))
|
|
g.writeln("t.Pop() // discard setter result")
|
|
return
|
|
}
|
|
}
|
|
|
|
// Check for alias->field := value (FIELD->NAME := value)
|
|
if aliasExpr, ok := a.Left.(*ast.AliasExpr); ok {
|
|
if aliasIdent, ok2 := aliasExpr.Alias.(*ast.IdentExpr); ok2 {
|
|
if fieldIdent, ok3 := aliasExpr.Field.(*ast.IdentExpr); ok3 {
|
|
g.emitExpr(a.Right)
|
|
g.writeln(fmt.Sprintf(`{ _wa := t.WA.(*hbrdd.WorkAreaManager); _wa.SetAliasField(%q, %q, t.Pop2()) }`, aliasIdent.Name, fieldIdent.Name))
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
if ident, ok := a.Left.(*ast.IdentExpr); ok {
|
|
if idx, found := locals[strings.ToUpper(ident.Name)]; found {
|
|
switch a.Op {
|
|
case token.ASSIGN:
|
|
g.emitExpr(a.Right)
|
|
g.writeln(fmt.Sprintf("t.PopLocalFast(%d)", idx))
|
|
case token.PLUSEQ:
|
|
g.emitExpr(a.Right)
|
|
g.writeln(fmt.Sprintf("t.LocalAdd(%d)", idx))
|
|
case token.MINUSEQ:
|
|
g.emitExpr(a.Right)
|
|
g.writeln("t.Negate()")
|
|
g.writeln(fmt.Sprintf("t.LocalAdd(%d)", idx))
|
|
default:
|
|
// General compound: push local, push right, op, pop local
|
|
g.writeln(fmt.Sprintf("t.PushLocalFast(%d)", idx))
|
|
g.emitExpr(a.Right)
|
|
g.emitBinaryOp(a.Op)
|
|
g.writeln(fmt.Sprintf("t.PopLocalFast(%d)", idx))
|
|
}
|
|
return
|
|
}
|
|
// Check module-level STATIC variable
|
|
upper := strings.ToUpper(ident.Name)
|
|
if goVar, found := g.staticVars[upper]; found {
|
|
g.emitExpr(a.Right)
|
|
g.writeln(fmt.Sprintf("%s = t.Pop2()", goVar))
|
|
return
|
|
}
|
|
}
|
|
// Fallback: general assignment via stack
|
|
g.emitExpr(a.Right)
|
|
g.writeln("// WARN: complex assignment target — simplified")
|
|
g.writeln("t.Pop()")
|
|
}
|
|
|
|
func (g *Generator) emitCallAsStmt(call *ast.CallExpr, locals localMap) {
|
|
if ident, ok := call.Func.(*ast.IdentExpr); ok {
|
|
g.writeln(fmt.Sprintf("t.PushSymbol(t.VM().FindSymbol(%q))", strings.ToUpper(ident.Name)))
|
|
} else {
|
|
g.emitExpr(call.Func)
|
|
}
|
|
g.writeln("t.PushNil()")
|
|
for _, arg := range call.Args {
|
|
g.emitExpr(arg)
|
|
}
|
|
g.writeln(fmt.Sprintf("t.Do(%d)", len(call.Args)))
|
|
}
|
|
|
|
func (g *Generator) emitIf(s *ast.IfStmt, locals localMap) {
|
|
g.emitExpr(s.Cond)
|
|
g.writeln("if t.PopLogical() {")
|
|
g.indent++
|
|
for _, stmt := range s.Body {
|
|
g.emitStmt(stmt, locals)
|
|
}
|
|
g.indent--
|
|
|
|
for _, ei := range s.ElseIfs {
|
|
g.writeIndent()
|
|
g.write("} else {\n")
|
|
g.indent++
|
|
g.emitExpr(ei.Cond)
|
|
g.writeln("if t.PopLogical() {")
|
|
g.indent++
|
|
for _, stmt := range ei.Body {
|
|
g.emitStmt(stmt, locals)
|
|
}
|
|
g.indent--
|
|
}
|
|
|
|
if len(s.ElseBody) > 0 {
|
|
g.writeln("} else {")
|
|
g.indent++
|
|
for _, stmt := range s.ElseBody {
|
|
g.emitStmt(stmt, locals)
|
|
}
|
|
g.indent--
|
|
}
|
|
|
|
g.writeln("}")
|
|
|
|
// Close nested elseif braces
|
|
for range s.ElseIfs {
|
|
g.writeln("}")
|
|
}
|
|
}
|
|
|
|
func (g *Generator) emitDoWhile(s *ast.DoWhileStmt, locals localMap) {
|
|
// Detect RDD commands in body for WA hoisting
|
|
hasRDD := hasRDDCommands(s.Body)
|
|
safeToHoist := hasRDD && !hasWorkareaChange(s.Body)
|
|
|
|
if safeToHoist && g.hoistedFields == nil {
|
|
g.writeln("{")
|
|
g.indent++
|
|
g.writeln("_dwa := t.WA.(*hbrdd.WorkAreaManager)")
|
|
g.writeln("_darea := _dwa.Current()")
|
|
g.hoistedDW = true
|
|
}
|
|
|
|
g.writeln("for {")
|
|
g.indent++
|
|
g.emitExpr(s.Cond)
|
|
g.writeln("if !t.PopLogical() { break }")
|
|
for _, stmt := range s.Body {
|
|
g.emitStmt(stmt, locals)
|
|
}
|
|
g.indent--
|
|
g.writeln("}")
|
|
|
|
if safeToHoist && g.hoistedDW {
|
|
g.hoistedDW = false
|
|
g.indent--
|
|
g.writeln("}")
|
|
}
|
|
}
|
|
|
|
// hasRDDCommands checks if any statement is an RDD operation.
|
|
func hasRDDCommands(stmts []ast.Stmt) bool {
|
|
for _, s := range stmts {
|
|
switch s.(type) {
|
|
case *ast.SkipCmd, *ast.GoCmd, *ast.SeekCmd,
|
|
*ast.ReplaceCmd, *ast.AppendCmd, *ast.DeleteCmd:
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// hasWorkareaChange checks for USE/SELECT that would invalidate cached area.
|
|
func hasWorkareaChange(stmts []ast.Stmt) bool {
|
|
for _, s := range stmts {
|
|
switch v := s.(type) {
|
|
case *ast.UseCmd, *ast.SelectCmd:
|
|
return true
|
|
case *ast.IfStmt:
|
|
if hasWorkareaChange(v.Body) || hasWorkareaChange(v.ElseBody) {
|
|
return true
|
|
}
|
|
case *ast.DoWhileStmt:
|
|
if hasWorkareaChange(v.Body) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// collectSymbols scans AST for all symbol names referenced by function calls.
|
|
// Returns unique names for hoisting FindSymbol to function prologue.
|
|
func collectSymbols(stmts []ast.Stmt) []string {
|
|
seen := map[string]bool{}
|
|
var names []string
|
|
var walk func([]ast.Stmt)
|
|
var walkExpr func(ast.Expr)
|
|
|
|
walkExpr = func(e ast.Expr) {
|
|
if e == nil {
|
|
return
|
|
}
|
|
switch v := e.(type) {
|
|
case *ast.CallExpr:
|
|
if ident, ok := v.Func.(*ast.IdentExpr); ok {
|
|
name := strings.ToUpper(ident.Name)
|
|
if !seen[name] {
|
|
seen[name] = true
|
|
names = append(names, name)
|
|
}
|
|
}
|
|
for _, a := range v.Args {
|
|
walkExpr(a)
|
|
}
|
|
case *ast.BinaryExpr:
|
|
walkExpr(v.Left)
|
|
walkExpr(v.Right)
|
|
case *ast.UnaryExpr:
|
|
walkExpr(v.X)
|
|
}
|
|
}
|
|
|
|
walk = func(stmts []ast.Stmt) {
|
|
for _, s := range stmts {
|
|
switch v := s.(type) {
|
|
case *ast.ExprStmt:
|
|
walkExpr(v.X)
|
|
case *ast.ReturnStmt:
|
|
if v.Value != nil {
|
|
walkExpr(v.Value)
|
|
}
|
|
case *ast.IfStmt:
|
|
walkExpr(v.Cond)
|
|
walk(v.Body)
|
|
walk(v.ElseBody)
|
|
case *ast.ForStmt:
|
|
walk(v.Body)
|
|
case *ast.ForEachStmt:
|
|
walk(v.Body)
|
|
case *ast.DoWhileStmt:
|
|
walkExpr(v.Cond)
|
|
walk(v.Body)
|
|
case *ast.SeqStmt:
|
|
walk(v.Body)
|
|
walk(v.RecoverBody)
|
|
case *ast.SwitchStmt:
|
|
for _, c := range v.Cases {
|
|
walk(c.Body)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
walk(stmts)
|
|
return names
|
|
}
|
|
|
|
// collectReplaceFields scans statements for REPLACE field names.
|
|
// Returns nil if unsafe to hoist (USE/SELECT/CLOSE found).
|
|
func collectReplaceFields(stmts []ast.Stmt) []string {
|
|
seen := map[string]bool{}
|
|
var fields []string
|
|
for _, s := range stmts {
|
|
switch v := s.(type) {
|
|
case *ast.ReplaceCmd:
|
|
for _, rf := range v.Fields {
|
|
if ident, ok := rf.Field.(*ast.IdentExpr); ok {
|
|
name := ident.Name
|
|
if !seen[name] {
|
|
seen[name] = true
|
|
fields = append(fields, name)
|
|
}
|
|
}
|
|
}
|
|
case *ast.UseCmd, *ast.SelectCmd:
|
|
return nil // workarea may change — unsafe to hoist
|
|
case *ast.IfStmt:
|
|
// Check nested blocks
|
|
if sub := collectReplaceFields(v.Body); sub == nil {
|
|
return nil
|
|
}
|
|
if sub := collectReplaceFields(v.ElseBody); sub == nil {
|
|
return nil
|
|
}
|
|
case *ast.DoWhileStmt:
|
|
if sub := collectReplaceFields(v.Body); sub == nil {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
return fields
|
|
}
|
|
|
|
// hasAppendInBody checks if any APPEND command exists in the statements.
|
|
func hasAppendInBody(stmts []ast.Stmt) bool {
|
|
for _, s := range stmts {
|
|
if _, ok := s.(*ast.AppendCmd); ok {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (g *Generator) emitFor(s *ast.ForStmt, locals localMap) {
|
|
idx, found := locals[strings.ToUpper(s.Var)]
|
|
if !found {
|
|
g.writeln("// ERROR: FOR variable not found in locals")
|
|
return
|
|
}
|
|
|
|
// i := start
|
|
g.emitExpr(s.Start)
|
|
g.writeln(fmt.Sprintf("t.PopLocalFast(%d)", idx))
|
|
|
|
// Detect step direction for comparison
|
|
isNegStep := false
|
|
if s.Step != nil {
|
|
if lit, ok := s.Step.(*ast.LiteralExpr); ok {
|
|
if lit.Kind == token.INT && len(lit.Value) > 0 && lit.Value[0] == '-' {
|
|
isNegStep = true
|
|
}
|
|
}
|
|
if un, ok := s.Step.(*ast.UnaryExpr); ok && un.Op == token.MINUS {
|
|
isNegStep = true
|
|
}
|
|
}
|
|
|
|
// Optimization: hoist WA/FieldIndex lookups outside FOR loop
|
|
// if body contains REPLACE and no USE/SELECT (safe to cache).
|
|
rddFields := collectReplaceFields(s.Body)
|
|
hoistRDD := len(rddFields) > 0 && hasAppendInBody(s.Body)
|
|
|
|
if hoistRDD {
|
|
g.writeln("{")
|
|
g.indent++
|
|
g.writeln("_rwa := t.WA.(*hbrdd.WorkAreaManager)")
|
|
g.writeln("_rarea := _rwa.Current()")
|
|
g.writeln("var _rdbf *dbf.DBFArea")
|
|
g.writeln("if _rarea != nil { _rdbf, _ = _rarea.(*dbf.DBFArea) }")
|
|
// Pre-compute field indexes
|
|
for i, fname := range rddFields {
|
|
g.writeln(fmt.Sprintf("var _rfi%d int = -1", i))
|
|
g.writeln(fmt.Sprintf("if _rdbf != nil { _rfi%d = _rdbf.FieldIndex(%q) }", i, fname))
|
|
}
|
|
g.hoistedFields = rddFields // store for emitReplaceCmdHoisted
|
|
}
|
|
|
|
g.writeln("for {")
|
|
g.indent++
|
|
|
|
// Comparison: fused opcode when limit is literal int (most common)
|
|
if lit, ok := s.To.(*ast.LiteralExpr); ok && lit.Kind == token.INT {
|
|
if isNegStep {
|
|
g.writeln(fmt.Sprintf("if !t.LocalGreaterEqualInt(%d, %s) { break }", idx, lit.Value))
|
|
} else {
|
|
g.writeln(fmt.Sprintf("if !t.LocalLessEqualInt(%d, %s) { break }", idx, lit.Value))
|
|
}
|
|
} else {
|
|
// General case: stack-based comparison
|
|
g.writeln(fmt.Sprintf("t.PushLocalFast(%d)", idx))
|
|
g.emitExpr(s.To)
|
|
if isNegStep {
|
|
g.writeln("t.GreaterEqual()")
|
|
} else {
|
|
g.writeln("t.LessEqual()")
|
|
}
|
|
g.writeln("if !t.PopLogical() { break }")
|
|
}
|
|
|
|
// Track FOR loop depth so LOOP can use goto instead of continue.
|
|
// Only emit label if LOOP is present in the body (Go rejects unused labels).
|
|
hasLoop := bodyHasLoop(s.Body)
|
|
forLabel := ""
|
|
prevForLabel := g.curForLabel
|
|
if hasLoop {
|
|
forLabel = fmt.Sprintf("_for_next_%d", g.forLabelSeq)
|
|
g.forLabelSeq++
|
|
g.curForLabel = forLabel
|
|
} else {
|
|
g.curForLabel = ""
|
|
}
|
|
|
|
// body
|
|
for _, stmt := range s.Body {
|
|
g.emitStmt(stmt, locals)
|
|
}
|
|
|
|
// Label for LOOP to jump to (skipping continue which would miss increment)
|
|
if hasLoop {
|
|
g.writeln(forLabel + ":")
|
|
}
|
|
|
|
// i += step (default 1)
|
|
if s.Step != nil {
|
|
g.emitExpr(s.Step)
|
|
g.writeln(fmt.Sprintf("t.LocalAdd(%d)", idx))
|
|
} else {
|
|
g.writeln(fmt.Sprintf("t.LocalAddInt(%d, 1)", idx))
|
|
}
|
|
|
|
g.curForLabel = prevForLabel
|
|
g.indent--
|
|
g.writeln("}")
|
|
|
|
// Close hoisting block
|
|
if hoistRDD {
|
|
g.hoistedFields = nil
|
|
g.indent--
|
|
g.writeln("}")
|
|
}
|
|
}
|
|
|
|
// bodyHasLoop checks if any statement in the body is a LOOP.
|
|
// Only checks the immediate level — LOOP inside nested FOR/DO WHILE is irrelevant.
|
|
func bodyHasLoop(stmts []ast.Stmt) bool {
|
|
for _, s := range stmts {
|
|
if hasLoopStmt(s) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func hasLoopStmt(s ast.Stmt) bool {
|
|
switch s := s.(type) {
|
|
case *ast.LoopStmt:
|
|
return true
|
|
case *ast.IfStmt:
|
|
for _, st := range s.Body {
|
|
if hasLoopStmt(st) {
|
|
return true
|
|
}
|
|
}
|
|
for _, ei := range s.ElseIfs {
|
|
for _, st := range ei.Body {
|
|
if hasLoopStmt(st) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
for _, st := range s.ElseBody {
|
|
if hasLoopStmt(st) {
|
|
return true
|
|
}
|
|
}
|
|
case *ast.SeqStmt:
|
|
for _, st := range s.Body {
|
|
if hasLoopStmt(st) {
|
|
return true
|
|
}
|
|
}
|
|
for _, st := range s.RecoverBody {
|
|
if hasLoopStmt(st) {
|
|
return true
|
|
}
|
|
}
|
|
case *ast.SwitchStmt:
|
|
for _, c := range s.Cases {
|
|
for _, st := range c.Body {
|
|
if hasLoopStmt(st) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
for _, st := range s.Otherwise {
|
|
if hasLoopStmt(st) {
|
|
return true
|
|
}
|
|
}
|
|
// Do NOT recurse into ForStmt/DoWhileStmt — nested LOOP is for the inner loop
|
|
}
|
|
return false
|
|
}
|
|
|
|
|
|
func (g *Generator) emitSwitch(s *ast.SwitchStmt, locals localMap) {
|
|
g.emitExpr(s.Expr)
|
|
g.writeln("_sw := t.Pop2()")
|
|
first := true
|
|
for _, c := range s.Cases {
|
|
if first {
|
|
g.emitExpr(c.Value)
|
|
g.writeln("if _sw.AsNumInt() == t.Pop2().AsNumInt() {")
|
|
first = false
|
|
} else {
|
|
g.emitExpr(c.Value)
|
|
g.writeln("} else if _sw.AsNumInt() == t.Pop2().AsNumInt() {")
|
|
}
|
|
g.indent++
|
|
for _, stmt := range c.Body {
|
|
g.emitStmt(stmt, locals)
|
|
}
|
|
g.indent--
|
|
}
|
|
if len(s.Otherwise) > 0 {
|
|
g.writeln("} else {")
|
|
g.indent++
|
|
for _, stmt := range s.Otherwise {
|
|
g.emitStmt(stmt, locals)
|
|
}
|
|
g.indent--
|
|
}
|
|
g.writeln("}")
|
|
}
|
|
|
|
func (g *Generator) emitBeginSequence(s *ast.SeqStmt, locals localMap) {
|
|
// BEGIN SEQUENCE → Go's panic/recover.
|
|
// Catches both *HbError (runtime errors) and BreakValue (Break() calls).
|
|
// BreakValue is defined in hbrtl, but we detect it via duck typing
|
|
// to avoid import cycles.
|
|
g.writeln("{ // BEGIN SEQUENCE")
|
|
g.indent++
|
|
g.writeln("_seqErr := func() (_recoverVal interface{}) {")
|
|
g.indent++
|
|
g.writeln("defer func() {")
|
|
g.indent++
|
|
g.writeln("if r := recover(); r != nil {")
|
|
g.indent++
|
|
g.writeln("_recoverVal = r")
|
|
g.indent--
|
|
g.writeln("}")
|
|
g.indent--
|
|
g.writeln("}()")
|
|
|
|
// Body
|
|
for _, stmt := range s.Body {
|
|
g.emitStmt(stmt, locals)
|
|
}
|
|
|
|
g.writeln("return nil")
|
|
g.indent--
|
|
g.writeln("}()")
|
|
|
|
// RECOVER
|
|
if len(s.RecoverBody) > 0 {
|
|
g.writeln("if _seqErr != nil {")
|
|
g.indent++
|
|
if s.RecoverVar != "" {
|
|
if idx, found := locals[strings.ToUpper(s.RecoverVar)]; found {
|
|
// Extract the value from the recovered panic:
|
|
// *HbError → error description string
|
|
// BreakValue (has .Value field) → the Break() argument
|
|
// other → string representation
|
|
g.writeln(fmt.Sprintf(`{ // RECOVER USING %s`, s.RecoverVar))
|
|
g.indent++
|
|
g.writeln(`switch _sv := _seqErr.(type) {`)
|
|
g.writeln(`case *hbrt.HbError:`)
|
|
g.writeln(fmt.Sprintf(` t.SetLocalFast(%d, hbrt.MakeString(_sv.Error()))`, idx))
|
|
g.writeln(`default:`)
|
|
// For BreakValue, use reflection-free approach: check if
|
|
// the type has a Value field via a local interface.
|
|
g.writeln(` type hasValue interface{ GetValue() hbrt.Value }`)
|
|
g.writeln(` if bv, ok := _sv.(hasValue); ok {`)
|
|
g.writeln(fmt.Sprintf(` t.SetLocalFast(%d, bv.GetValue())`, idx))
|
|
g.writeln(` } else {`)
|
|
g.writeln(fmt.Sprintf(` t.SetLocalFast(%d, hbrt.MakeString("error"))`, idx))
|
|
g.writeln(` }`)
|
|
g.writeln(`}`)
|
|
g.indent--
|
|
g.writeln(`}`)
|
|
}
|
|
}
|
|
for _, stmt := range s.RecoverBody {
|
|
g.emitStmt(stmt, locals)
|
|
}
|
|
g.indent--
|
|
g.writeln("}")
|
|
} else {
|
|
g.writeln("_ = _seqErr")
|
|
}
|
|
|
|
g.indent--
|
|
g.writeln("} // END SEQUENCE")
|
|
}
|
|
|
|
func (g *Generator) emitForEach(s *ast.ForEachStmt, locals localMap) {
|
|
varIdx, found := locals[strings.ToUpper(s.Var)]
|
|
if !found {
|
|
g.writeln("// ERROR: FOR EACH variable not in locals")
|
|
return
|
|
}
|
|
|
|
// Evaluate collection
|
|
g.emitExpr(s.Collection)
|
|
g.writeln("{ _feArr := t.Pop2()")
|
|
g.writeln("if _feArr.IsArray() {")
|
|
g.indent++
|
|
g.writeln("_feItems := _feArr.AsArray().Items")
|
|
g.writeln("for _feI := 0; _feI < len(_feItems); _feI++ {")
|
|
g.indent++
|
|
g.writeln(fmt.Sprintf("t.SetLocalFast(%d, _feItems[_feI])", varIdx))
|
|
|
|
for _, stmt := range s.Body {
|
|
g.emitStmt(stmt, locals)
|
|
}
|
|
|
|
g.indent--
|
|
g.writeln("}")
|
|
g.indent--
|
|
g.writeln("} }")
|
|
}
|
|
|
|
// --- Expression emission ---
|
|
// Each emitExpr leaves one value on the stack.
|
|
|
|
// emitMultiAssign: a, b := Func() or a, b := x, y
|
|
func (g *Generator) emitMultiAssign(s *ast.MultiAssignStmt, locals localMap) {
|
|
if len(s.Values) == 1 {
|
|
// Single RHS: a, b := Func() → call function, unpack array result
|
|
g.emitExpr(s.Values[0])
|
|
g.writeln("{")
|
|
g.indent++
|
|
g.writeln("_mr := t.Pop2()")
|
|
g.writeln("if _mr.IsArray() {")
|
|
g.indent++
|
|
g.writeln("_arr := _mr.AsArray()")
|
|
for i, name := range s.Targets {
|
|
if name == "_" {
|
|
continue
|
|
}
|
|
idx := locals[strings.ToUpper(name)]
|
|
if idx > 0 {
|
|
g.writeln(fmt.Sprintf("if %d < len(_arr.Items) { t.SetLocalFast(%d, _arr.Items[%d]) }", i, idx, i))
|
|
}
|
|
}
|
|
g.indent--
|
|
g.writeln("} else {")
|
|
g.indent++
|
|
// Not array — assign first target, rest get NIL
|
|
if s.Targets[0] != "_" {
|
|
idx := locals[strings.ToUpper(s.Targets[0])]
|
|
if idx > 0 {
|
|
g.writeln(fmt.Sprintf("t.SetLocalFast(%d, _mr)", idx))
|
|
}
|
|
}
|
|
g.indent--
|
|
g.writeln("}")
|
|
g.indent--
|
|
g.writeln("}")
|
|
} else {
|
|
// Multiple RHS: a, b := x, y (parallel assign)
|
|
// Evaluate all RHS first, then assign
|
|
for i, val := range s.Values {
|
|
g.emitExpr(val)
|
|
g.writeln(fmt.Sprintf("_mv%d := t.Pop2()", i))
|
|
}
|
|
for i, name := range s.Targets {
|
|
if name == "_" || i >= len(s.Values) {
|
|
continue
|
|
}
|
|
idx := locals[strings.ToUpper(name)]
|
|
if idx > 0 {
|
|
g.writeln(fmt.Sprintf("t.SetLocalFast(%d, _mv%d)", idx, i))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// emitDefer: DEFER expr → Go defer
|
|
func (g *Generator) emitDefer(s *ast.DeferStmt, locals localMap) {
|
|
g.writeln("defer func() {")
|
|
g.indent++
|
|
g.emitExpr(s.Call)
|
|
g.writeln("t.Pop() // discard defer result")
|
|
g.indent--
|
|
g.writeln("}()")
|
|
}
|
|
|
|
func (g *Generator) emitExpr(expr ast.Expr) {
|
|
switch e := expr.(type) {
|
|
case *ast.LiteralExpr:
|
|
g.emitLiteral(e)
|
|
case *ast.IdentExpr:
|
|
g.emitIdent(e)
|
|
case *ast.BinaryExpr:
|
|
// Short-circuit AND/OR: Harbour evaluates right operand only if needed
|
|
if e.Op == token.AND {
|
|
g.emitExpr(e.Left)
|
|
g.writeln("if !t.PopLogical() {")
|
|
g.writeln("t.PushBool(false)")
|
|
g.writeln("} else {")
|
|
g.emitExpr(e.Right)
|
|
g.writeln("}")
|
|
} else if e.Op == token.OR {
|
|
g.emitExpr(e.Left)
|
|
g.writeln("if t.PopLogical() {")
|
|
g.writeln("t.PushBool(true)")
|
|
g.writeln("} else {")
|
|
g.emitExpr(e.Right)
|
|
g.writeln("}")
|
|
} else {
|
|
g.emitExpr(e.Left)
|
|
g.emitExpr(e.Right)
|
|
g.emitBinaryOp(e.Op)
|
|
}
|
|
case *ast.UnaryExpr:
|
|
g.emitExpr(e.X)
|
|
g.emitUnaryOp(e.Op)
|
|
case *ast.AssignExpr:
|
|
// Handle compound assignment (+=, -=, :=) in expression context.
|
|
// Needed for code block bodies like {|x| nSum += x}.
|
|
g.emitAssignExpr(e)
|
|
case *ast.CallExpr:
|
|
g.emitCall(e)
|
|
case *ast.DotExpr:
|
|
// pkg.Member as value (rare — usually inside CallExpr)
|
|
g.writeln(fmt.Sprintf("t.PushValue(hbrt.WrapGo(%s.%s))", g.dotPkgName(e), e.Member))
|
|
case *ast.SendExpr:
|
|
g.emitSendExpr(e)
|
|
case *ast.IndexExpr:
|
|
g.emitExpr(e.X)
|
|
g.emitExpr(e.Index)
|
|
g.writeln("t.ArrayPush()")
|
|
case *ast.SelfExpr:
|
|
g.writeln("t.PushSelf()") // :: alone, rare
|
|
case *ast.ArrayLitExpr:
|
|
for _, item := range e.Items {
|
|
g.emitExpr(item)
|
|
}
|
|
g.writeln(fmt.Sprintf("t.ArrayGen(%d)", len(e.Items)))
|
|
case *ast.HashLitExpr:
|
|
for i := range e.Keys {
|
|
g.emitExpr(e.Keys[i])
|
|
g.emitExpr(e.Values[i])
|
|
}
|
|
g.writeln(fmt.Sprintf("t.HashGen(%d)", len(e.Keys)))
|
|
case *ast.BlockExpr:
|
|
g.emitBlock(e)
|
|
case *ast.SliceExpr:
|
|
// a[low:high] → hbrt.ArraySlice(array, low, high)
|
|
g.emitExpr(e.X)
|
|
if e.Low != nil {
|
|
g.emitExpr(e.Low)
|
|
} else {
|
|
g.writeln("t.PushInt(1)") // default: from start (1-based)
|
|
}
|
|
if e.High != nil {
|
|
g.emitExpr(e.High)
|
|
} else {
|
|
g.writeln("t.PushInt(-1)") // default: to end (-1 = all)
|
|
}
|
|
g.writeln("t.ArraySlice()")
|
|
case *ast.NilSafeExpr:
|
|
// obj?:Method() → if not nil, call; else push NIL
|
|
g.emitExpr(e.X)
|
|
g.writeln("{")
|
|
g.indent++
|
|
g.writeln("_ns := t.Pop2()")
|
|
g.writeln("if _ns.IsNil() {")
|
|
g.indent++
|
|
g.writeln("t.PushNil()")
|
|
g.indent--
|
|
g.writeln("} else {")
|
|
g.indent++
|
|
g.writeln("t.PushValue(_ns)")
|
|
for _, arg := range e.Args {
|
|
g.emitExpr(arg)
|
|
}
|
|
g.writeln(fmt.Sprintf("t.Send(%q, %d)", e.Method, len(e.Args)))
|
|
g.indent--
|
|
g.writeln("}")
|
|
g.indent--
|
|
g.writeln("}")
|
|
case *ast.InterpolatedString:
|
|
// Already converted to fmt.Sprintf CallExpr by parser
|
|
g.emitExpr(e.Parts[0]) // shouldn't reach here normally
|
|
case *ast.MacroExpr:
|
|
g.writeln("t.MacroPush() // runtime macro compilation")
|
|
g.writeln("t.PushNil()")
|
|
case *ast.AliasExpr:
|
|
g.emitAliasExpr(e)
|
|
case *ast.RefExpr:
|
|
// @variable — pass by reference
|
|
// In Five, we push a ByRef wrapper that holds the local index
|
|
if ident, ok := e.X.(*ast.IdentExpr); ok {
|
|
if idx, found := g.curLocals[strings.ToUpper(ident.Name)]; found {
|
|
g.writeln(fmt.Sprintf("t.PushLocalRef(%d)", idx))
|
|
} else {
|
|
g.emitExpr(e.X) // fallback: push value
|
|
}
|
|
} else {
|
|
g.emitExpr(e.X)
|
|
}
|
|
case *ast.IIfExpr:
|
|
g.emitExpr(e.Cond)
|
|
g.writeln("if t.PopLogical() {")
|
|
g.indent++
|
|
g.emitExpr(e.True)
|
|
g.indent--
|
|
g.writeln("} else {")
|
|
g.indent++
|
|
g.emitExpr(e.False)
|
|
g.indent--
|
|
g.writeln("}")
|
|
|
|
case *ast.PostfixExpr:
|
|
g.emitExpr(e.X)
|
|
g.writeln("t.Dup()")
|
|
if e.Op == token.INC {
|
|
g.writeln("t.Inc()")
|
|
} else {
|
|
g.writeln("t.Dec()")
|
|
}
|
|
g.writeln("t.Pop() // keep original for postfix")
|
|
default:
|
|
g.writeln(fmt.Sprintf("t.PushNil() // WARN: unhandled expr %T", expr))
|
|
}
|
|
}
|
|
|
|
// exprToString extracts a string representation from an AST expression.
|
|
// Used for INDEX ON key and filename, where idents are field/file names, not variables.
|
|
func exprToString(expr ast.Expr) string {
|
|
switch e := expr.(type) {
|
|
case *ast.IdentExpr:
|
|
return e.Name
|
|
case *ast.LiteralExpr:
|
|
if e.Kind == token.STRING {
|
|
return `"` + e.Value + `"`
|
|
}
|
|
return e.Value
|
|
case *ast.BinaryExpr:
|
|
left := exprToString(e.Left)
|
|
right := exprToString(e.Right)
|
|
opStr := ""
|
|
switch e.Op {
|
|
case token.PLUS:
|
|
opStr = "+"
|
|
case token.MINUS:
|
|
opStr = "-"
|
|
case token.EQ:
|
|
opStr = "="
|
|
case token.EXEQ:
|
|
opStr = "=="
|
|
case token.NEQ:
|
|
opStr = "!="
|
|
case token.LT:
|
|
opStr = "<"
|
|
case token.GT:
|
|
opStr = ">"
|
|
case token.LTE:
|
|
opStr = "<="
|
|
case token.GTE:
|
|
opStr = ">="
|
|
case token.AND:
|
|
opStr = ".AND."
|
|
case token.OR:
|
|
opStr = ".OR."
|
|
}
|
|
if opStr != "" {
|
|
return left + " " + opStr + " " + right
|
|
}
|
|
case *ast.UnaryExpr:
|
|
if e.Op == token.NOT {
|
|
return "!" + exprToString(e.X)
|
|
}
|
|
case *ast.CallExpr:
|
|
if ident, ok := e.Func.(*ast.IdentExpr); ok {
|
|
args := ""
|
|
for i, a := range e.Args {
|
|
if i > 0 {
|
|
args += ","
|
|
}
|
|
args += exprToString(a)
|
|
}
|
|
return ident.Name + "(" + args + ")"
|
|
}
|
|
case *ast.AliasExpr:
|
|
// FIELD->NAME, M->VAR, ALIAS->FIELD
|
|
alias := exprToString(e.Alias)
|
|
field := exprToString(e.Field)
|
|
return alias + "->" + field
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (g *Generator) emitLiteral(e *ast.LiteralExpr) {
|
|
switch e.Kind {
|
|
case token.INT:
|
|
g.writeln(fmt.Sprintf("t.PushInt(%s)", e.Value))
|
|
case token.DOUBLE:
|
|
g.writeln(fmt.Sprintf("t.PushDouble(%s, 255, 255)", e.Value))
|
|
case token.STRING:
|
|
g.writeln(fmt.Sprintf("t.PushString(%q)", e.Value))
|
|
case token.TRUE:
|
|
g.writeln("t.PushBool(true)")
|
|
case token.FALSE:
|
|
g.writeln("t.PushBool(false)")
|
|
case token.NIL_LIT:
|
|
g.writeln("t.PushNil()")
|
|
default:
|
|
g.writeln(fmt.Sprintf("t.PushNil() // WARN: unknown literal kind %v", e.Kind))
|
|
}
|
|
}
|
|
|
|
func (g *Generator) emitIdent(e *ast.IdentExpr) {
|
|
upper := strings.ToUpper(e.Name)
|
|
|
|
// Special: Self keyword → PushSelf
|
|
if upper == "SELF" {
|
|
g.writeln("t.PushSelf()")
|
|
return
|
|
}
|
|
|
|
if idx, found := g.curLocals[upper]; found {
|
|
g.writeln(fmt.Sprintf("t.PushLocalFast(%d)", idx))
|
|
} else if goVar, found := g.staticVars[upper]; found {
|
|
// Module-level STATIC variable
|
|
g.writeln(fmt.Sprintf("t.PushValue(%s)", goVar))
|
|
} else {
|
|
// Not a local — could be unresolved global variable or function ref
|
|
g.writeln(fmt.Sprintf("t.PushLocal(0) // UNRESOLVED: %q", e.Name))
|
|
}
|
|
}
|
|
|
|
func (g *Generator) emitCall(e *ast.CallExpr) {
|
|
// Check for Go package call: pkg.Func(args)
|
|
if dot, ok := e.Func.(*ast.DotExpr); ok {
|
|
if g.isGoPackage(dot) {
|
|
g.emitGoPackageCall(dot, e.Args)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Inline hot-path RTL functions — skip Frame/EndProc/VM dispatch entirely
|
|
if ident, ok := e.Func.(*ast.IdentExpr); ok {
|
|
if g.tryEmitInlineRTL(ident.Name, e.Args) {
|
|
return
|
|
}
|
|
upper := strings.ToUpper(ident.Name)
|
|
// Guard: reserved words must never be emitted as function calls.
|
|
// This catches PRG bugs like stray ENDIF/ENDDO/NEXT from bad IF nesting.
|
|
if isReservedWord(upper) {
|
|
g.writeln(fmt.Sprintf("// WARN: reserved word %q used as function call — skipped", upper))
|
|
g.writeln("t.PushNil()")
|
|
return
|
|
}
|
|
if g.symCache != nil {
|
|
if varName, ok := g.symCache[upper]; ok {
|
|
g.writeln(fmt.Sprintf("t.PushSymbol(%s)", varName))
|
|
} else {
|
|
g.writeln(fmt.Sprintf("t.PushSymbol(t.VM().FindSymbol(%q))", upper))
|
|
}
|
|
} else {
|
|
g.writeln(fmt.Sprintf("t.PushSymbol(t.VM().FindSymbol(%q))", upper))
|
|
}
|
|
} else {
|
|
g.emitExpr(e.Func)
|
|
}
|
|
g.writeln("t.PushNil()")
|
|
for _, arg := range e.Args {
|
|
g.emitExpr(arg)
|
|
}
|
|
g.writeln(fmt.Sprintf("t.Function(%d)", len(e.Args)))
|
|
}
|
|
|
|
// emitAssignExpr handles := / += / -= in expression context (e.g. code block body).
|
|
func (g *Generator) emitAssignExpr(e *ast.AssignExpr) {
|
|
// alias->field := value in expression context
|
|
if aliasExpr, ok := e.Left.(*ast.AliasExpr); ok {
|
|
if aliasIdent, ok2 := aliasExpr.Alias.(*ast.IdentExpr); ok2 {
|
|
if fieldIdent, ok3 := aliasExpr.Field.(*ast.IdentExpr); ok3 {
|
|
g.emitExpr(e.Right)
|
|
g.writeln("t.Dup()")
|
|
g.writeln(fmt.Sprintf(`{ _wa := t.WA.(*hbrdd.WorkAreaManager); _wa.SetAliasField(%q, %q, t.Pop2()) }`, aliasIdent.Name, fieldIdent.Name))
|
|
return
|
|
}
|
|
}
|
|
}
|
|
if ident, ok := e.Left.(*ast.IdentExpr); ok {
|
|
if idx, found := g.curLocals[strings.ToUpper(ident.Name)]; found {
|
|
switch e.Op {
|
|
case token.ASSIGN:
|
|
g.emitExpr(e.Right)
|
|
g.writeln("t.Dup()")
|
|
g.writeln(fmt.Sprintf("t.PopLocalFast(%d)", idx))
|
|
case token.PLUSEQ:
|
|
g.emitExpr(e.Right)
|
|
g.writeln(fmt.Sprintf("t.LocalAdd(%d)", idx))
|
|
g.writeln(fmt.Sprintf("t.PushLocalFast(%d)", idx))
|
|
case token.MINUSEQ:
|
|
g.emitExpr(e.Right)
|
|
g.writeln("t.Negate()")
|
|
g.writeln(fmt.Sprintf("t.LocalAdd(%d)", idx))
|
|
g.writeln(fmt.Sprintf("t.PushLocalFast(%d)", idx))
|
|
default:
|
|
g.writeln(fmt.Sprintf("t.PushLocalFast(%d)", idx))
|
|
g.emitExpr(e.Right)
|
|
g.emitBinaryOp(e.Op)
|
|
g.writeln("t.Dup()")
|
|
g.writeln(fmt.Sprintf("t.PopLocalFast(%d)", idx))
|
|
}
|
|
return
|
|
}
|
|
}
|
|
// Fallback: unknown target
|
|
g.emitExpr(e.Right)
|
|
g.writeln("t.Dup()")
|
|
g.writeln("// WARN: compound assignment — unknown target")
|
|
}
|
|
|
|
// isReservedWord returns true if the name is a Harbour reserved keyword
|
|
// that must never be emitted as a function call.
|
|
func isReservedWord(name string) bool {
|
|
switch name {
|
|
case "IF", "ELSE", "ELSEIF", "ENDIF",
|
|
"DO", "WHILE", "ENDDO",
|
|
"FOR", "NEXT", "TO", "STEP",
|
|
"RETURN", "FUNCTION", "PROCEDURE",
|
|
"LOCAL", "STATIC", "PRIVATE", "PUBLIC",
|
|
"BEGIN", "SEQUENCE", "RECOVER", "END",
|
|
"SWITCH", "CASE", "OTHERWISE", "ENDCASE",
|
|
"EXIT", "LOOP",
|
|
"CLASS", "ENDCLASS", "METHOD", "DATA",
|
|
"WITH", "OBJECT",
|
|
"NIL", "TRUE", "FALSE",
|
|
"AND", "OR", "NOT", "IN":
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// tryEmitInlineRTL emits direct Go code for known RTL functions.
|
|
// Returns true if handled (no VM dispatch needed).
|
|
// This eliminates Frame/EndProc/symbol lookup for hot-path functions.
|
|
func (g *Generator) tryEmitInlineRTL(name string, args []ast.Expr) bool {
|
|
upper := strings.ToUpper(name)
|
|
switch upper {
|
|
case "LTRIM":
|
|
if len(args) == 1 {
|
|
g.emitExpr(args[0])
|
|
g.writeln("{ _s := t.Pop2().AsString(); t.PushString(strings.TrimLeft(_s, \" \")) }")
|
|
g.imports["strings"] = true
|
|
return true
|
|
}
|
|
case "RTRIM", "TRIM":
|
|
if len(args) == 1 {
|
|
g.emitExpr(args[0])
|
|
g.writeln("{ _s := t.Pop2().AsString(); t.PushString(strings.TrimRight(_s, \" \")) }")
|
|
g.imports["strings"] = true
|
|
return true
|
|
}
|
|
case "ALLTRIM":
|
|
if len(args) == 1 {
|
|
g.emitExpr(args[0])
|
|
g.writeln("{ _s := t.Pop2().AsString(); t.PushString(strings.TrimSpace(_s)) }")
|
|
g.imports["strings"] = true
|
|
return true
|
|
}
|
|
case "UPPER":
|
|
if len(args) == 1 {
|
|
g.emitExpr(args[0])
|
|
g.writeln("{ _s := t.Pop2().AsString(); t.PushString(strings.ToUpper(_s)) }")
|
|
g.imports["strings"] = true
|
|
return true
|
|
}
|
|
case "LOWER":
|
|
if len(args) == 1 {
|
|
g.emitExpr(args[0])
|
|
g.writeln("{ _s := t.Pop2().AsString(); t.PushString(strings.ToLower(_s)) }")
|
|
g.imports["strings"] = true
|
|
return true
|
|
}
|
|
case "LEN":
|
|
if len(args) == 1 {
|
|
g.emitExpr(args[0])
|
|
g.writeln("{ _v := t.Pop2(); if _v.IsString() { t.PushInt(len(_v.AsString())) } else if _v.IsArray() { t.PushInt(len(_v.AsArray().Items)) } else if _v.IsHash() { t.PushInt(len(_v.AsHash().Keys)) } else { t.PushInt(0) } }")
|
|
return true
|
|
}
|
|
case "EMPTY":
|
|
if len(args) == 1 {
|
|
g.emitExpr(args[0])
|
|
g.writeln("{ _v := t.Pop2(); t.PushBool(_v.IsNil() || (_v.IsString() && len(strings.TrimSpace(_v.AsString())) == 0) || (_v.IsNumeric() && _v.AsNumDouble() == 0) || (_v.IsLogical() && !_v.AsBool()) || (_v.IsDate() && _v.AsJulian() == 0)) }")
|
|
g.imports["strings"] = true
|
|
return true
|
|
}
|
|
case "CHR":
|
|
if len(args) == 1 {
|
|
g.emitExpr(args[0])
|
|
g.writeln("t.PushString(string(byte(t.Pop2().AsNumInt())))")
|
|
return true
|
|
}
|
|
case "ASC":
|
|
if len(args) == 1 {
|
|
g.emitExpr(args[0])
|
|
g.writeln("{ _s := t.Pop2().AsString(); if len(_s)>0 { t.PushInt(int(_s[0])) } else { t.PushInt(0) } }")
|
|
return true
|
|
}
|
|
case "EOF":
|
|
if len(args) == 0 {
|
|
if g.hoistedDW {
|
|
g.writeln(fmt.Sprintf("t.PushBool(%s != nil && %s.EOF())", g.hoistedAreaVar(), g.hoistedAreaVar()))
|
|
} else {
|
|
g.writeln("{ _wa := t.WA.(*hbrdd.WorkAreaManager); if _a := _wa.Current(); _a != nil { t.PushBool(_a.EOF()) } else { t.PushBool(true) } }")
|
|
}
|
|
return true
|
|
}
|
|
case "BOF":
|
|
if len(args) == 0 {
|
|
if g.hoistedDW {
|
|
g.writeln(fmt.Sprintf("t.PushBool(%s != nil && %s.BOF())", g.hoistedAreaVar(), g.hoistedAreaVar()))
|
|
} else {
|
|
g.writeln("{ _wa := t.WA.(*hbrdd.WorkAreaManager); if _a := _wa.Current(); _a != nil { t.PushBool(_a.BOF()) } else { t.PushBool(true) } }")
|
|
}
|
|
return true
|
|
}
|
|
case "FOUND":
|
|
if len(args) == 0 {
|
|
if g.hoistedDW {
|
|
g.writeln(fmt.Sprintf("t.PushBool(%s != nil && %s.Found())", g.hoistedAreaVar(), g.hoistedAreaVar()))
|
|
} else {
|
|
g.writeln("{ _wa := t.WA.(*hbrdd.WorkAreaManager); if _a := _wa.Current(); _a != nil { t.PushBool(_a.Found()) } else { t.PushBool(false) } }")
|
|
}
|
|
return true
|
|
}
|
|
case "RECNO":
|
|
if len(args) == 0 {
|
|
if g.hoistedDW {
|
|
g.writeln(fmt.Sprintf("if %s != nil { t.PushInt(int(%s.RecNo())) } else { t.PushInt(0) }", g.hoistedAreaVar(), g.hoistedAreaVar()))
|
|
} else {
|
|
g.writeln("{ _wa := t.WA.(*hbrdd.WorkAreaManager); if _a := _wa.Current(); _a != nil { t.PushInt(int(_a.RecNo())) } else { t.PushInt(0) } }")
|
|
}
|
|
return true
|
|
}
|
|
case "DELETED":
|
|
if len(args) == 0 {
|
|
if g.hoistedDW {
|
|
g.writeln(fmt.Sprintf("t.PushBool(%s != nil && %s.Deleted())", g.hoistedAreaVar(), g.hoistedAreaVar()))
|
|
} else {
|
|
g.writeln("{ _wa := t.WA.(*hbrdd.WorkAreaManager); if _a := _wa.Current(); _a != nil { t.PushBool(_a.Deleted()) } else { t.PushBool(false) } }")
|
|
}
|
|
return true
|
|
}
|
|
case "RECCOUNT":
|
|
if len(args) == 0 {
|
|
g.writeln("{ _wa := t.WA.(*hbrdd.WorkAreaManager); if _a := _wa.Current(); _a != nil { _rc, _ := _a.RecCount(); t.PushInt(int(_rc)) } else { t.PushInt(0) } }")
|
|
return true
|
|
}
|
|
case "STR":
|
|
if len(args) >= 1 && len(args) <= 3 {
|
|
g.emitExpr(args[0])
|
|
g.writeln("{ _sv := t.Pop2()")
|
|
if len(args) >= 2 {
|
|
g.emitExpr(args[1])
|
|
g.writeln("_sw := int(t.Pop2().AsNumInt())")
|
|
} else {
|
|
g.writeln("_sw := 10")
|
|
}
|
|
if len(args) >= 3 {
|
|
g.emitExpr(args[2])
|
|
g.writeln("_sd := int(t.Pop2().AsNumInt())")
|
|
} else {
|
|
g.writeln("_sd := 0")
|
|
}
|
|
g.writeln("_ss := fmt.Sprintf(\"%*.*f\", _sw, _sd, _sv.AsNumDouble())")
|
|
g.writeln("if len(_ss) > _sw && _sw > 0 { _ss = strings.Repeat(\"*\", _sw) }")
|
|
g.writeln("t.PushString(_ss) }")
|
|
g.imports["fmt"] = true
|
|
g.imports["strings"] = true
|
|
return true
|
|
}
|
|
case "PADR":
|
|
if len(args) >= 2 {
|
|
g.emitExpr(args[0])
|
|
g.emitExpr(args[1])
|
|
g.writeln("{ _pn := int(t.Pop2().AsNumInt()); _ps := t.Pop2().AsString()")
|
|
g.writeln("if len(_ps) >= _pn { t.PushString(_ps[:_pn])")
|
|
g.writeln("} else { t.PushString(_ps + hbrtl.Spaces(_pn - len(_ps))) } }")
|
|
return true
|
|
}
|
|
case "PADL":
|
|
if len(args) >= 2 && len(args) <= 3 {
|
|
g.emitExpr(args[0])
|
|
g.emitExpr(args[1])
|
|
if len(args) == 3 {
|
|
g.emitExpr(args[2])
|
|
g.writeln("{ _pf := t.Pop2().AsString(); _pn := int(t.Pop2().AsNumInt()); _ps := t.Pop2().AsString()")
|
|
g.writeln("if len(_ps) >= _pn { t.PushString(_ps[len(_ps)-_pn:])")
|
|
g.writeln("} else { t.PushString(strings.Repeat(_pf[:1], _pn-len(_ps)) + _ps) } }")
|
|
g.imports["strings"] = true
|
|
} else {
|
|
g.writeln("{ _pn := int(t.Pop2().AsNumInt()); _ps := t.Pop2().AsString()")
|
|
g.writeln("if len(_ps) >= _pn { t.PushString(_ps[len(_ps)-_pn:])")
|
|
g.writeln("} else { t.PushString(hbrtl.Spaces(_pn - len(_ps)) + _ps) } }")
|
|
}
|
|
return true
|
|
}
|
|
case "SUBSTR", "SUBSTRING":
|
|
if len(args) >= 2 && len(args) <= 3 {
|
|
g.emitExpr(args[0])
|
|
g.emitExpr(args[1])
|
|
if len(args) == 3 {
|
|
g.emitExpr(args[2])
|
|
g.writeln("{ _sl := int(t.Pop2().AsNumInt()); _sp := int(t.Pop2().AsNumInt())-1; _ss := t.Pop2().AsString()")
|
|
} else {
|
|
g.writeln("{ _sl := 0; _sp := int(t.Pop2().AsNumInt())-1; _ss := t.Pop2().AsString(); _sl = len(_ss) - _sp")
|
|
}
|
|
g.writeln("if _sp < 0 { _sp = 0 }; if _sp > len(_ss) { _sp = len(_ss) }")
|
|
g.writeln("if _sp+_sl > len(_ss) { _sl = len(_ss) - _sp }")
|
|
g.writeln("t.PushString(_ss[_sp:_sp+_sl]) }")
|
|
return true
|
|
}
|
|
case "LEFT":
|
|
if len(args) == 2 {
|
|
g.emitExpr(args[0])
|
|
g.emitExpr(args[1])
|
|
g.writeln("{ _ln := int(t.Pop2().AsNumInt()); _ls := t.Pop2().AsString()")
|
|
g.writeln("if _ln >= len(_ls) { t.PushString(_ls) } else if _ln <= 0 { t.PushString(\"\") } else { t.PushString(_ls[:_ln]) } }")
|
|
return true
|
|
}
|
|
case "RIGHT":
|
|
if len(args) == 2 {
|
|
g.emitExpr(args[0])
|
|
g.emitExpr(args[1])
|
|
g.writeln("{ _rn := int(t.Pop2().AsNumInt()); _rs := t.Pop2().AsString()")
|
|
g.writeln("if _rn >= len(_rs) { t.PushString(_rs) } else if _rn <= 0 { t.PushString(\"\") } else { t.PushString(_rs[len(_rs)-_rn:]) } }")
|
|
return true
|
|
}
|
|
case "AT":
|
|
if len(args) == 2 {
|
|
g.emitExpr(args[0])
|
|
g.emitExpr(args[1])
|
|
g.writeln("{ _as := t.Pop2().AsString(); _ak := t.Pop2().AsString()")
|
|
g.writeln("_ai := strings.Index(_as, _ak)")
|
|
g.writeln("if _ai >= 0 { t.PushInt(_ai+1) } else { t.PushInt(0) } }")
|
|
g.imports["strings"] = true
|
|
return true
|
|
}
|
|
case "IIF":
|
|
if len(args) == 3 {
|
|
g.emitExpr(args[0])
|
|
g.writeln("if t.Pop2().AsBool() {")
|
|
g.indent++
|
|
g.emitExpr(args[1])
|
|
g.indent--
|
|
g.writeln("} else {")
|
|
g.indent++
|
|
g.emitExpr(args[2])
|
|
g.indent--
|
|
g.writeln("}")
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Spaces is exported for use by generated code.
|
|
func init() {
|
|
// hbrtl.Spaces is available to generated code via import
|
|
}
|
|
|
|
// isGoPackage checks if a DotExpr refers to an imported Go package.
|
|
func (g *Generator) isGoPackage(dot *ast.DotExpr) bool {
|
|
if ident, ok := dot.X.(*ast.IdentExpr); ok {
|
|
pkgName := ident.Name
|
|
// Check against imported package names
|
|
for path := range g.imports {
|
|
// "database/sql" → last segment "sql"
|
|
parts := strings.Split(path, "/")
|
|
name := parts[len(parts)-1]
|
|
if alias, ok := g.importAlias[path]; ok && alias != "_" && alias != "" {
|
|
name = alias
|
|
}
|
|
if name == pkgName {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// dotPkgName extracts the package identifier from a DotExpr.
|
|
func (g *Generator) dotPkgName(dot *ast.DotExpr) string {
|
|
if ident, ok := dot.X.(*ast.IdentExpr); ok {
|
|
return ident.Name
|
|
}
|
|
return "unknown"
|
|
}
|
|
|
|
// emitGoPackageCall generates direct Go function call with auto type bridging.
|
|
// PRG: result := sql.Open("sqlite", ":memory:")
|
|
// Go: { _r := hbrt.GoCallFunc(sql.Open, args...); t.PushValue(_r[0]) }
|
|
func (g *Generator) emitGoPackageCall(dot *ast.DotExpr, args []ast.Expr) {
|
|
pkg := g.dotPkgName(dot)
|
|
fn := dot.Member
|
|
qualName := pkg + "." + fn
|
|
regName := pkg + "_" + fn // safe Go variable name
|
|
|
|
// Register FastFunc in init block
|
|
g.goFastFuncs = append(g.goFastFuncs, goFastEntry{regName: regName, qualName: qualName})
|
|
|
|
// Build arg list
|
|
g.writeln("{")
|
|
g.indent++
|
|
|
|
argNames := make([]string, len(args))
|
|
for i, arg := range args {
|
|
argName := fmt.Sprintf("_a%d", i)
|
|
argNames[i] = argName
|
|
g.emitExpr(arg)
|
|
g.writeln(fmt.Sprintf("%s := t.Pop2()", argName))
|
|
}
|
|
|
|
argsStr := ""
|
|
for i, name := range argNames {
|
|
if i > 0 {
|
|
argsStr += ", "
|
|
}
|
|
argsStr += name
|
|
}
|
|
|
|
// Use FastPath (type-specialized, 3-11x faster than reflect)
|
|
g.writeln(fmt.Sprintf("_results := hbrt.GoCallFast(_ff_%s, %s)", regName, argsStr))
|
|
g.writeln("if len(_results) > 0 { t.PushValue(_results[0]) } else { t.PushNil() }")
|
|
g.indent--
|
|
g.writeln("}")
|
|
}
|
|
|
|
type goFastEntry struct {
|
|
regName string // Go variable: strings_ToUpper
|
|
qualName string // Go call: strings.ToUpper
|
|
}
|
|
|
|
func (g *Generator) emitAliasExpr(e *ast.AliasExpr) {
|
|
fieldIdent, isFieldIdent := e.Field.(*ast.IdentExpr)
|
|
|
|
// Case 1: alias->field (static alias, simple field name)
|
|
if ident, ok := e.Alias.(*ast.IdentExpr); ok && isFieldIdent {
|
|
g.writeln(fmt.Sprintf(`t.PushAliasField(%q, %q)`, ident.Name, fieldIdent.Name))
|
|
return
|
|
}
|
|
|
|
// Case 2: (expr)->field (dynamic alias, simple field name)
|
|
if isFieldIdent {
|
|
g.emitExpr(e.Alias)
|
|
g.writeln(fmt.Sprintf(`t.PushDynAliasField(t.Pop2().AsString(), %q)`, fieldIdent.Name))
|
|
return
|
|
}
|
|
|
|
// Case 3: alias->(expr) or (expr)->(expr) — workarea context expression
|
|
// Harbour: save current WA, select new WA, evaluate expr, restore WA
|
|
// Example: (nArea)->(Used()) → evaluate Used() in workarea nArea
|
|
// Example: CUSTOMERS->(RecCount()) → evaluate RecCount() in CUSTOMERS workarea
|
|
if ident, ok := e.Alias.(*ast.IdentExpr); ok {
|
|
_, isLocal := g.curLocals[strings.ToUpper(ident.Name)]
|
|
if isLocal {
|
|
// Local variable: emit value (numeric area number)
|
|
g.emitExpr(e.Alias)
|
|
g.writeln(`t.WASaveAndSelect(int(t.Pop2().AsNumInt()))`)
|
|
} else {
|
|
// Static alias name: resolve by alias string
|
|
g.writeln(fmt.Sprintf(`t.WASaveAndSelectAlias(%q)`, ident.Name))
|
|
}
|
|
} else {
|
|
// Dynamic: numeric area from expression
|
|
g.emitExpr(e.Alias)
|
|
g.writeln(`t.WASaveAndSelect(int(t.Pop2().AsNumInt()))`)
|
|
}
|
|
g.emitExpr(e.Field)
|
|
g.writeln(`t.WARestore()`)
|
|
}
|
|
|
|
func (g *Generator) fieldName(expr ast.Expr) string {
|
|
if ident, ok := expr.(*ast.IdentExpr); ok {
|
|
return ident.Name
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (g *Generator) emitSendExpr(e *ast.SendExpr) {
|
|
// Self access: ::field (no parens) → PushSelfField
|
|
// Self method: ::method() (has parens) → Send on Self
|
|
if _, isSelf := e.Object.(*ast.SelfExpr); isSelf {
|
|
if !e.HasParens && len(e.Args) == 0 {
|
|
// ::field (getter, no parentheses)
|
|
g.writeln(fmt.Sprintf("t.PushSelfField(%q)", strings.ToUpper(e.Method)))
|
|
return
|
|
}
|
|
// ::method() or ::method(args) — method call on Self
|
|
g.writeln("t.PushSelf()")
|
|
for _, arg := range e.Args {
|
|
g.emitExpr(arg)
|
|
}
|
|
g.writeln(fmt.Sprintf("t.Send(%q, %d)", e.Method, len(e.Args)))
|
|
return
|
|
}
|
|
|
|
// General: obj:method(args) or obj:field
|
|
// Check at runtime: if Go object → GoCall, else Harbour Send
|
|
g.emitExpr(e.Object)
|
|
g.writeln("{")
|
|
g.indent++
|
|
g.writeln("_obj := t.Pop2()")
|
|
|
|
// Push args and capture them
|
|
argNames := make([]string, len(e.Args))
|
|
for i, arg := range e.Args {
|
|
argNames[i] = fmt.Sprintf("_sa%d", i)
|
|
g.emitExpr(arg)
|
|
g.writeln(fmt.Sprintf("%s := t.Pop2()", argNames[i]))
|
|
}
|
|
|
|
g.writeln("if hbrt.IsGoObject(_obj) {")
|
|
g.indent++
|
|
// Go object: use reflect bridge
|
|
argsStr := ""
|
|
for i, name := range argNames {
|
|
if i > 0 {
|
|
argsStr += ", "
|
|
}
|
|
argsStr += name
|
|
}
|
|
g.writeln(fmt.Sprintf("_gr := hbrt.GoCallCached(_obj, %q, %s)", e.Method, argsStr))
|
|
g.writeln("if len(_gr) > 0 { t.PushValue(_gr[0]) } else { t.PushNil() }")
|
|
g.indent--
|
|
g.writeln("} else {")
|
|
g.indent++
|
|
// Harbour object: use Send
|
|
g.writeln("t.PushValue(_obj)")
|
|
for _, name := range argNames {
|
|
g.writeln(fmt.Sprintf("t.PushValue(%s)", name))
|
|
}
|
|
g.writeln(fmt.Sprintf("t.Send(%q, %d)", e.Method, len(e.Args)))
|
|
g.indent--
|
|
g.writeln("}")
|
|
g.indent--
|
|
g.writeln("}")
|
|
}
|
|
|
|
func (g *Generator) emitBlock(e *ast.BlockExpr) {
|
|
// Code block: {|params| body}
|
|
// Block params are passed via Frame() from Eval/AEval.
|
|
nParams := len(e.Params)
|
|
|
|
// Collect free variables in the block body that reference outer locals.
|
|
// These need to be captured via Go closure variables.
|
|
outerLocals := g.curLocals
|
|
blockLocals := make(localMap)
|
|
for i, p := range e.Params {
|
|
blockLocals[strings.ToUpper(p)] = i + 1
|
|
}
|
|
|
|
// Find all idents in block body that are in outerLocals but NOT in blockLocals
|
|
freeVars := g.collectFreeVars(e.Body, blockLocals, outerLocals)
|
|
|
|
// Harbour: closures share outer locals via RefCell (mutable capture).
|
|
// Convert each captured outer local to a RefCell, then pass the RefCell
|
|
// into the block. Both outer function and block read/write through it.
|
|
for _, fv := range freeVars {
|
|
outerIdx := outerLocals[fv]
|
|
// Ensure outer local is a RefCell (PushLocalRef creates one if needed,
|
|
// but we do it inline to avoid stack ops).
|
|
g.writeln(fmt.Sprintf("t.EnsureLocalRef(%d) // share %s via RefCell", outerIdx, fv))
|
|
}
|
|
|
|
// Capture the RefCell values with unique names to avoid Go scope issues.
|
|
capSeq := g.blockSeq
|
|
g.blockSeq++
|
|
capNames := make(map[string]string) // fv → Go var name
|
|
for _, fv := range freeVars {
|
|
outerIdx := outerLocals[fv]
|
|
capName := fmt.Sprintf("_cap_%s_%d", fv, capSeq)
|
|
g.writeln(fmt.Sprintf("%s := t.LocalRaw(%d) // capture RefCell %s", capName, outerIdx, fv))
|
|
capNames[fv] = capName
|
|
}
|
|
|
|
g.writeln(fmt.Sprintf("t.PushBlock(func(t *hbrt.Thread) {"))
|
|
g.indent++
|
|
nLocals := len(freeVars)
|
|
g.writeln(fmt.Sprintf("t.Frame(%d, %d)", nParams, nLocals))
|
|
g.writeln("defer t.EndProc()")
|
|
|
|
// Inject RefCell values directly into block locals — reads/writes go through RefCell
|
|
for i, fv := range freeVars {
|
|
localIdx := nParams + i + 1
|
|
blockLocals[fv] = localIdx
|
|
g.writeln(fmt.Sprintf("t.SetLocalRaw(%d, %s) // inject shared RefCell %s", localIdx, capNames[fv], fv))
|
|
}
|
|
|
|
g.curLocals = blockLocals
|
|
g.emitExpr(e.Body)
|
|
g.writeln("t.RetValue()")
|
|
|
|
g.curLocals = outerLocals
|
|
g.indent--
|
|
g.writeln(fmt.Sprintf("}, %d)", 0))
|
|
}
|
|
|
|
// collectFreeVars finds identifier names in expr that exist in outerLocals but not blockLocals.
|
|
func (g *Generator) collectFreeVars(expr ast.Expr, blockLocals, outerLocals localMap) []string {
|
|
var result []string
|
|
seen := map[string]bool{}
|
|
g.walkExprIdents(expr, func(name string) {
|
|
upper := strings.ToUpper(name)
|
|
if seen[upper] {
|
|
return
|
|
}
|
|
if _, inBlock := blockLocals[upper]; inBlock {
|
|
return
|
|
}
|
|
if _, inOuter := outerLocals[upper]; inOuter {
|
|
seen[upper] = true
|
|
result = append(result, upper)
|
|
}
|
|
})
|
|
return result
|
|
}
|
|
|
|
// walkExprIdents calls fn for each IdentExpr in the expression tree.
|
|
func (g *Generator) walkExprIdents(expr ast.Expr, fn func(string)) {
|
|
if expr == nil {
|
|
return
|
|
}
|
|
switch e := expr.(type) {
|
|
case *ast.IdentExpr:
|
|
fn(e.Name)
|
|
case *ast.BinaryExpr:
|
|
g.walkExprIdents(e.Left, fn)
|
|
g.walkExprIdents(e.Right, fn)
|
|
case *ast.UnaryExpr:
|
|
g.walkExprIdents(e.X, fn)
|
|
case *ast.PostfixExpr:
|
|
g.walkExprIdents(e.X, fn)
|
|
case *ast.CallExpr:
|
|
g.walkExprIdents(e.Func, fn)
|
|
for _, a := range e.Args {
|
|
g.walkExprIdents(a, fn)
|
|
}
|
|
case *ast.IndexExpr:
|
|
g.walkExprIdents(e.X, fn)
|
|
g.walkExprIdents(e.Index, fn)
|
|
case *ast.DotExpr:
|
|
g.walkExprIdents(e.X, fn)
|
|
case *ast.AssignExpr:
|
|
g.walkExprIdents(e.Left, fn)
|
|
g.walkExprIdents(e.Right, fn)
|
|
case *ast.ArrayLitExpr:
|
|
for _, item := range e.Items {
|
|
g.walkExprIdents(item, fn)
|
|
}
|
|
case *ast.IIfExpr:
|
|
g.walkExprIdents(e.Cond, fn)
|
|
g.walkExprIdents(e.True, fn)
|
|
g.walkExprIdents(e.False, fn)
|
|
case *ast.SendExpr:
|
|
g.walkExprIdents(e.Object, fn)
|
|
for _, a := range e.Args {
|
|
g.walkExprIdents(a, fn)
|
|
}
|
|
case *ast.AliasExpr:
|
|
g.walkExprIdents(e.Alias, fn)
|
|
g.walkExprIdents(e.Field, fn)
|
|
case *ast.BlockExpr:
|
|
g.walkExprIdents(e.Body, fn)
|
|
}
|
|
}
|
|
|
|
func (g *Generator) emitBinaryOp(op token.Kind) {
|
|
switch op {
|
|
case token.PLUS:
|
|
g.writeln("t.Plus()")
|
|
case token.MINUS:
|
|
g.writeln("t.Minus()")
|
|
case token.STAR:
|
|
g.writeln("t.Mult()")
|
|
case token.SLASH:
|
|
g.writeln("t.Divide()")
|
|
case token.PERCENT:
|
|
g.writeln("t.Modulus()")
|
|
case token.POWER:
|
|
g.writeln("t.Power()")
|
|
case token.EQ, token.EXEQ:
|
|
g.writeln("t.Equal()")
|
|
case token.NEQ:
|
|
g.writeln("t.NotEqual()")
|
|
case token.LT:
|
|
g.writeln("t.Less()")
|
|
case token.GT:
|
|
g.writeln("t.Greater()")
|
|
case token.LTE:
|
|
g.writeln("t.LessEqual()")
|
|
case token.GTE:
|
|
g.writeln("t.GreaterEqual()")
|
|
case token.AND:
|
|
g.writeln("t.And()")
|
|
case token.OR:
|
|
g.writeln("t.Or()")
|
|
case token.DOLLAR:
|
|
g.writeln("t.InString()") // $ operator
|
|
// Compound assign ops (shouldn't reach here normally)
|
|
case token.PLUSEQ:
|
|
g.writeln("t.Plus()")
|
|
case token.MINUSEQ:
|
|
g.writeln("t.Minus()")
|
|
case token.STAREQ:
|
|
g.writeln("t.Mult()")
|
|
case token.SLASHEQ:
|
|
g.writeln("t.Divide()")
|
|
default:
|
|
g.writeln(fmt.Sprintf("// WARN: unhandled binary op %v", op))
|
|
}
|
|
}
|
|
|
|
func (g *Generator) emitUnaryOp(op token.Kind) {
|
|
switch op {
|
|
case token.MINUS:
|
|
g.writeln("t.Negate()")
|
|
case token.NOT:
|
|
g.writeln("t.Not()")
|
|
case token.INC:
|
|
g.writeln("t.Inc()")
|
|
case token.DEC:
|
|
g.writeln("t.Dec()")
|
|
default:
|
|
g.writeln(fmt.Sprintf("// WARN: unhandled unary op %v", op))
|
|
}
|
|
}
|