Files
five/hbrt/frbmem.go
Charles KWON OhJun 59568f3301 Five v0.9 — Harbour + Go fusion language
- Compiler: PP → Lexer → Parser → Analyzer → Gengo pipeline
- Parser: 232/236 (98%) Harbour compatibility, registry-based dispatch
- RTL: 351 Harbour-compatible functions
- RDD: DBF/NTX/CDX engines with Rushmore bitmap optimization
- Go Interop: IMPORT + pkg.Func() + obj:Method() with FastPath (15M calls/sec)
- HB_FUNC API: Full Harbour C API compatible Go bridge
- Concurrency: SPAWN/LAUNCH/GOROUTINE, <-, WATCH, PARALLEL FOR, ASYNC/AWAIT
- Extensions: Multi-return, DEFER, Slice, f-string, Nil-safe ?:, CONST
- Macro Compiler: Runtime AST parsing and evaluation
- Debugger: TUI debugger with source display, breakpoints, stepping
- FRB: Native + Pcode dual mode runtime binary
- Tests: 13 packages ALL PASS

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 09:41:50 +09:00

233 lines
6.1 KiB
Go

// 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.
// If Go compiler is available, uses native plugin mode.
// If not, falls back to pcode interpreter mode (--pcode).
func FrbCompileSource(vm *VM, prgSource string, fiveExe string) (*FrbModule, error) {
// Check if Go is available
if !isGoAvailable() {
return frbCompilePcode(vm, prgSource, fiveExe)
}
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: "<dynamic>",
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