From 5bfdc476ef3f5bf8ee2b83c93435b41593812dde Mon Sep 17 00:00:00 2001 From: CharlesKWON Date: Mon, 13 Apr 2026 18:49:33 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20STATIC=20inside=20FUNCTION=20=E2=80=94?= =?UTF-8?q?=20persistent=20variables=20now=20work?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Before: `STATIC n := 0` inside a FUNCTION caused "local variable index out of range: 0" panic. The gengo code generator only handled module-level STATIC (file scope) but silently ignored function-level STATIC declarations. After: Function-level STATIC variables are emitted as Go package-level vars with function-name prefixed names (e.g., `static_COUNTER_N`), registered in staticVars map during function emission, and cleaned up after the function to prevent name collisions. Also fixes compound assignment (+=, -=, *=, /=) on STATIC variables, which previously only handled simple assignment (:=). FUNCTION Counter() STATIC n := 0 // persists across calls n++ // n++ already worked (postfix handler) n += 10 // was broken, now works RETURN n Verified: Counter() → 1, 2, 3 (n++) CountA() → 10, 20, 30 (n += 10, separate scope) CountB() → 101, 102, 103 (n += 1, init 100, separate scope) go test ./... 14 packages OK FiveSql2 43/43 100% compat_harbour 51/51 Co-Authored-By: Claude Opus 4.6 (1M context) --- compiler/gengo/gengo.go | 89 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 86 insertions(+), 3 deletions(-) diff --git a/compiler/gengo/gengo.go b/compiler/gengo/gengo.go index 1957ff1..4cac17a 100644 --- a/compiler/gengo/gengo.go +++ b/compiler/gengo/gengo.go @@ -345,6 +345,44 @@ func (g *Generator) emitTopLevelStatic(vd *ast.VarDecl) { func (g *Generator) emitFuncDecl(fn *ast.FuncDecl) { goName := "HB_" + strings.ToUpper(fn.Name) + + // Emit function-level STATIC variables as package-level Go vars. + // Harbour: STATIC inside FUNCTION persists across calls but is + // scoped to the function. We prefix with funcname to avoid clashes. + for _, d := range fn.Decls { + if vd, ok := d.(*ast.VarDecl); ok && vd.Scope == ast.ScopeStatic { + for _, v := range vd.Vars { + varName := "static_" + strings.ToUpper(fn.Name) + "_" + strings.ToUpper(v.Name) + initVal := "hbrt.MakeNil()" + if v.Init != nil { + initVal = g.exprToGoLiteral(v.Init) + } + g.writeln(fmt.Sprintf("var %s = %s", varName, initVal)) + if g.staticVars == nil { + g.staticVars = make(map[string]string) + } + g.staticVars[strings.ToUpper(v.Name)] = varName + } + } + } + // Also scan body for mid-function STATIC declarations + for _, s := range fn.Body { + if vd, ok := s.(*ast.VarDecl); ok && vd.Scope == ast.ScopeStatic { + for _, v := range vd.Vars { + varName := "static_" + strings.ToUpper(fn.Name) + "_" + strings.ToUpper(v.Name) + initVal := "hbrt.MakeNil()" + if v.Init != nil { + initVal = g.exprToGoLiteral(v.Init) + } + g.writeln(fmt.Sprintf("var %s = %s", varName, initVal)) + if g.staticVars == nil { + g.staticVars = make(map[string]string) + } + g.staticVars[strings.ToUpper(v.Name)] = varName + } + } + } + g.writeln(fmt.Sprintf("func %s(t *hbrt.Thread) {", goName)) g.indent++ @@ -393,6 +431,23 @@ func (g *Generator) emitFuncDecl(fn *ast.FuncDecl) { g.indent-- g.writeln("}") g.writeln("") + + // Remove function-scoped STATIC vars from the map so they don't + // leak into other functions that happen to share a variable name. + for _, d := range fn.Decls { + if vd, ok := d.(*ast.VarDecl); ok && vd.Scope == ast.ScopeStatic { + for _, v := range vd.Vars { + delete(g.staticVars, strings.ToUpper(v.Name)) + } + } + } + for _, s := range fn.Body { + if vd, ok := s.(*ast.VarDecl); ok && vd.Scope == ast.ScopeStatic { + for _, v := range vd.Vars { + delete(g.staticVars, strings.ToUpper(v.Name)) + } + } + } } type localMap map[string]int @@ -893,11 +948,39 @@ func (g *Generator) emitAssign(a *ast.AssignExpr, locals localMap) { } return } - // Check module-level STATIC variable + // Check module-level or function-level STATIC variable upper := strings.ToUpper(ident.Name) if goVar, found := g.staticVars[upper]; found { - g.emitExpr(a.Right) - g.writeln(fmt.Sprintf("%s = t.Pop2()", goVar)) + switch a.Op { + case token.ASSIGN: + g.emitExpr(a.Right) + g.writeln(fmt.Sprintf("%s = t.Pop2()", goVar)) + case token.PLUSEQ: + g.writeln(fmt.Sprintf("t.PushValue(%s)", goVar)) + g.emitExpr(a.Right) + g.writeln("t.Plus()") + g.writeln(fmt.Sprintf("%s = t.Pop2()", goVar)) + case token.MINUSEQ: + g.writeln(fmt.Sprintf("t.PushValue(%s)", goVar)) + g.emitExpr(a.Right) + g.writeln("t.Minus()") + g.writeln(fmt.Sprintf("%s = t.Pop2()", goVar)) + case token.STAREQ: + g.writeln(fmt.Sprintf("t.PushValue(%s)", goVar)) + g.emitExpr(a.Right) + g.writeln("t.Mult()") + g.writeln(fmt.Sprintf("%s = t.Pop2()", goVar)) + case token.SLASHEQ: + g.writeln(fmt.Sprintf("t.PushValue(%s)", goVar)) + g.emitExpr(a.Right) + g.writeln("t.Divide()") + g.writeln(fmt.Sprintf("%s = t.Pop2()", goVar)) + default: + g.writeln(fmt.Sprintf("t.PushValue(%s)", goVar)) + g.emitExpr(a.Right) + g.emitBinaryOp(a.Op) + g.writeln(fmt.Sprintf("%s = t.Pop2()", goVar)) + } return } }