Core change: - dbf.KeyEvalFunc: global callback set by gengo before OrderCreate - evalKeyExprInner default case: calls KeyEvalFunc for unknown functions - Final fallback: any unresolvable expression → KeyEvalFunc → MacroEval - valueToKeyBytes: converts MacroEval result to index key bytes - gengo: sets dbf.KeyEvalFunc = t.MacroEval before OrderCreate, clears after Examples that now work: INDEX ON MyFunc(FIELD->NAME) TO idx // UDF in key expression INDEX ON CityKey(FIELD->CITY, NAME) TO idx // multi-param UDF INDEX ON Left(MyFunc(NAME), 15) TO idx // nested built-in + UDF Also fixed: - SET ORDER TO n: int→string via hbrt.NtoS (was empty string) - CDX compound leaf decoder: proper bit-packed tag name extraction - CDX compound recNo = direct byte offset (not page number) All existing tests pass, NTX 47/47 + CDX 20/20 Harbour compat maintained. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1566 lines
40 KiB
Go
1566 lines
40 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
|
|
Debug bool // if true, emit t.DebugLine() calls
|
|
}
|
|
|
|
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()
|
|
}
|
|
|
|
return g.buf.String()
|
|
}
|
|
|
|
// --- 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
|
|
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.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")
|
|
}
|
|
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.PopLocal(%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[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[v.Name] = idx
|
|
idx++
|
|
}
|
|
}
|
|
}
|
|
return m
|
|
}
|
|
|
|
// --- 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:
|
|
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:
|
|
g.writeln("{ _wa := t.WA.(*hbrdd.WorkAreaManager)")
|
|
g.writeln("if _area := _wa.Current(); _area != nil { _area.Append() } }")
|
|
case *ast.DeleteCmd:
|
|
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 {
|
|
g.writeln("if idx, ok := area.(hbrdd.Indexer); ok {")
|
|
g.indent++
|
|
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) {
|
|
// LOCAL declared in mid-function: allocate new local slots dynamically
|
|
// For now, emit as local variable with initialization
|
|
for _, v := range s.Vars {
|
|
// Find or assign local index
|
|
idx, found := locals[v.Name]
|
|
if !found {
|
|
// Assign next available slot
|
|
maxIdx := 0
|
|
for _, i := range locals {
|
|
if i > maxIdx {
|
|
maxIdx = i
|
|
}
|
|
}
|
|
idx = maxIdx + 1
|
|
locals[v.Name] = idx
|
|
}
|
|
if v.Init != nil {
|
|
g.emitExpr(v.Init)
|
|
g.writeln(fmt.Sprintf("t.PopLocal(%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[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 {
|
|
if idx, found := locals[ident.Name]; 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
|
|
}
|
|
}
|
|
// 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
|
|
}
|
|
}
|
|
|
|
if ident, ok := a.Left.(*ast.IdentExpr); ok {
|
|
if idx, found := locals[ident.Name]; found {
|
|
switch a.Op {
|
|
case token.ASSIGN:
|
|
g.emitExpr(a.Right)
|
|
g.writeln(fmt.Sprintf("t.PopLocal(%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.PushLocal(%d)", idx))
|
|
g.emitExpr(a.Right)
|
|
g.emitBinaryOp(a.Op)
|
|
g.writeln(fmt.Sprintf("t.PopLocal(%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) {
|
|
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("}")
|
|
}
|
|
|
|
func (g *Generator) emitFor(s *ast.ForStmt, locals localMap) {
|
|
idx, found := locals[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.PopLocal(%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
|
|
}
|
|
}
|
|
|
|
g.writeln("for {")
|
|
g.indent++
|
|
|
|
// Comparison: ascending → i <= to, descending → i >= to
|
|
g.writeln(fmt.Sprintf("t.PushLocal(%d)", idx))
|
|
g.emitExpr(s.To)
|
|
if isNegStep {
|
|
g.writeln("t.GreaterEqual()")
|
|
} else {
|
|
g.writeln("t.LessEqual()")
|
|
}
|
|
g.writeln("if !t.PopLogical() { break }")
|
|
|
|
// body
|
|
for _, stmt := range s.Body {
|
|
g.emitStmt(stmt, locals)
|
|
}
|
|
|
|
// 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.indent--
|
|
g.writeln("}")
|
|
}
|
|
|
|
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.
|
|
// Use a _seqBreak flag to signal Break() was called.
|
|
// Break() panics with *HbError, caught by our recover.
|
|
g.writeln("{ // BEGIN SEQUENCE")
|
|
g.indent++
|
|
g.writeln("_seqErr := func() (_recoverErr *hbrt.HbError) {")
|
|
g.indent++
|
|
g.writeln("defer func() {")
|
|
g.indent++
|
|
g.writeln("if r := recover(); r != nil {")
|
|
g.indent++
|
|
g.writeln("if hbErr, ok := r.(*hbrt.HbError); ok {")
|
|
g.writeln(" _recoverErr = hbErr")
|
|
g.writeln("} else { panic(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[s.RecoverVar]; found {
|
|
g.writeln(fmt.Sprintf("t.SetLocal(%d, hbrt.MakeString(_seqErr.Error()))", idx))
|
|
}
|
|
}
|
|
for _, stmt := range s.RecoverBody {
|
|
g.emitStmt(stmt, locals)
|
|
}
|
|
g.indent--
|
|
g.writeln("}")
|
|
}
|
|
|
|
g.indent--
|
|
g.writeln("} // END SEQUENCE")
|
|
}
|
|
|
|
func (g *Generator) emitForEach(s *ast.ForEachStmt, locals localMap) {
|
|
varIdx, found := locals[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.SetLocal(%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.SetLocal(%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.SetLocal(%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.SetLocal(%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:
|
|
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:
|
|
g.emitExpr(e.Right)
|
|
g.writeln("t.Dup()")
|
|
g.writeln("// WARN: compound assignment — value on stack")
|
|
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[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[e.Name]; found {
|
|
g.writeln(fmt.Sprintf("t.PushLocal(%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
|
|
}
|
|
}
|
|
|
|
if ident, ok := e.Func.(*ast.IdentExpr); ok {
|
|
g.writeln(fmt.Sprintf("t.PushSymbol(t.VM().FindSymbol(%q))", strings.ToUpper(ident.Name)))
|
|
} 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)))
|
|
}
|
|
|
|
// 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) {
|
|
// alias->field or (expr)->field
|
|
// Push alias, then field name, call runtime FieldGet by name
|
|
if ident, ok := e.Alias.(*ast.IdentExpr); ok {
|
|
// Static alias: customers->name
|
|
g.writeln(fmt.Sprintf(`t.PushAliasField(%q, %q)`, ident.Name, g.fieldName(e.Field)))
|
|
} else {
|
|
// Dynamic: (cAlias)->field
|
|
g.emitExpr(e.Alias)
|
|
g.writeln(fmt.Sprintf(`t.PushDynAliasField(t.Pop2().AsString(), %q)`, g.fieldName(e.Field)))
|
|
}
|
|
}
|
|
|
|
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}
|
|
// The block function receives the SAME thread (t), not a new one.
|
|
// Block params are passed via Frame() from Eval/AEval.
|
|
nParams := len(e.Params)
|
|
g.writeln(fmt.Sprintf("t.PushBlock(func(t *hbrt.Thread) {"))
|
|
g.indent++
|
|
g.writeln(fmt.Sprintf("t.Frame(%d, 0)", nParams))
|
|
g.writeln("defer t.EndProc()")
|
|
|
|
// Build local map for block params
|
|
oldLocals := g.curLocals
|
|
blockLocals := make(localMap)
|
|
for i, p := range e.Params {
|
|
blockLocals[p] = i + 1
|
|
}
|
|
g.curLocals = blockLocals
|
|
|
|
g.emitExpr(e.Body)
|
|
g.writeln("t.RetValue()")
|
|
|
|
g.curLocals = oldLocals
|
|
g.indent--
|
|
g.writeln(fmt.Sprintf("}, %d)", 0))
|
|
}
|
|
|
|
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))
|
|
}
|
|
}
|