Each PRG file's preprocessor instance was set up with only its OWN
directory on the include search path (`filepath.Dir(prgFile)`).
That worked for self-contained files but broke any multi-file
build where one PRG `#include`s a header that lives next to a
SIBLING PRG — the other file's directory wasn't on the path, so
the include silently failed and PP just skipped it ("// #include
\"FiveSqlDef.ch\" — not found (skipped)").
This was the root cause behind test_sql_standards's mass-failure
pattern. The test does
#include "FiveSqlDef.ch"
...
Assert( ..., h["columns"][1][1][1] == ND_FN .AND. ... )
`FiveSqlDef.ch` lives in `_FiveSql2/src/` (next to TSqlExecutor.prg
and friends), but the test source sits in `_FiveSql2/test/`.
Building with `./five build _FiveSql2/test/test_sql_standards.prg
_FiveSql2/src/*.prg` should resolve the header from a sibling
input file's directory — but only the test's own dir was searched,
so ND_FN / ND_LIT / ND_BIN / ND_UNI all stayed undefined and the
identifiers fell through to runtime memvar lookup, returning NIL.
Every assertion that compared against the constants therefore
silently failed (24 / 64 passing because non-constant assertions
still worked).
buildMultiPRGWithIncludes now seeds the user-include list with the
directory of every input PRG before handing off to buildMultiPRG.
A test under one directory can now resolve a `#include` that lives
next to a sibling source file in the same multi-file build.
Result: test_sql_standards goes from 24 / 64 to **64 / 64**. The
parser was already correct end-to-end — every SQL:2003-2023
construct it had been advertising actually worked; the test just
couldn't read the constants it was asserting against.
Wired test_sql_standards into the std.ch runner with a per-test
override so it picks up the FiveSql2 src files. Suite stands at
17/17.
Other gates green:
go test ./... : PASS
FiveSql2 SQL:1999 : 43/43
FiveSql2 standards : 64/64 (was 24/64)
Harbour compat : 56/56
std.ch suite : 17/17
FRB suite : 7/7
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
942 lines
27 KiB
Go
942 lines
27 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"
|
|
"strconv"
|
|
"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> [-b [module:]line ...] [--cli]")
|
|
}
|
|
prg := ""
|
|
var breakpoints []string
|
|
var watches []string
|
|
useCLI := false
|
|
for i := 2; i < len(os.Args); i++ {
|
|
a := os.Args[i]
|
|
switch a {
|
|
case "-b", "--break":
|
|
if i+1 >= len(os.Args) {
|
|
fatal("-b requires an argument: [module:]line")
|
|
}
|
|
breakpoints = append(breakpoints, os.Args[i+1])
|
|
i++
|
|
case "-w", "--watch":
|
|
if i+1 >= len(os.Args) {
|
|
fatal("-w requires an argument: <expr>")
|
|
}
|
|
watches = append(watches, os.Args[i+1])
|
|
i++
|
|
case "--cli":
|
|
useCLI = true
|
|
default:
|
|
if prg == "" {
|
|
prg = a
|
|
} else {
|
|
fatal("unexpected argument: " + a)
|
|
}
|
|
}
|
|
}
|
|
if prg == "" {
|
|
fatal("usage: five debug <file.prg> [-b [module:]line ...] [-w <expr> ...] [--cli]")
|
|
}
|
|
debugPRGWithOpts(prg, breakpoints, watches, useCLI)
|
|
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 — pgoArgs() adds -pgo=default.pgo when available.
|
|
cmd := exec.Command(goPath(), buildArgs(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(), buildArgs(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) {
|
|
// Multi-file builds bring along sibling PRGs whose own #include
|
|
// references a .ch file living next to them (e.g. FiveSqlDef.ch
|
|
// in _FiveSql2/src/). Each file's PP only adds its OWN dir by
|
|
// default, so a test under _FiveSql2/test/ couldn't find a .ch
|
|
// kept in _FiveSql2/src/. Promote every input file's dir into
|
|
// the shared user-include list so siblings can resolve each
|
|
// other's headers.
|
|
seen := map[string]bool{}
|
|
for _, dir := range includes {
|
|
seen[dir] = true
|
|
}
|
|
for _, f := range prgFiles {
|
|
dir := filepath.Dir(f)
|
|
if dir != "" && !seen[dir] {
|
|
seen[dir] = true
|
|
includes = append(includes, dir)
|
|
}
|
|
}
|
|
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)
|
|
}
|
|
if len(ppErrors) > 0 {
|
|
fatal(fmt.Sprintf("%d preprocessor error(s) in %s", len(ppErrors), prgFile))
|
|
}
|
|
|
|
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)
|
|
}
|
|
if len(ppErrors) > 0 {
|
|
fatal(fmt.Sprintf("%d preprocessor error(s) in %s", len(ppErrors), prgFile))
|
|
}
|
|
|
|
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)
|
|
}
|
|
if len(ppErrors) > 0 {
|
|
fatal(fmt.Sprintf("%d preprocessor error(s) in %s", len(ppErrors), prgFile))
|
|
}
|
|
|
|
// 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() }
|
|
|
|
// pgoArgs returns ["-pgo=<path>"] when the Five project root contains a
|
|
// default.pgo file — profile-guided compilation. Empty otherwise, so
|
|
// builds proceed without PGO when the profile hasn't been collected.
|
|
// The FIVE_NO_PGO env var forces it off (useful when collecting a new
|
|
// profile or A/B benchmarking).
|
|
func pgoArgs() []string {
|
|
if os.Getenv("FIVE_NO_PGO") != "" {
|
|
return nil
|
|
}
|
|
root := findFiveRoot()
|
|
p := filepath.Join(root, "default.pgo")
|
|
if fi, err := os.Stat(p); err == nil && !fi.IsDir() && fi.Size() > 0 {
|
|
return []string{"-pgo=" + p}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// buildArgs composes the full args for a `go build` invocation,
|
|
// inserting -pgo when a profile is available.
|
|
func buildArgs(output string) []string {
|
|
args := []string{"build"}
|
|
args = append(args, pgoArgs()...)
|
|
return append(args, "-o", output, ".")
|
|
}
|
|
|
|
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)
|
|
}
|
|
if len(ppErrors) > 0 {
|
|
fatal(fmt.Sprintf("%d preprocessor error(s) in %s", len(ppErrors), prgFile))
|
|
}
|
|
|
|
// 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 is kept as a thin wrapper for backward-compatibility — no
|
|
// pre-launch breakpoints, TUI frontend.
|
|
func debugPRG(prgFile string) { debugPRGWithOpts(prgFile, nil, nil, false) }
|
|
|
|
// parseBPSpec parses "[module:]line" into (module, line, ok). A bare
|
|
// "42" uses defaultMod. Colons inside module (Windows paths) aren't
|
|
// supported — use forward slashes.
|
|
func parseBPSpec(spec, defaultMod string) (string, int, bool) {
|
|
mod := defaultMod
|
|
lineStr := spec
|
|
if i := strings.LastIndex(spec, ":"); i > 0 {
|
|
mod = spec[:i]
|
|
lineStr = spec[i+1:]
|
|
}
|
|
n, err := strconv.Atoi(strings.TrimSpace(lineStr))
|
|
if err != nil || n <= 0 {
|
|
return "", 0, false
|
|
}
|
|
return mod, n, true
|
|
}
|
|
|
|
// debugPRGWithOpts compiles PRG with debug info and runs with interactive
|
|
// debugger. breakpoints is a list of "[module:]line" strings (module
|
|
// defaults to the PRG's basename). watches is a list of PRG expressions
|
|
// auto-evaluated at each stop. useCLI picks the gdb-style CLI frontend
|
|
// instead of the full-screen TUI.
|
|
func debugPRGWithOpts(prgFile string, breakpoints, watches []string, useCLI bool) {
|
|
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)
|
|
}
|
|
if len(ppErrors) > 0 {
|
|
fatal(fmt.Sprintf("%d preprocessor error(s) in %s", len(ppErrors), prgFile))
|
|
}
|
|
|
|
// 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)
|
|
|
|
// Build the debug setup: create Debugger, register any pre-launch
|
|
// breakpoints + watches, choose frontend, then run.
|
|
callback := "hbrt.TUIDebugger()"
|
|
if useCLI {
|
|
callback = "hbrt.CLIDebugger()"
|
|
}
|
|
prgBase := filepath.Base(prgFile)
|
|
var setupLines []string
|
|
for _, spec := range breakpoints {
|
|
mod, line, ok := parseBPSpec(spec, prgBase)
|
|
if !ok {
|
|
fatal(fmt.Sprintf("invalid -b %q — expected [module:]line", spec))
|
|
}
|
|
setupLines = append(setupLines,
|
|
fmt.Sprintf("vm.Debugger.AddBreakpoint(%q, %d)", mod, line))
|
|
}
|
|
for _, w := range watches {
|
|
setupLines = append(setupLines,
|
|
fmt.Sprintf("vm.Debugger.Watches = append(vm.Debugger.Watches, %q)", w))
|
|
}
|
|
// If any breakpoints were set, start in Continue mode so the program
|
|
// runs until it hits one. Otherwise keep step-line (legacy behavior).
|
|
startMode := "hbrt.DbgStepLine"
|
|
if len(breakpoints) > 0 {
|
|
startMode = "hbrt.DbgContinue"
|
|
}
|
|
debugSetup := fmt.Sprintf(
|
|
"vm.Debugger = hbrt.NewDebugger()\n\tvm.Debugger.SourceDir = %s\n\tvm.Debugger.Mode = %s\n\tvm.Debugger.Callback = %s\n\t%s\n\tvm.Run(\"MAIN\")",
|
|
fmt.Sprintf("%q", mustAbs(".")),
|
|
startMode,
|
|
callback,
|
|
strings.Join(setupLines, "\n\t"))
|
|
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)
|
|
}
|