Files
five/cmd/five/main.go
CharlesKWON 8aaed994f4 perf(FiveSql2): hybrid fast path — 11x speedup on string WHERE scans
Implements hybrid execution model: keep AST tree-walk for SQL:2013+
features (Window, Recursive CTE, JOIN, aggregates) while compiling
simple SELECT hot paths to Go + pcode. See docs/FiveSql2-Hybrid-Plan.md
for the full architecture rationale (why not SQLite-style VDBE).

Hot path (single table, no joins/groups/aggregates):
  - TryBuildFieldPositions: resolves SELECT column list to FieldPos
    array once per query (bails to PRG loop on any complex expr).
  - TryCompileWhere + SqlExprToPrg: walks WHERE AST, emits equivalent
    PRG source, runs it through PcCompile to get a PcodeFunc.
  - SqlScan RTL: Go-native scan loop — GoTop/EOF/Skip/GetValue
    direct, ExecPcode per row for WHERE, result array pre-alloc.

WHERE compiler scope:
  - ND_LIT numeric/logical/string (string literals AllTrim'd to match
    SqlCmpEq CHAR-padding semantics; rejects embedded quotes/newlines)
  - ND_COL: CHAR fields auto-wrapped with AllTrim(FieldGet(n)) based
    on dbStruct() lookup cached once per query in aCompileStruct
  - ND_BIN: = <> != < <= > >= AND OR + - * /
  - ND_UNI: NOT -
  - Anything else (ND_FN, ND_CASE, ND_SUB, ND_PAR, LIKE, IN, IS NULL,
    BETWEEN, dates) returns NIL → falls back to PRG tree-walk.

Bench (50k rows, ~/tmp ext4):
                        Before      After     Speedup
  Numeric WHERE         ~150ms     11.7ms     ~13x
  String WHERE          119.3ms    10.5ms     11.4x
  No WHERE               -         14.6ms      -
  Raw RDD baseline        6.8ms     6.8ms      1.0x

Remaining gap to raw RDD (~1.5x) is structural: Value boxing, result
array construction, per-row ExecPcode frame overhead. Would need a
Value-pool or SoA refactor to close further.

Side fixes bundled:
  - TSqlIndex:FindExclusive short-circuited. Originally called
    dbInfo(DBI_FULLPATH)/DBI_SHARED which are unresolved symbols in
    Five (dbInfo is a stub, DBI_* never defined). Panic'd with
    "local variable index out of range: 0" whenever a standalone PRG
    had a workarea Used before calling five_SQL. 43-test masked the
    bug because it only reached FindExclusive with no open workareas.
    Restore the scan once dbInfo lands in hbrtl.
  - cmd/five/main.go: FIVE_KEEP_BUILD=1 env var keeps the temp Go
    project around for debugging gengo output.

Validation:
  - FiveSql2 43/43
  - Harbour compat 51/51
  - go test ./... ALL PASS

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 09:15:08 +09:00

798 lines
22 KiB
Go

// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
// All rights reserved.
// Five CLI — the Harbour + Go fusion language tool.
//
// Usage:
// five run <file.prg> Compile and run a PRG file
// five build <file.prg> [-o out] Compile to native binary
// five gen <file.prg> Generate Go source (for debugging)
package main
import (
"five/compiler/analyzer"
"five/compiler/ast"
"five/compiler/gengo"
"five/compiler/genpc"
"five/compiler/parser"
"five/compiler/pp"
"five/hbrt"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
)
func main() {
if len(os.Args) < 2 {
printUsage()
os.Exit(1)
}
cmd := os.Args[1]
switch cmd {
case "run":
if len(os.Args) < 3 {
fatal("usage: five run <file.prg>")
}
runPRG(os.Args[2])
case "build":
if len(os.Args) < 3 {
fatal("usage: five build <file.prg> [file2.prg ...] [-o output] [-I includedir]")
}
output := ""
var prgFiles []string
var includeDirs []string
for i := 2; i < len(os.Args); i++ {
if os.Args[i] == "-o" && i+1 < len(os.Args) {
output = os.Args[i+1]
i++
} else if os.Args[i] == "-I" && i+1 < len(os.Args) {
includeDirs = append(includeDirs, os.Args[i+1])
i++
} else if strings.HasPrefix(os.Args[i], "-I") {
includeDirs = append(includeDirs, os.Args[i][2:])
} else {
prgFiles = append(prgFiles, os.Args[i])
}
}
if len(prgFiles) == 1 {
buildPRGWithIncludes(prgFiles[0], output, includeDirs)
} else {
buildMultiPRGWithIncludes(prgFiles, output, includeDirs)
}
case "gen":
if len(os.Args) < 3 {
fatal("usage: five gen <file.prg>")
}
genPRG(os.Args[2])
case "debug":
if len(os.Args) < 3 {
fatal("usage: five debug <file.prg>")
}
debugPRG(os.Args[2])
case "frb":
if len(os.Args) < 3 {
fatal("usage: five frb <file.prg> [-o output.frb] [--pcode]")
}
output := ""
prgFile := os.Args[2]
pcodeMode := false
for i := 3; i < len(os.Args); i++ {
if os.Args[i] == "-o" && i+1 < len(os.Args) {
output = os.Args[i+1]
i++
} else if os.Args[i] == "--pcode" {
pcodeMode = true
}
}
if output == "" {
base := strings.TrimSuffix(filepath.Base(prgFile), filepath.Ext(prgFile))
output = base + ".frb"
}
if pcodeMode {
buildFRBPcode(prgFile, output)
} else {
buildFRB(prgFile, output)
}
case "version":
fmt.Println("Five 0.1.0 — Harbour + Go fusion language")
fmt.Println("Copyright (c) 2026 Charles KWON OhJun")
default:
fmt.Fprintf(os.Stderr, "unknown command: %s\n", cmd)
printUsage()
os.Exit(1)
}
}
func printUsage() {
fmt.Fprintln(os.Stderr, "Five — Harbour + Go fusion language")
fmt.Fprintln(os.Stderr, "")
fmt.Fprintln(os.Stderr, "Usage:")
fmt.Fprintln(os.Stderr, " five run <file.prg> Compile and run")
fmt.Fprintln(os.Stderr, " five build <file.prg> [-o out] Compile to binary")
fmt.Fprintln(os.Stderr, " five gen <file.prg> Show generated Go code")
fmt.Fprintln(os.Stderr, " five debug <file.prg> Debug with interactive debugger")
fmt.Fprintln(os.Stderr, " five frb <file.prg> [-o out] Compile to FRB module")
fmt.Fprintln(os.Stderr, " five version Show version")
}
// runPRG compiles a PRG file and runs it immediately.
func runPRG(prgFile string) {
goCode := compilePRG(prgFile)
// Create temp build directory
tmpDir, err := os.MkdirTemp("", "five-build-*")
if err != nil {
fatal("failed to create temp dir: " + err.Error())
}
if os.Getenv("FIVE_KEEP_BUILD") == "" {
defer os.RemoveAll(tmpDir)
} else {
fmt.Fprintln(os.Stderr, "[FIVE_KEEP_BUILD] keeping:", tmpDir)
}
writeGoProject(tmpDir, prgFile, goCode)
// go run
cmd := exec.Command(goPath(), "run", ".")
cmd.Dir = tmpDir
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
os.Exit(exitErr.ExitCode())
}
fatal("run failed: " + err.Error())
}
}
// buildPRG compiles a PRG file to a native binary.
func buildPRG(prgFile, output string) {
goCode := compilePRG(prgFile)
if output == "" {
base := strings.TrimSuffix(filepath.Base(prgFile), filepath.Ext(prgFile))
output = base
}
// Create temp build directory
tmpDir, err := os.MkdirTemp("", "five-build-*")
if err != nil {
fatal("failed to create temp dir: " + err.Error())
}
if os.Getenv("FIVE_KEEP_BUILD") == "" {
defer os.RemoveAll(tmpDir)
} else {
fmt.Fprintln(os.Stderr, "[FIVE_KEEP_BUILD] keeping:", tmpDir)
}
writeGoProject(tmpDir, prgFile, goCode)
// Resolve absolute path for output
absOutput, err := filepath.Abs(output)
if err != nil {
fatal("cannot resolve output path: " + err.Error())
}
// go build
cmd := exec.Command(goPath(), "build", "-o", absOutput, ".")
cmd.Dir = tmpDir
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
fatal("build failed: " + err.Error())
}
fmt.Fprintf(os.Stderr, "Built: %s\n", absOutput)
}
// genPRG generates Go source and prints it to stdout.
func genPRG(prgFile string) {
goCode := compilePRG(prgFile)
fmt.Print(goCode)
}
// buildMultiPRG compiles multiple PRG files into one binary.
func buildMultiPRG(prgFiles []string, output string) {
if output == "" {
base := strings.TrimSuffix(filepath.Base(prgFiles[0]), filepath.Ext(prgFiles[0]))
output = base
}
tmpDir, err := os.MkdirTemp("", "five-build-*")
if err != nil {
fatal("failed to create temp dir: " + err.Error())
}
if os.Getenv("FIVE_KEEP_BUILD") == "" {
defer os.RemoveAll(tmpDir)
} else {
fmt.Fprintln(os.Stderr, "[FIVE_KEEP_BUILD] keeping:", tmpDir)
}
// Phase 1: Parse all files and collect cross-file function names
type parsedFile struct {
file *ast.File
prgFile string
}
var parsed []parsedFile
crossFileFuncs := make(map[string]bool)
for _, prgFile := range prgFiles {
f := parsePRGFile(prgFile)
parsed = append(parsed, parsedFile{file: f, prgFile: prgFile})
for _, d := range f.Decls {
switch decl := d.(type) {
case *ast.FuncDecl:
crossFileFuncs[strings.ToUpper(decl.Name)] = true
case *ast.ClassDecl:
crossFileFuncs[strings.ToUpper(decl.Name)] = true
}
}
}
// Phase 2: Analyze and generate each file with cross-file function awareness
for i, pf := range parsed {
diags := analyzer.Analyze(pf.file, crossFileFuncs)
for _, d := range diags {
if d.Severity <= analyzer.SevWarning {
fmt.Fprintf(os.Stderr, "%s\n", d)
}
}
var goCode string
if i > 0 {
goCode = gengo.GenerateLibrary(pf.file)
} else {
goCode = gengo.Generate(pf.file)
}
goFile := fmt.Sprintf("prg_%d.go", i)
writeFile(filepath.Join(tmpDir, goFile), goCode)
}
// Write go.mod
fiveRoot := findFiveRoot()
goMod := fmt.Sprintf("module five-generated\n\ngo 1.21\n\nrequire five v0.0.0\n\nreplace five => %s\n", fiveRoot)
writeFile(filepath.Join(tmpDir, "go.mod"), goMod)
// go mod tidy
tidy := exec.Command(goPath(), "mod", "tidy")
tidy.Dir = tmpDir
tidy.Stderr = os.Stderr
if err := tidy.Run(); err != nil {
fmt.Fprintf(os.Stderr, "warning: go mod tidy: %v\n", err)
}
// go build
absOutput, err := filepath.Abs(output)
if err != nil {
fatal("cannot resolve output path: " + err.Error())
}
cmd := exec.Command(goPath(), "build", "-o", absOutput, ".")
cmd.Dir = tmpDir
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
fatal("build failed: " + err.Error())
}
fmt.Fprintf(os.Stderr, "Built: %s\n", absOutput)
}
// Global user include dirs (set by build command, used by compilePRGMode)
var userIncludeDirs []string
// buildPRGWithIncludes is buildPRG with -I support.
func buildPRGWithIncludes(prgFile, output string, includes []string) {
userIncludeDirs = includes
buildPRG(prgFile, output)
}
// buildMultiPRGWithIncludes is buildMultiPRG with -I support.
func buildMultiPRGWithIncludes(prgFiles []string, output string, includes []string) {
userIncludeDirs = includes
buildMultiPRG(prgFiles, output)
}
// parsePRGFile preprocesses and parses a PRG file, returning the AST.
func parsePRGFile(prgFile string) *ast.File {
source, err := os.ReadFile(prgFile)
if err != nil {
fatal("cannot read file: " + err.Error())
}
pre := pp.New()
pre.AddIncludeDir(filepath.Dir(prgFile))
pre.AddIncludeDir(filepath.Join(filepath.Dir(prgFile), "include"))
fiveRoot := findFiveRoot()
pre.AddIncludeDir(filepath.Join(fiveRoot, "include"))
if exePath, err := os.Executable(); err == nil {
pre.AddIncludeDir(filepath.Join(filepath.Dir(exePath), "include"))
}
for _, dir := range userIncludeDirs {
pre.AddIncludeDir(dir)
}
if hbInc := os.Getenv("HB_INC"); hbInc != "" {
pre.AddIncludeDir(hbInc)
}
for _, p := range []string{"/usr/local/include/harbour", "/usr/include/harbour"} {
if _, err := os.Stat(p); err == nil {
pre.AddIncludeDir(p)
}
}
processed, ppErrors := pre.Process(prgFile, string(source))
for _, e := range ppErrors {
fmt.Fprintf(os.Stderr, "pp: %s\n", e)
}
file, errs := parser.ParseWithGoDumps(prgFile, processed, pre.GoDumps)
if len(errs) > 0 {
for _, e := range errs {
fmt.Fprintf(os.Stderr, "%s\n", e)
}
fatal(fmt.Sprintf("%d parse error(s) in %s", len(errs), prgFile))
}
return file
}
// compilePRGMode compiles with library flag support.
func compilePRGMode(prgFile string, isLibrary bool) string {
source, err := os.ReadFile(prgFile)
if err != nil {
fatal("cannot read file: " + err.Error())
}
pre := pp.New()
pre.AddIncludeDir(filepath.Dir(prgFile))
pre.AddIncludeDir(filepath.Join(filepath.Dir(prgFile), "include"))
// Five's own include directory (from project root) — must come BEFORE Harbour system includes
fiveRoot := findFiveRoot()
pre.AddIncludeDir(filepath.Join(fiveRoot, "include"))
if exePath, err := os.Executable(); err == nil {
pre.AddIncludeDir(filepath.Join(filepath.Dir(exePath), "include"))
}
// User-specified -I directories — before Harbour system includes
for _, dir := range userIncludeDirs {
pre.AddIncludeDir(dir)
}
// Harbour include paths from environment or standard locations (LAST)
if hbInc := os.Getenv("HB_INC"); hbInc != "" {
pre.AddIncludeDir(hbInc)
}
for _, p := range []string{"/usr/local/include/harbour", "/usr/include/harbour"} {
if _, err := os.Stat(p); err == nil {
pre.AddIncludeDir(p)
}
}
processed, ppErrors := pre.Process(prgFile, string(source))
for _, e := range ppErrors {
fmt.Fprintf(os.Stderr, "pp: %s\n", e)
}
file, errs := parser.ParseWithGoDumps(prgFile, processed, pre.GoDumps)
if len(errs) > 0 {
for _, e := range errs {
fmt.Fprintf(os.Stderr, "%s\n", e)
}
fatal(fmt.Sprintf("%d parse error(s) in %s", len(errs), prgFile))
}
// Semantic analysis (warnings — non-fatal)
diags := analyzer.Analyze(file)
for _, d := range diags {
if d.Severity <= analyzer.SevWarning {
fmt.Fprintf(os.Stderr, "%s\n", d)
}
}
if isLibrary {
return gengo.GenerateLibrary(file)
}
return gengo.Generate(file)
}
// compilePRG preprocesses, parses, and generates Go source.
func compilePRG(prgFile string) string {
source, err := os.ReadFile(prgFile)
if err != nil {
fatal("cannot read file: " + err.Error())
}
// Phase 1: Preprocessor (#include, #define, #ifdef)
pre := pp.New()
pre.AddIncludeDir(filepath.Dir(prgFile))
pre.AddIncludeDir(filepath.Join(filepath.Dir(prgFile), "include"))
// Five's own include directory — MUST come before Harbour system includes
fiveRoot := findFiveRoot()
pre.AddIncludeDir(filepath.Join(fiveRoot, "include"))
if exePath, err := os.Executable(); err == nil {
pre.AddIncludeDir(filepath.Join(filepath.Dir(exePath), "include"))
}
// User-specified -I directories
for _, dir := range userIncludeDirs {
pre.AddIncludeDir(dir)
}
// Harbour include paths (LAST — Five's own headers take priority)
if harbourInc := os.Getenv("HB_INC"); harbourInc != "" {
pre.AddIncludeDir(harbourInc)
}
for _, p := range []string{"/usr/local/include/harbour", "/usr/include/harbour"} {
if _, err := os.Stat(p); err == nil {
pre.AddIncludeDir(p)
}
}
processed, ppErrors := pre.Process(prgFile, string(source))
for _, e := range ppErrors {
fmt.Fprintf(os.Stderr, "pp: %s\n", e)
}
// Phase 2: Parse
file, errs := parser.ParseWithGoDumps(prgFile, processed, pre.GoDumps)
if len(errs) > 0 {
for _, e := range errs {
fmt.Fprintf(os.Stderr, "%s\n", e)
}
fatal(fmt.Sprintf("%d parse error(s)", len(errs)))
}
// Phase 2.5: Analyze (warnings — non-fatal)
diags := analyzer.Analyze(file)
for _, d := range diags {
if d.Severity <= analyzer.SevWarning {
fmt.Fprintf(os.Stderr, "%s\n", d)
}
}
// Phase 3: Generate
return gengo.Generate(file)
}
// writeGoProject creates a temporary Go project with the generated code.
func writeGoProject(dir, prgFile, goCode string) {
// Find the Five module path (where go.mod is)
fiveRoot := findFiveRoot()
// go.mod with replace directive pointing to Five source
goMod := fmt.Sprintf(`module five-generated
go 1.21
require five v0.0.0
replace five => %s
`, fiveRoot)
writeFile(filepath.Join(dir, "go.mod"), goMod)
writeFile(filepath.Join(dir, "main.go"), goCode)
// Run go mod tidy to resolve dependencies
tidy := exec.Command(goPath(), "mod", "tidy")
tidy.Dir = dir
tidy.Stderr = os.Stderr
if err := tidy.Run(); err != nil {
// Non-fatal — some environments may not need it
_ = err
}
}
// findFiveRoot finds the root directory of the Five project (where go.mod is).
// findFiveRoot finds the Five project root (directory containing go.mod with "module five").
// Tries: 1) walk up from cwd, 2) walk up from executable. Fallback: cwd.
func findFiveRoot() string {
if root := walkUpForGoMod(""); root != "" {
return root
}
if exe, err := os.Executable(); err == nil {
if root := walkUpForGoMod(filepath.Dir(exe)); root != "" {
return root
}
}
abs, _ := filepath.Abs(".")
return abs
}
// walkUpForGoMod walks up from startDir (or cwd if empty) looking for go.mod.
func walkUpForGoMod(startDir string) string {
dir := startDir
if dir == "" {
dir, _ = os.Getwd()
}
for {
modPath := filepath.Join(dir, "go.mod")
if _, err := os.Stat(modPath); err == nil {
return dir
}
parent := filepath.Dir(dir)
if parent == dir {
break
}
dir = parent
}
return ""
}
// goPath is an alias for findGoBin (deduplicated).
func goPath() string { return findGoBin() }
func writeFile(path, content string) {
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
fatal("cannot write " + path + ": " + err.Error())
}
}
// buildFRB compiles a PRG to Five Runtime Binary (.frb).
// FRB = Go plugin (.so) wrapped in FRB header for runtime loading.
func buildFRB(prgFile, outputFile string) {
source, err := os.ReadFile(prgFile)
if err != nil {
fatal("cannot read file: " + err.Error())
}
// Phase 1: Preprocess
pre := pp.New()
pre.AddIncludeDir(filepath.Dir(prgFile))
pre.AddIncludeDir(filepath.Join(filepath.Dir(prgFile), "include"))
if exePath, err := os.Executable(); err == nil {
pre.AddIncludeDir(filepath.Join(filepath.Dir(exePath), "include"))
}
processed, ppErrors := pre.Process(prgFile, string(source))
for _, e := range ppErrors {
fmt.Fprintf(os.Stderr, "pp: %s\n", e)
}
// Phase 2: Parse
file, parseErrors := parser.ParseWithGoDumps(prgFile, processed, pre.GoDumps)
if len(parseErrors) > 0 {
for _, e := range parseErrors {
fmt.Fprintf(os.Stderr, "%s\n", e)
}
fatal(fmt.Sprintf("%d parse error(s)", len(parseErrors)))
}
// Phase 3: Generate Go source as library
goSrc := gengo.GenerateLibrary(file)
// Phase 4: Build as Go plugin
tmpDir, err := os.MkdirTemp("", "five-frb-*")
if err != nil {
fatal("cannot create temp dir: " + err.Error())
}
if os.Getenv("FIVE_KEEP_BUILD") == "" {
defer os.RemoveAll(tmpDir)
} else {
fmt.Fprintln(os.Stderr, "[FIVE_KEEP_BUILD] keeping:", tmpDir)
}
// Write go.mod — point to Five's module root
fiveRoot := mustAbs(".")
if exePath, err := os.Executable(); err == nil {
fiveRoot = filepath.Dir(exePath)
}
// Find go.mod in fiveRoot or parent directories
for d := fiveRoot; ; d = filepath.Dir(d) {
if _, err := os.Stat(filepath.Join(d, "go.mod")); err == nil {
fiveRoot = d
break
}
if d == filepath.Dir(d) {
break
}
}
modName := fmt.Sprintf("frbmod_%d_%d", os.Getpid(), time.Now().UnixNano())
goModContent := fmt.Sprintf("module %s\n\ngo 1.21.13\n\nrequire five v0.0.0\n\nreplace five => %s\n", modName, fiveRoot)
writeFile(filepath.Join(tmpDir, "go.mod"), goModContent)
// Write Go source (no additional exports needed — init() handles registration)
writeFile(filepath.Join(tmpDir, "module.go"), goSrc)
// Find go binary
goBin := findGoBin()
// go mod tidy
tidyCmd := exec.Command(goBin, "mod", "tidy")
tidyCmd.Dir = tmpDir
if output, err := tidyCmd.CombinedOutput(); err != nil {
fmt.Fprintf(os.Stderr, "tidy error: %s\n%s\n", err, string(output))
fatal("go mod tidy failed")
}
// Build plugin
soFile := filepath.Join(tmpDir, "module.so")
buildCmd := exec.Command(goBin, "build", "-buildmode=plugin", "-o", soFile, ".")
buildCmd.Dir = tmpDir
if output, err := buildCmd.CombinedOutput(); err != nil {
fmt.Fprintf(os.Stderr, "%s\n", string(output))
fatal("plugin build failed")
}
// Phase 5: Package as FRB
soData, err := os.ReadFile(soFile)
if err != nil {
fatal("cannot read .so: " + err.Error())
}
frbData := make([]byte, hbrt.FrbHeaderSize+len(soData))
frbData[0] = hbrt.FrbMagic0
frbData[1] = hbrt.FrbMagic1
frbData[2] = hbrt.FrbMagic2
frbData[3] = hbrt.FrbMagic3
frbData[4] = hbrt.FrbVersion1
frbData[5] = 0 // flags
// Flags: 0
frbData[6] = 0
frbData[7] = 0
// SymCount: 0 (symbols are self-registering via init())
frbData[8] = 0
frbData[9] = 0
frbData[10] = 0
frbData[11] = 0
copy(frbData[12:], soData)
if err := os.WriteFile(outputFile, frbData, 0755); err != nil {
fatal("cannot write FRB: " + err.Error())
}
fmt.Fprintf(os.Stderr, "FRB: %s (%d bytes)\n", outputFile, len(frbData))
}
// debugPRG compiles PRG with debug info and runs with interactive debugger.
func debugPRG(prgFile string) {
source, err := os.ReadFile(prgFile)
if err != nil {
fatal("cannot read file: " + err.Error())
}
// Phase 1: Preprocess
pre := pp.New()
pre.AddIncludeDir(filepath.Dir(prgFile))
pre.AddIncludeDir(filepath.Join(filepath.Dir(prgFile), "include"))
if exePath, err := os.Executable(); err == nil {
pre.AddIncludeDir(filepath.Join(filepath.Dir(exePath), "include"))
}
processed, ppErrors := pre.Process(prgFile, string(source))
for _, e := range ppErrors {
fmt.Fprintf(os.Stderr, "pp: %s\n", e)
}
// Phase 2: Parse
file, parseErrors := parser.ParseWithGoDumps(prgFile, processed, pre.GoDumps)
if len(parseErrors) > 0 {
for _, e := range parseErrors {
fmt.Fprintf(os.Stderr, "%s\n", e)
}
fatal(fmt.Sprintf("%d parse error(s)", len(parseErrors)))
}
// Phase 3: Generate Go with debug info
goSrc := gengo.GenerateWithDebug(file)
// Phase 4: Build and run (same as runPRG but with debug setup)
tmpDir, err := os.MkdirTemp("", "five-debug-*")
if err != nil {
fatal("cannot create temp dir: " + err.Error())
}
if os.Getenv("FIVE_KEEP_BUILD") == "" {
defer os.RemoveAll(tmpDir)
} else {
fmt.Fprintln(os.Stderr, "[FIVE_KEEP_BUILD] keeping:", tmpDir)
}
fiveRoot := findProjectRoot()
goMod := fmt.Sprintf("module five-generated\n\ngo 1.21.13\n\nrequire five v0.0.0\n\nreplace five => %s\n", fiveRoot)
writeFile(filepath.Join(tmpDir, "go.mod"), goMod)
// Add debug setup to main (use %q for safe path escaping)
debugSetup := fmt.Sprintf(
"vm.Debugger = hbrt.NewDebugger()\n\tvm.Debugger.SourceDir = %s\n\tvm.Debugger.Callback = hbrt.TUIDebugger()\n\tvm.Run(\"MAIN\")",
fmt.Sprintf("%q", mustAbs(".")))
goSrc = strings.Replace(goSrc, "vm.Run(\"MAIN\")", debugSetup, 1)
// Remove unused fmt import if added
// (no longer needed since we don't use fmt.Println in generated code)
writeFile(filepath.Join(tmpDir, "main.go"), goSrc)
goBin := findGoBin()
tidyCmd := exec.Command(goBin, "mod", "tidy")
tidyCmd.Dir = tmpDir
if out, err := tidyCmd.CombinedOutput(); err != nil {
fmt.Fprintf(os.Stderr, "warning: go mod tidy: %v\n%s", err, out)
}
runCmd := exec.Command(goBin, "run", ".")
runCmd.Dir = tmpDir
runCmd.Stdin = os.Stdin
runCmd.Stdout = os.Stdout
runCmd.Stderr = os.Stderr
runCmd.Run()
}
// findProjectRoot is an alias for findFiveRoot (deduplicated).
func findProjectRoot() string { return findFiveRoot() }
// buildFRBPcode compiles PRG to pcode FRB (no Go compiler needed to run).
func buildFRBPcode(prgFile, outputFile string) {
source, err := os.ReadFile(prgFile)
if err != nil {
fatal("cannot read file: " + err.Error())
}
// Phase 1: Preprocess
pre := pp.New()
pre.AddIncludeDir(filepath.Dir(prgFile))
pre.AddIncludeDir(filepath.Join(filepath.Dir(prgFile), "include"))
if exePath, err := os.Executable(); err == nil {
pre.AddIncludeDir(filepath.Join(filepath.Dir(exePath), "include"))
}
processed, _ := pre.Process(prgFile, string(source))
// Phase 2: Parse
file, parseErrors := parser.ParseWithGoDumps(prgFile, processed, pre.GoDumps)
if len(parseErrors) > 0 {
for _, e := range parseErrors {
fmt.Fprintf(os.Stderr, "%s\n", e)
}
fatal(fmt.Sprintf("%d parse error(s)", len(parseErrors)))
}
// Phase 3: Generate pcode
pcMod := genpc.Generate(file)
// Phase 4: Serialize
pcData := hbrt.SerializePcodeModule(pcMod)
// Phase 5: Write FRB
frbData := make([]byte, hbrt.FrbHeaderSize+len(pcData))
frbData[0] = hbrt.FrbMagic0
frbData[1] = hbrt.FrbMagic1
frbData[2] = hbrt.FrbMagic2
frbData[3] = hbrt.FrbMagic3
frbData[4] = hbrt.FrbVersion1
frbData[5] = 0
frbData[6] = hbrt.FrbModePcode
frbData[7] = 0
// func count
frbData[8] = byte(len(pcMod.Funcs))
frbData[9] = byte(len(pcMod.Funcs) >> 8)
frbData[10] = 0
frbData[11] = 0
frbData = append(frbData[:12], pcData...)
if err := os.WriteFile(outputFile, frbData, 0755); err != nil {
fatal("cannot write FRB: " + err.Error())
}
fmt.Fprintf(os.Stderr, "FRB (pcode): %s (%d bytes, %d functions)\n", outputFile, len(frbData), len(pcMod.Funcs))
}
func findGoBin() string {
// Try PATH first
if p, err := exec.LookPath("go"); err == nil {
return p
}
// Common locations
for _, p := range []string{"/usr/local/go/bin/go", "/usr/bin/go", "/snap/bin/go"} {
if _, err := os.Stat(p); err == nil {
return p
}
}
return "go"
}
func mustAbs(path string) string {
abs, err := filepath.Abs(path)
if err != nil {
return path
}
return abs
}
func fatal(msg string) {
fmt.Fprintf(os.Stderr, "five: %s\n", msg)
os.Exit(1)
}