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:
2026-04-13 18:49:33 +09:00
parent 3adc9d7d59
commit 5bfdc476ef

View File

@@ -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
}
}