// 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 { g := &Generator{ file: file, imports: map[string]bool{"five/hbrt": true, "five/hbrtl": true}, } // Collect symbols from declarations for _, d := range file.Decls { switch decl := d.(type) { case *ast.FuncDecl: scope := "hbrt.FsPublic|hbrt.FsLocal" if 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: // Class constructor symbol: Person() → HB_PERSON_CTOR className := strings.ToUpper(decl.Name) g.symbols = append(g.symbols, symbolEntry{ name: className, scope: "hbrt.FsPublic|hbrt.FsLocal", fn: "HB_" + className + "_CTOR", }) } } // Check if xBase commands are used — if so, add RDD imports if hasXBaseCommands(file) { g.imports["five/hbrdd"] = true g.imports["five/hbrdd/dbf"] = true } // Collect user imports 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 } } // Generate file g.emitHeader() g.emitSymbols() for _, d := range file.Decls { g.emitDecl(d) } g.emitFastFuncRegistrations() if !g.IsLibrary { g.emitMain() } else { g.emitInitModule() } return g.buf.String() } // GenerateDebug generates Go code with debug line hooks enabled. func (g *Generator) GenerateDebug(file *ast.File) string { g.file = file g.imports = map[string]bool{"five/hbrt": true, "five/hbrtl": true} g.Debug = true return Generate(file) // Generate uses package-level, need to thread debug flag through } // GenerateWithDebug is like Generate but includes DebugLine calls. func GenerateWithDebug(file *ast.File) string { g := &Generator{ file: file, imports: map[string]bool{"five/hbrt": true, "five/hbrtl": true}, Debug: true, } // Same logic as Generate but with debug flag for _, d := range file.Decls { switch decl := d.(type) { case *ast.FuncDecl: scope := "hbrt.FsPublic|hbrt.FsLocal" if 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() g.emitMain() return g.buf.String() } // GenerateLibrary generates Go code without main() — for multi-PRG builds. func GenerateLibrary(file *ast.File) string { g := &Generator{ file: file, imports: map[string]bool{"five/hbrt": true, "five/hbrtl": true}, IsLibrary: true, } for _, d := range file.Decls { switch decl := d.(type) { case *ast.FuncDecl: scope := "hbrt.FsPublic|hbrt.FsLocal" 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() g.emitInitModule() 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)) } g.writeln(fmt.Sprintf("idx.OrderCreate(hbrdd.OrderCreateParams{KeyExpr: _keyExpr, FilePath: _file, ForExpr: %s, Unique: %v, Descending: %v})", forExpr, s.Unique, s.Descending)) g.indent-- g.writeln("}") g.indent-- g.writeln("}") g.indent-- g.writeln("}") case *ast.SetCmd: g.writeln("{") g.indent++ g.writeln("wa := t.WA.(*hbrdd.WorkAreaManager)") g.writeln("if area := wa.Current(); area != nil {") g.indent++ upper := strings.ToUpper(s.Setting) switch upper { case "FILTER": if s.Expr != nil { g.writeln("if flt, ok := area.(hbrdd.Filterer); ok {") g.indent++ g.emitExpr(s.Expr) g.writeln(`flt.SetFilter(t.Pop2().AsString(), nil)`) g.indent-- g.writeln("}") } else { g.writeln("if flt, ok := area.(hbrdd.Filterer); ok { flt.ClearFilter() }") } case "ORDER": if s.Expr != nil { g.writeln("if idx, ok := area.(hbrdd.Indexer); ok {") g.indent++ g.emitExpr(s.Expr) g.writeln(`idx.OrderListFocus(t.Pop2().AsString())`) 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("// TODO: unhandled stmt %T", 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("// TODO: general assignment target") 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("// assign to: TODO") 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("// MACRO: TODO - runtime 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() // TODO: 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 + ")" } } 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() // TODO: 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("// TODO: 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("// TODO: unary op %v", op)) } }