// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com) // All rights reserved. // FRB (Five Runtime Binary) RTL functions. // // PRG Usage: // pMod := FrbLoad("module.frb") // load module // FrbDo(pMod, "MYFUNC", args...) // call function // FrbUnload(pMod) // unload // // // Or one-shot: // result := FrbRun("module.frb", arg1, arg2) package hbrtl import ( "five/compiler/genpc" "five/compiler/parser" "five/compiler/pp" "five/hbrt" "fmt" "os" "os/exec" "path/filepath" "strings" ) // frbCompileInProc compiles PRG source to a pcode FrbModule entirely // in-process — no external `five` binary needed. Used by FrbCompile/ // FrbExec when the host can't shell out (running from a directory // where `five` isn't on PATH and isn't next to the binary). Avoids // the plugin-runtime-mismatch failure mode of native FRB plugins // AND removes the "find the five exe" fragility entirely. func frbCompileInProc(vm *hbrt.VM, prgSource string) (*hbrt.FrbModule, error) { prep := pp.New() processed, errs := prep.Process("dynamic.prg", prgSource) if len(errs) > 0 { return nil, fmt.Errorf("preprocess: %s", strings.Join(errs, "; ")) } file, perrs := parser.Parse("dynamic.prg", processed) if len(perrs) > 0 { msgs := make([]string, 0, len(perrs)) for _, e := range perrs { msgs = append(msgs, e.Error()) } return nil, fmt.Errorf("parse: %s", strings.Join(msgs, "; ")) } pcMod := genpc.Generate(file) // Surface unsupported-AST-node warnings on stderr so dynamic // FrbCompile callers see "pcode: AST node X not supported …" // instead of getting a silently-broken module back. for _, w := range pcMod.Warnings { fmt.Fprintln(os.Stderr, w) } // Build a FrbModule from the pcode functions. Mirrors what // hbrt/frb.go's frbLoadPcode does, but without the disk hop. frbMod := &hbrt.FrbModule{ Name: "dynamic", LocalSyms: make(map[string]*hbrt.Symbol), OldSyms: make(map[string]*hbrt.Symbol), BindMode: hbrt.FrbBindDefault, VM: vm, } for name, fn := range pcMod.Funcs { pcFn := fn pcModRef := pcMod goFunc := func(t *hbrt.Thread) { hbrt.ExecPcode(t, pcFn, pcModRef) } frbMod.LocalSyms[name] = &hbrt.Symbol{ Name: name, Scope: hbrt.FsPublic | hbrt.FsLocal, Func: goFunc, } } // Register non-Main symbols globally (Main stays module-local). for name, sym := range frbMod.LocalSyms { if name == "MAIN" { continue } old := vm.FindSymbol(name) if old != nil { frbMod.OldSyms[name] = old continue } vm.RegisterSymbol(sym) frbMod.Registered = append(frbMod.Registered, name) } return frbMod, nil } // findFiveExe locates the 'five' compiler binary func findFiveExe() string { // 1. Check same directory as running executable if exe, err := os.Executable(); err == nil { dir := filepath.Dir(exe) fiveExe := filepath.Join(dir, "five") if _, err := os.Stat(fiveExe); err == nil { return fiveExe } } // 2. Check PATH if p, err := exec.LookPath("five"); err == nil { return p } // 3. Check current directory if _, err := os.Stat("./five"); err == nil { abs, _ := filepath.Abs("./five") return abs } return "five" // hope it's in PATH } // FRBLOAD(cFileName) → pModule func FrbLoadFunc(t *hbrt.Thread) { t.Frame(1, 0) defer t.EndProc() filename := t.Local(1).AsString() mod, err := hbrt.FrbLoad(t.VM(), filename) if err != nil { fmt.Fprintf(os.Stderr, "FrbLoad error: %v\n", err) t.RetNil() return } t.RetPointer(mod) } // FRBDO(pModule, cFuncName [, args...]) → xResult func FrbDoFunc(t *hbrt.Thread) { nParams := t.ParamCount() t.Frame(nParams, 0) defer t.EndProc() modVal := t.Local(1) funcName := strings.ToUpper(t.Local(2).AsString()) // Look up function: module-local scope first, then VM global var fn func(*hbrt.Thread) if modVal.IsPointer() { if mod, ok := modVal.AsPointer().(*hbrt.FrbModule); ok { fn = mod.FindFunc(funcName) } } if fn == nil { sym := t.VM().FindSymbol(funcName) if sym != nil { fn = sym.Func } } if fn == nil { t.RetNil() return } // Snapshot SP *before* pushing args. After the inner call, // Frame()/PcOpRetValue should have left SP back at this baseline, // but pcode-mode bodies can occasionally leak intermediate stack // values (e.g. FOR-loop control vestiges). Reseating SP to the // snapshot before reading retVal stops those leaks from polluting // the caller's argument frame — which is what made // `? "label", FrbDo(...), "tail"` show "1" or "2" in place of the // label string when the inner function had a loop. savedSP := t.SP() // Push args for the function for i := 3; i <= nParams; i++ { t.PushValue(t.Local(i)) } t.PendingParams2(nParams - 2) fn(t) t.SetSP(savedSP) t.PushValue(t.GetRetValue()) t.RetValue() } // FRBUNLOAD(pModule) → NIL func FrbUnloadFunc(t *hbrt.Thread) { t.Frame(1, 0) defer t.EndProc() v := t.Local(1) if !v.IsNil() && v.IsPointer() { if mod, ok := v.AsPointer().(*hbrt.FrbModule); ok { hbrt.FrbUnload(mod) } } t.RetNil() } // FRBCOMPILE(cPrgSource) → pModule // Compile PRG source string to FRB module in memory. In-process pcode // compilation is the default — no external `five` binary or `go` // toolchain needed at runtime. The legacy native-plugin path is still // reachable via hbrt.FrbCompileSource for callers that want it, but // that path is fragile (Go plugins require byte-identical runtime). func FrbCompileFunc(t *hbrt.Thread) { t.Frame(1, 0) defer t.EndProc() source := t.Local(1).AsString() mod, err := frbCompileInProc(t.VM(), source) if err != nil { fmt.Fprintf(os.Stderr, "FrbCompile error: %v\n", err) t.RetNil() return } t.RetPointer(mod) } // FRBEXEC(cPrgSource [, args...]) → xResult // Compile PRG source, run Main(), unload — all in one call. func FrbExecFunc(t *hbrt.Thread) { nParams := t.ParamCount() t.Frame(nParams, 0) defer t.EndProc() source := t.Local(1).AsString() mod, err := frbCompileInProc(t.VM(), source) if err != nil { fmt.Fprintf(os.Stderr, "FrbExec error: %v\n", err) t.RetNil() return } defer hbrt.FrbUnload(mod) // Look up MAIN inside the freshly-compiled module first, NOT // via t.VM().FindSymbol — Main is intentionally kept module-local // (frbLoadPcode skips it during VM registration), so a global // lookup would resolve to the *caller's* Main and recurse forever. fn := mod.FindFunc("MAIN") if fn == nil { t.RetNil() return } savedSP := t.SP() for i := 2; i <= nParams; i++ { t.PushValue(t.Local(i)) } t.PendingParams2(nParams - 1) fn(t) t.SetSP(savedSP) t.PushValue(t.GetRetValue()) t.RetValue() } // FRBRUN(cFileName [, args...]) → xResult // Load, execute startup function, unload — all in one call. func FrbRunFunc(t *hbrt.Thread) { nParams := t.ParamCount() t.Frame(nParams, 0) defer t.EndProc() filename := t.Local(1).AsString() mod, err := hbrt.FrbLoad(t.VM(), filename) if err != nil { t.RetNil() return } defer hbrt.FrbUnload(mod) // Same module-local Main lookup as FrbExec — see comment there // for why a t.VM().FindSymbol("MAIN") would recurse into the // outer (caller's) Main. fn := mod.FindFunc("MAIN") if fn == nil { t.RetNil() return } savedSP := t.SP() for i := 2; i <= nParams; i++ { t.PushValue(t.Local(i)) } t.PendingParams2(nParams - 1) fn(t) t.SetSP(savedSP) t.PushValue(t.GetRetValue()) t.RetValue() }