feat(dispatch): file-name routing via auto-renamed Main symbols (AOT)
Lets app/api/foo.prg keep its idiomatic `FUNCTION Main()` shape while
multiple such files compile into one binary. fnode auto-renames each
library file's Main into a unique symbol derived from the basename:
app/api/hello.prg -> HELLO__MAIN
app/api/admin-stats.prg -> ADMIN_STATS__MAIN (hyphen -> underscore)
Three moving parts:
cmd/fnode/main.go
parseOne for every PRG, then rename Main on every file except
the first (the entry). crossFile map updated so the analyzer
treats the renamed symbol as declared.
hbrtl_ext/dispatch/dispatch.go
New HB_FUNC FNODE_CALL(cFuncName) that does VM.FindSymbol +
PushSymbol/Function dance and discards the return value. Same
pattern pgserver's callPRG helper uses internally.
app/bridge_server.prg
BridgeDispatch now derives the symbol name from hReq["path"]
( /api/foo[.prg] -> FOO__MAIN ), invokes FNODE_CALL, and
maps "not found" errors to HTTP 404 (other errors -> 500).
Hardcoded /api/hello and /api/echo handlers replaced by the
path-driven model.
Verified end-to-end with app/api/hello.prg and app/api/admin-stats.prg:
GET /api/hello.prg -> 200 + JSON from HELLO__MAIN
GET /api/hello -> 200 (extension optional)
GET /api/admin-stats.prg?from=2026 -> 200 from ADMIN_STATS__MAIN
with query string echoed
GET /api/nope -> 404 "function not found"
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -38,6 +38,7 @@ var defaultRTL = []string{
|
||||
"fivenode_go/hbrtl_ext/httpserver",
|
||||
"fivenode_go/hbrtl_ext/bridge_capi",
|
||||
"fivenode_go/hbrtl_ext/pgrtl",
|
||||
"fivenode_go/hbrtl_ext/dispatch",
|
||||
}
|
||||
|
||||
func main() {
|
||||
@@ -167,6 +168,28 @@ func emitGeneratedSources(tmpDir string, o buildOpts) {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Auto-rename the Main() of each library file (i > 0) to a
|
||||
// unique name derived from its file basename so multiple .prg
|
||||
// files that all define `FUNCTION Main()` can be linked into
|
||||
// one binary. The dispatcher (FNODE_CALL or HTTP path router)
|
||||
// looks up the renamed symbol by the same convention:
|
||||
// app/api/admin-stats.prg → ADMIN_STATS__MAIN
|
||||
for i := 1; i < len(ps); i++ {
|
||||
newName := mainNameFor(ps[i].prgFile)
|
||||
renameMain(ps[i].file, newName)
|
||||
crossFile[newName] = true
|
||||
delete(crossFile, "MAIN") // only the entry file owns MAIN
|
||||
}
|
||||
// Re-add MAIN if the entry file declares it (it almost always
|
||||
// does); without this the analyzer would warn about the entry
|
||||
// file's Main looking undeclared.
|
||||
for _, d := range ps[0].file.Decls {
|
||||
if fd, ok := d.(*ast.FuncDecl); ok && strings.EqualFold(fd.Name, "Main") {
|
||||
crossFile["MAIN"] = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for i, p := range ps {
|
||||
diags := analyzer.Analyze(p.file, crossFile)
|
||||
for _, d := range diags {
|
||||
@@ -185,6 +208,31 @@ func emitGeneratedSources(tmpDir string, o buildOpts) {
|
||||
}
|
||||
}
|
||||
|
||||
// renameMain finds the file's top-level FUNCTION Main (case-insensitive)
|
||||
// and rewrites its Name to newName. Other functions / classes are left
|
||||
// alone so cross-file calls keep working. Returns whether a Main was
|
||||
// found and renamed.
|
||||
func renameMain(f *ast.File, newName string) bool {
|
||||
for _, d := range f.Decls {
|
||||
if fd, ok := d.(*ast.FuncDecl); ok && strings.EqualFold(fd.Name, "Main") {
|
||||
fd.Name = newName
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// mainNameFor derives the unique Main-symbol name from a .prg path.
|
||||
// `app/api/admin-stats.prg` → `ADMIN_STATS__MAIN`. Hyphens become
|
||||
// underscores so Harbour's identifier rules accept the result; the
|
||||
// "__MAIN" suffix lets the dispatcher recognise these as entry points
|
||||
// rather than helper functions.
|
||||
func mainNameFor(prgPath string) string {
|
||||
base := strings.TrimSuffix(filepath.Base(prgPath), filepath.Ext(prgPath))
|
||||
base = strings.ReplaceAll(base, "-", "_")
|
||||
return strings.ToUpper(base) + "__MAIN"
|
||||
}
|
||||
|
||||
func parseOne(prgFile string, includes []string) *ast.File {
|
||||
src, err := os.ReadFile(prgFile)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user