feat(static): file-local scoping for STATIC FUNCTION / PROCEDURE
Harbour STATIC FUNCTION / PROCEDURE is scoped to its source file —
multiple .prg files can each declare a `STATIC FUNCTION fn_HGet()`
without colliding. Five previously dropped them into the global VM
symbol table by their plain name, so multi-file builds (e.g. labdb's
22 .prg files where seven each defined their own STATIC fn_HGet)
either failed with redeclaration or silently linked every caller to
whichever definition won. fivenode_go's `sed` rename workaround can
now go away.
Mechanism
* ast.FuncDecl gains IsStatic. parser.go sets it whenever the
top-level STATIC keyword precedes FUNCTION / PROCEDURE.
* gengo records every same-file STATIC FUNCTION name in
g.staticFuncs. The symbol-table entry and the Go function name
for those declarations are mangled to
__STATIC__<fileKey>__<NAME>
so two files declaring `helper()` register two distinct symbols.
* emitPushSymbol rewrites call sites that match a name in
g.staticFuncs to the same mangled form, so same-file references
still resolve while cross-file references would look for a
symbol that doesn't exist.
* cmd/five/main.go's buildMultiPRG excludes STATIC FUNCTIONs from
the cross-file analyzer table — a foreign file calling another
file's STATIC now triggers a clean "undeclared variable" warning
instead of a runtime "function not found" deep inside vm.Run.
Verified
/tmp/a.prg + /tmp/b.prg each define `STATIC FUNCTION helper()`
returning their own string. Building both into one binary shows
each file calling its own helper:
a says: alpha (from a.prg)
(call B): beta (from b.prg)
Misuse (file X calling file Y's STATIC) now warns at compile
time. Full regression: go test ./compiler/... ./hbrt/...
./hbrtl/..., Compat 56/56, std.ch 17/17, FRB 7/7, FiveSql2 43/43.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -259,7 +259,15 @@ func buildMultiPRG(prgFiles []string, output string) {
|
||||
for _, d := range f.Decls {
|
||||
switch decl := d.(type) {
|
||||
case *ast.FuncDecl:
|
||||
// STATIC FUNCTION is file-local: don't expose to
|
||||
// other files via the cross-file table so the
|
||||
// analyzer warns on misuse and the runtime fails
|
||||
// with a clean "undeclared" rather than a
|
||||
// mismatched lookup that silently resolves to
|
||||
// some other file's STATIC.
|
||||
if !decl.IsStatic {
|
||||
crossFileFuncs[strings.ToUpper(decl.Name)] = true
|
||||
}
|
||||
case *ast.ClassDecl:
|
||||
crossFileFuncs[strings.ToUpper(decl.Name)] = true
|
||||
}
|
||||
|
||||
@@ -85,6 +85,7 @@ type FuncDecl struct {
|
||||
FuncPos token.Position
|
||||
Name string
|
||||
IsProc bool // PROCEDURE (no return value)
|
||||
IsStatic bool // STATIC FUNCTION/PROCEDURE — file-local visibility
|
||||
Params []*ParamDecl // declared parameters
|
||||
Decls []Decl // LOCAL, STATIC, FIELD — must come first
|
||||
Body []Stmt // executable statements — after declarations
|
||||
|
||||
@@ -52,6 +52,24 @@ type Generator struct {
|
||||
// Class name of the currently-emitting method body, used to resolve
|
||||
// ::super: at compile time against the defining class's Parent.
|
||||
curMethodClass string
|
||||
// staticFuncs holds the uppercase names of STATIC FUNCTION /
|
||||
// PROCEDURE declared in THIS file. emitPushSymbol uses it to
|
||||
// rewrite same-file references to the mangled symbol so two
|
||||
// files declaring `STATIC FUNCTION foo()` don't collide in the
|
||||
// global VM symbol table.
|
||||
staticFuncs map[string]bool
|
||||
}
|
||||
|
||||
// staticSymName returns the mangled VM symbol / Go function name used
|
||||
// for a STATIC FUNCTION declared in this file. Format keeps it within
|
||||
// what Harbour-style identifiers allow and unique across files:
|
||||
//
|
||||
// __STATIC__<fileKey>__<NAME>
|
||||
//
|
||||
// The leading underscores keep these out of the user-callable PUBLIC
|
||||
// namespace.
|
||||
func (g *Generator) staticSymName(name string) string {
|
||||
return "__STATIC__" + g.fileKey() + "__" + strings.ToUpper(name)
|
||||
}
|
||||
|
||||
type symbolEntry struct {
|
||||
@@ -85,6 +103,15 @@ func doGenerate(file *ast.File, debug, library bool) string {
|
||||
IsLibrary: library,
|
||||
}
|
||||
|
||||
// First pass: index every STATIC FUNCTION in this file so the rest
|
||||
// of the generator can rewrite local references to the mangled name.
|
||||
g.staticFuncs = map[string]bool{}
|
||||
for _, d := range file.Decls {
|
||||
if fd, ok := d.(*ast.FuncDecl); ok && fd.IsStatic {
|
||||
g.staticFuncs[strings.ToUpper(fd.Name)] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Collect symbols from declarations
|
||||
for _, d := range file.Decls {
|
||||
switch decl := d.(type) {
|
||||
@@ -93,10 +120,20 @@ func doGenerate(file *ast.File, debug, library bool) string {
|
||||
if !library && (decl.Name == "Main" || decl.Name == "MAIN") {
|
||||
scope += "|hbrt.FsFirst"
|
||||
}
|
||||
symName := strings.ToUpper(decl.Name)
|
||||
goName := "FV_" + symName
|
||||
if decl.IsStatic {
|
||||
// Mangle so two files defining the same STATIC name
|
||||
// register distinct symbols. The mangled name is also
|
||||
// what same-file callers look up via emitPushSymbol,
|
||||
// so cross-file resolution genuinely can't find it.
|
||||
symName = g.staticSymName(decl.Name)
|
||||
goName = "FV_" + symName
|
||||
}
|
||||
g.symbols = append(g.symbols, symbolEntry{
|
||||
name: strings.ToUpper(decl.Name),
|
||||
name: symName,
|
||||
scope: scope,
|
||||
fn: "FV_" + strings.ToUpper(decl.Name),
|
||||
fn: goName,
|
||||
})
|
||||
case *ast.ClassDecl:
|
||||
className := strings.ToUpper(decl.Name)
|
||||
@@ -233,9 +270,17 @@ func (g *Generator) fileKey() string {
|
||||
// emitPushSymbol writes PushSymbol using the lazy-cached package-level
|
||||
// variable. First call resolves via VM; subsequent calls skip the
|
||||
// RWMutex + map lookup.
|
||||
//
|
||||
// Same-file STATIC FUNCTION names get rewritten to their mangled
|
||||
// form so two files defining `STATIC FUNCTION foo()` each see only
|
||||
// their own version (Harbour file-local STATIC semantics).
|
||||
func (g *Generator) emitPushSymbol(name string) {
|
||||
v := g.symVar(name)
|
||||
g.writeln(fmt.Sprintf("t.PushSymbol(t.GetSym(&%s, %q))", v, name))
|
||||
target := name
|
||||
if g.staticFuncs[strings.ToUpper(name)] {
|
||||
target = g.staticSymName(name)
|
||||
}
|
||||
v := g.symVar(target)
|
||||
g.writeln(fmt.Sprintf("t.PushSymbol(t.GetSym(&%s, %q))", v, target))
|
||||
}
|
||||
|
||||
|
||||
@@ -511,7 +556,11 @@ func (g *Generator) emitTopLevelStatic(vd *ast.VarDecl) {
|
||||
}
|
||||
|
||||
func (g *Generator) emitFuncDecl(fn *ast.FuncDecl) {
|
||||
goName := "FV_" + strings.ToUpper(fn.Name)
|
||||
symName := strings.ToUpper(fn.Name)
|
||||
if fn.IsStatic {
|
||||
symName = g.staticSymName(fn.Name)
|
||||
}
|
||||
goName := "FV_" + symName
|
||||
|
||||
// Emit function-level STATIC variables as package-level Go vars.
|
||||
// Harbour: STATIC inside FUNCTION persists across calls but is
|
||||
|
||||
@@ -236,7 +236,11 @@ func (p *Parser) parseFile() *ast.File {
|
||||
// STATIC FUNCTION/PROCEDURE → function declaration with static scope
|
||||
if p.peekAt(1) == token.FUNCTION_KW || p.peekAt(1) == token.PROCEDURE {
|
||||
p.advance() // skip STATIC
|
||||
file.Decls = append(file.Decls, p.parseFuncDecl())
|
||||
fd := p.parseFuncDecl()
|
||||
if fd != nil {
|
||||
fd.IsStatic = true
|
||||
}
|
||||
file.Decls = append(file.Decls, fd)
|
||||
} else {
|
||||
// Top-level STATIC declaration (module-level variable)
|
||||
file.Decls = append(file.Decls, p.parseVarDecl())
|
||||
|
||||
Reference in New Issue
Block a user