// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com) // All rights reserved. // FRB in-memory compilation — compile PRG source at runtime and execute. // This is Five's equivalent of Harbour's hb_compileFromBuf() + hb_hrbRun(). // // Usage from PRG: // pMod := FrbCompile(cPrgSource) // compile PRG string → FRB in memory // result := FrbDo(pMod, "MYFUNC", args) // call compiled function // FrbUnload(pMod) // // // Or one-shot: // result := FrbExec(cPrgSource) // compile + run Main() + unload package hbrt import ( "encoding/binary" "fmt" "os" "os/exec" "path/filepath" "plugin" ) // FrbCompileSource compiles PRG source code to an FRB module in memory. // Strategy: // 1. If Go isn't installed, go straight to pcode mode. // 2. Try the native Go-plugin path first (faster, native speed). // 3. If the plugin build fails, OR if loading the resulting plugin // fails (the most common failure: "plugin was built with a // different version of package runtime", which fires whenever // the host binary and the plugin weren't compiled byte-for-byte // against the same Go runtime — happens routinely after `go // build` rebuilds), fall back to pcode mode. Pcode is a few x // slower but always works. func FrbCompileSource(vm *VM, prgSource string, fiveExe string) (*FrbModule, error) { if !isGoAvailable() { return frbCompilePcode(vm, prgSource, fiveExe) } mod, err := frbCompilePlugin(vm, prgSource, fiveExe) if err == nil { return mod, nil } // Plugin path failed — try pcode. Don't surface the plugin // error: pcode either works (return its module) or doesn't // (return its error). return frbCompilePcode(vm, prgSource, fiveExe) } // frbCompilePlugin is the original native-plugin path, factored out // of FrbCompileSource so the latter can pick a fallback on failure. func frbCompilePlugin(vm *VM, prgSource string, fiveExe string) (*FrbModule, error) { tmpDir, err := os.MkdirTemp("", "frb-mem-*") if err != nil { return nil, err } // Write PRG source to temp file with unique name prgFile := filepath.Join(tmpDir, fmt.Sprintf("dynamic_%d.prg", frbSeq)) frbSeq++ if err := os.WriteFile(prgFile, []byte(prgSource), 0644); err != nil { os.RemoveAll(tmpDir) return nil, err } // Find five executable if fiveExe == "" { fiveExe, _ = os.Executable() } // Compile PRG → FRB using five frb command frbFile := filepath.Join(tmpDir, "dynamic.frb") cmd := exec.Command(fiveExe, "frb", prgFile, "-o", frbFile) if output, err := cmd.CombinedOutput(); err != nil { os.RemoveAll(tmpDir) return nil, fmt.Errorf("compile failed: %s\n%w", string(output), err) } // Load FRB mod, err := FrbLoad(vm, frbFile) if err != nil { os.RemoveAll(tmpDir) return nil, err } // Override TempDir to clean up everything mod.TempDir = tmpDir return mod, nil } // FrbCompileDirect compiles PRG source directly to a Go plugin without // going through the five CLI. Uses the compiler packages directly. // This is faster than FrbCompileSource for hot compilation. func FrbCompileDirect(vm *VM, prgSource string) (*FrbModule, error) { tmpDir, err := os.MkdirTemp("", "frb-direct-*") if err != nil { return nil, err } // We need the Five project root for go.mod replace directive fiveRoot := findFiveRoot() if fiveRoot == "" { os.RemoveAll(tmpDir) return nil, fmt.Errorf("cannot find Five project root (go.mod)") } // Write Go source — import compiler packages inline // This uses exec to run a helper that does the compilation helperSrc := fmt.Sprintf(`package main import ( "five/compiler/gengo" "five/compiler/parser" "five/compiler/pp" "fmt" "os" ) func main() { source := %q pre := pp.New() processed, _ := pre.Process("dynamic.prg", source) file, errs := parser.Parse("dynamic.prg", processed) if len(errs) > 0 { for _, e := range errs { fmt.Fprintln(os.Stderr, e) } os.Exit(1) } goSrc := gengo.GenerateLibrary(file) fmt.Print(goSrc) } `, prgSource) helperFile := filepath.Join(tmpDir, "helper.go") os.WriteFile(helperFile, []byte(helperSrc), 0644) // Write go.mod for helper goMod := fmt.Sprintf("module frbhelper\n\ngo 1.21\n\nrequire five v0.0.0\nreplace five => %s\n", fiveRoot) os.WriteFile(filepath.Join(tmpDir, "go.mod"), []byte(goMod), 0644) // Run go mod tidy + generate tidyCmd := exec.Command("go", "mod", "tidy") tidyCmd.Dir = tmpDir tidyCmd.CombinedOutput() genCmd := exec.Command("go", "run", "helper.go") genCmd.Dir = tmpDir goSrcBytes, err := genCmd.Output() if err != nil { os.RemoveAll(tmpDir) return nil, fmt.Errorf("codegen failed: %w", err) } // Write generated module.go os.WriteFile(filepath.Join(tmpDir, "module.go"), goSrcBytes, 0644) os.Remove(helperFile) // remove helper, keep module.go // Build plugin soFile := filepath.Join(tmpDir, "module.so") buildCmd := exec.Command("go", "build", "-buildmode=plugin", "-o", soFile, "module.go") buildCmd.Dir = tmpDir if output, err := buildCmd.CombinedOutput(); err != nil { os.RemoveAll(tmpDir) return nil, fmt.Errorf("plugin build failed: %s\n%w", string(output), err) } // Load plugin p, err := plugin.Open(soFile) if err != nil { os.RemoveAll(tmpDir) return nil, fmt.Errorf("plugin load failed: %w", err) } vm.RegisterLibModules() return &FrbModule{ Name: "", Plugin: p, TempDir: tmpDir, }, nil } // findFiveRoot locates the Five project root by searching for go.mod func findFiveRoot() string { // Try executable location first if exe, err := os.Executable(); err == nil { dir := filepath.Dir(exe) for d := dir; ; d = filepath.Dir(d) { if _, err := os.Stat(filepath.Join(d, "go.mod")); err == nil { return d } if d == filepath.Dir(d) { break } } } // Try current directory if cwd, err := os.Getwd(); err == nil { for d := cwd; ; d = filepath.Dir(d) { if _, err := os.Stat(filepath.Join(d, "go.mod")); err == nil { return d } if d == filepath.Dir(d) { break } } } return "" } var frbSeq int // sequence number for unique module names // isGoAvailable checks if the Go compiler is installed. func isGoAvailable() bool { for _, p := range []string{"go", "/usr/local/go/bin/go", "/usr/bin/go"} { if _, err := exec.LookPath(p); err == nil { return true } if _, err := os.Stat(p); err == nil { return true } } return false } // frbCompilePcode compiles PRG source to pcode FRB (no Go needed). func frbCompilePcode(vm *VM, prgSource string, fiveExe string) (*FrbModule, error) { tmpDir, err := os.MkdirTemp("", "frb-pcode-*") if err != nil { return nil, err } prgFile := filepath.Join(tmpDir, fmt.Sprintf("dynamic_%d.prg", frbSeq)) frbSeq++ os.WriteFile(prgFile, []byte(prgSource), 0644) frbFile := filepath.Join(tmpDir, "dynamic.frb") cmd := exec.Command(fiveExe, "frb", prgFile, "-o", frbFile, "--pcode") if output, err := cmd.CombinedOutput(); err != nil { os.RemoveAll(tmpDir) return nil, fmt.Errorf("pcode compile failed: %s\n%w", string(output), err) } mod, err := FrbLoad(vm, frbFile) if err != nil { os.RemoveAll(tmpDir) return nil, err } mod.TempDir = tmpDir return mod, nil } var _ = binary.LittleEndian // keep import