fix: STATIC inside FUNCTION — persistent variables now work
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user