diff --git a/cmd/five/main.go b/cmd/five/main.go index d6e0f56..0d4b2a3 100644 --- a/cmd/five/main.go +++ b/cmd/five/main.go @@ -259,7 +259,15 @@ func buildMultiPRG(prgFiles []string, output string) { for _, d := range f.Decls { switch decl := d.(type) { case *ast.FuncDecl: - crossFileFuncs[strings.ToUpper(decl.Name)] = true + // 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 } diff --git a/compiler/ast/ast.go b/compiler/ast/ast.go index 291abe9..41821f9 100644 --- a/compiler/ast/ast.go +++ b/compiler/ast/ast.go @@ -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 diff --git a/compiler/gengo/gengo.go b/compiler/gengo/gengo.go index 201f85a..28e72e8 100644 --- a/compiler/gengo/gengo.go +++ b/compiler/gengo/gengo.go @@ -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____ +// +// 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 diff --git a/compiler/parser/parser.go b/compiler/parser/parser.go index 98a72b2..1399bfc 100644 --- a/compiler/parser/parser.go +++ b/compiler/parser/parser.go @@ -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())