- 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>
233 lines
6.1 KiB
Go
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
|