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:
2026-05-27 11:16:44 +09:00
parent 176f4e5cf5
commit b213f594aa
6 changed files with 161 additions and 60 deletions

View File

@@ -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 {