fix(gengo): count nested LOCALs into the function frame

Function-entry Frame() allocation counted only top-level LOCAL
declarations from fn.Body. Mid-function LOCALs hidden inside an
IF / FOR / WHILE / DO CASE / SWITCH / SEQUENCE block weren't
included, so the runtime allocated a frame too small to hold them.
Subsequent reads/writes via PopLocalFast / PushLocalFast / LocalAdd
to those slot indices then either silently scribbled past the frame
(read-back saw NIL) or panicked with "local variable index out of
range" once the index exceeded the underlying slice.

This is the underlying bug behind frb_demo Section 4 — the
`LOCAL ch := Channel(1)` declared inside `IF pAsync != NIL` got
slot N+1 from the codegen but the runtime only allocated N. The
Channel value was scribbled past the frame, ChReceive then read
NIL from a non-existent slot, and the goroutine's ChSend(49) had
nowhere to land.

New helper gen_util.go::countLocalsInStmts walks every nested body
(IF + ElseIfs + ElseBody, ForStmt, ForEachStmt, DoWhileStmt,
SeqStmt's Body + RecoverBody, SwitchStmt's Cases + Otherwise) and
totals every ScopeLocal VarDecl. The function-emit caller adds this
to the top-level count before sizing the Frame.

Test fixture (tests/frb/test_frb_goroutine.prg) reproduces the
demo Section 4 shape — `LOCAL ch := Channel(1)` inside IF, then
`Go("WORKER", ch, 7)`, then ChReceive(ch). Wired into the FRB
runner so it stands at 6/6.

Other gates green:
  go test ./...      : PASS
  FiveSql2 SQL:1999  : 43/43
  Harbour compat     : 56/56
  std.ch suite       : 15/15
  FRB suite          : 6/6

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-04 07:05:22 +09:00
parent 6a30c4e50e
commit dca7bb22e5
4 changed files with 66 additions and 7 deletions

View File

@@ -9,6 +9,8 @@
# test_frb_compile — FrbCompile / FrbExec — in-memory compile.
# test_frb_loop — FOR loop accumulators (`+=` and `:=`).
# test_frb_step — FOR ... STEP -1 / STEP 2 in pcode mode.
# test_frb_goroutine — Go("WORKER", ...) launches dynamic FRB
# function in a goroutine + Channel rendezvous.
#
# Builds frb_simple.frb (and mathlib_pc.frb if needed) into the
# scratch dir before running the loaders.
@@ -34,6 +36,7 @@ TESTS=(
test_frb_compile
test_frb_loop
test_frb_step
test_frb_goroutine
)
pass=0

View File

@@ -0,0 +1,14 @@
FUNCTION Main()
LOCAL cAsync := ;
'FUNCTION Worker(ch, n)' + Chr(10) + ;
' ChSend(ch, n * n)' + Chr(10) + ;
' RETURN NIL' + Chr(10)
LOCAL pAsync := FrbCompile(cAsync)
IF pAsync != NIL
LOCAL ch := Channel(1)
Go("WORKER", ch, 7)
? " 7^2 from dynamic goroutine =", ChReceive(ch)
FrbUnload(pAsync)
ENDIF
RETURN NIL