fix(gengo): hoist #pragma BEGINDUMP imports + wire HB_FUNC registration

Two bugs blocked Five's own inline-Go feature:

1. Inline Go blocks placed mid-file couldn't carry an `import` list
   because Go rejects declarations before imports in the same file.
   examples/godump_demo.prg and friends (real Five demos) hit
   "syntax error: imports must appear before other declarations"
   during compile of the generated Go.

   hoistGoImports parses the raw dump body for `import (...)` blocks
   and single-form `import "path"` lines, registers each path into
   the generator's imports map, and returns the body with those
   directives stripped. The top-of-file import block then carries
   everything the dump needs.

2. HB_FUNC() calls inside the inline block's init() enqueue
   registrations into hbrt.dynamicFuncs, but the VM only promotes
   them to its symbol table when RegisterLibModules() is called.
   gengo's generated main() skipped that step, so dispatch on the
   inline-defined names panicked with "no function symbol for call".
   Emit vm.RegisterLibModules() after RegisterModule(symbols).

Verified: examples/godump_demo.prg builds and runs; the inline
GoUpper / GoFib / GoGCD / GoSplit / GoSquare / GoTypeOf functions
all dispatch. Matches the feature's original design intent.

FiveSql2 43/43, Harbour compat 56/56, Go test ALL PASS.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-18 17:58:49 +09:00
parent 5514780b11
commit b1024c5244

View File

@@ -800,6 +800,10 @@ func (g *Generator) emitMain() {
g.writeln("vm := hbrt.NewVM()")
g.writeln("hbrtl.RegisterRTL(vm)")
g.writeln("vm.RegisterModule(symbols)")
// Pick up any HB_FUNC() registrations from #pragma BEGINDUMP blocks
// — their package init() queues them into hbrt.dynamicFuncs, and
// RegisterLibModules pulls them into this VM.
g.writeln("vm.RegisterLibModules()")
// Find main function
mainName := "MAIN"
for _, sym := range g.symbols {
@@ -848,15 +852,76 @@ func (g *Generator) emitDecl(d ast.Decl) {
g.emitTopLevelStatic(decl)
}
case *ast.GoDumpDecl:
// Inline Go code from #pragma BEGINDUMP ... ENDDUMP
// Inline Go code from #pragma BEGINDUMP ... ENDDUMP. Extract
// any `import` directives inside the block and fold them into
// the generator's own imports map — Go insists imports live at
// the top of the file, so we can't just splat the raw dump in
// the middle of the declarations.
if decl.Code != "" {
body := hoistGoImports(decl.Code, g.imports)
g.writeln("\n// --- Inline Go code (#pragma BEGINDUMP) ---")
g.write(decl.Code)
g.write(body)
g.writeln("\n// --- End inline Go code ---\n")
}
}
}
// hoistGoImports scans a raw Go source block for top-level `import`
// statements, registers each imported path in `imports`, and returns
// the body with those directives stripped. Handles both forms:
//
// import "path"
// import (
// "path1"
// alias "path2"
// _ "path3"
// )
//
// Imports in the middle of the dump would violate Go's "imports first"
// rule. Five's own top-of-file import block will already include
// whatever the hoisted set adds, so the end result links correctly.
func hoistGoImports(code string, imports map[string]bool) string {
lines := strings.Split(code, "\n")
var out []string
i := 0
for i < len(lines) {
trim := strings.TrimSpace(lines[i])
// Block form: `import (`
if trim == "import (" || trim == "import(" {
i++
for i < len(lines) {
t := strings.TrimSpace(lines[i])
if t == ")" {
i++
break
}
// Parse `path`, `alias path`, `_ path` — each with
// quoted path as the final token.
if start := strings.Index(t, `"`); start >= 0 {
if end := strings.LastIndex(t, `"`); end > start {
imports[t[start+1:end]] = true
}
}
i++
}
continue
}
// Single form: `import "path"` or `import alias "path"`.
if strings.HasPrefix(trim, "import ") || strings.HasPrefix(trim, "import\t") {
if start := strings.Index(trim, `"`); start >= 0 {
if end := strings.LastIndex(trim, `"`); end > start {
imports[trim[start+1:end]] = true
}
}
i++
continue
}
out = append(out, lines[i])
i++
}
return strings.Join(out, "\n")
}
// emitTopLevelStatic emits module-level STATIC variables as package-level Go vars.
func (g *Generator) emitTopLevelStatic(vd *ast.VarDecl) {
for _, v := range vd.Vars {