Files
five/compiler/gengo/gen_util.go
CharlesKWON dca7bb22e5 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>
2026-05-04 07:05:22 +09:00

159 lines
4.4 KiB
Go

// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
// All rights reserved.
package gengo
import (
"five/compiler/ast"
"strings"
)
// hasXBaseCommands checks if the file contains any xBase commands.
func hasXBaseCommands(file *ast.File) bool {
for _, d := range file.Decls {
switch decl := d.(type) {
case *ast.FuncDecl:
if scanStmtsForXBase(decl.Body) {
return true
}
case *ast.MethodDecl:
if scanStmtsForXBase(decl.Body) {
return true
}
}
}
return false
}
// xbaseFuncNames is the set of RTL functions that require the hbrdd import
// in the generated Go code. When a PRG only uses these via function-call
// syntax (no xBase commands like USE), the compiler still needs hbrdd.
var xbaseFuncNames = map[string]bool{
"DBUSEAREA": true, "DBCREATE": true, "DBAPPEND": true, "DBSKIP": true,
"DBGOTO": true, "DBGOTOP": true, "DBGOBOTTOM": true, "DBSEEK": true,
"DBDELETE": true, "DBRECALL": true, "DBCLOSEAREA": true, "DBCLOSEALL": true,
"DBSELECTAREA": true, "DBCOMMIT": true, "DBPACK": true, "DBZAP": true,
"DBRLOCK": true, "DBRUNLOCK": true, "FLOCK": true, "DBUNLOCK": true,
"FIELDGET": true, "FIELDPUT": true, "FIELDPOS": true, "FIELDNAME": true,
"FCOUNT": true, "RECNO": true, "RECCOUNT": true, "EOF": true, "BOF": true,
"FOUND": true, "DELETED": true, "LASTREC": true, "ALIAS": true,
"ORDSETFOCUS": true, "ORDCOUNT": true, "ORDNAME": true, "ORDKEY": true,
"USED": true, "SELECT": true, "SETDELETED": true,
"FIVE_SQL": true, // FiveSql2 entry point also needs hbrdd
}
func scanExprForXBase(expr ast.Expr) bool {
if expr == nil {
return false
}
switch e := expr.(type) {
case *ast.CallExpr:
if ident, ok := e.Func.(*ast.IdentExpr); ok {
if xbaseFuncNames[strings.ToUpper(ident.Name)] {
return true
}
}
for _, arg := range e.Args {
if scanExprForXBase(arg) {
return true
}
}
case *ast.BinaryExpr:
return scanExprForXBase(e.Left) || scanExprForXBase(e.Right)
case *ast.AssignExpr:
return scanExprForXBase(e.Right)
case *ast.AliasExpr:
// FIELD->NAME always needs hbrdd
return true
}
return false
}
// countLocalsInStmts walks a statement list (recursively into IF /
// FOR / WHILE / DO CASE / SWITCH / SEQUENCE / WITH bodies) and totals
// every `LOCAL` declaration found. Function-entry frame allocation
// must include these so a mid-function or nested LOCAL doesn't get
// assigned a slot index past the runtime's allocated count — that
// previously surfaced as silent NIL reads / out-of-range writes
// (e.g. frb_demo Section 4 with `LOCAL ch := Channel(1)` inside an
// IF block returning NIL on receive).
func countLocalsInStmts(stmts []ast.Stmt) int {
n := 0
for _, s := range stmts {
switch v := s.(type) {
case *ast.VarDecl:
if v.Scope == ast.ScopeLocal {
n += len(v.Vars)
}
case *ast.IfStmt:
n += countLocalsInStmts(v.Body)
for _, ei := range v.ElseIfs {
n += countLocalsInStmts(ei.Body)
}
n += countLocalsInStmts(v.ElseBody)
case *ast.ForStmt:
n += countLocalsInStmts(v.Body)
case *ast.ForEachStmt:
n += countLocalsInStmts(v.Body)
case *ast.DoWhileStmt:
n += countLocalsInStmts(v.Body)
case *ast.SeqStmt:
n += countLocalsInStmts(v.Body)
n += countLocalsInStmts(v.RecoverBody)
case *ast.SwitchStmt:
for _, c := range v.Cases {
n += countLocalsInStmts(c.Body)
}
n += countLocalsInStmts(v.Otherwise)
}
}
return n
}
func scanStmtsForXBase(stmts []ast.Stmt) bool {
for _, s := range stmts {
switch v := s.(type) {
case *ast.UseCmd, *ast.GoCmd, *ast.SkipCmd, *ast.SeekCmd,
*ast.ReplaceCmd, *ast.AppendCmd, *ast.DeleteCmd,
*ast.SelectCmd, *ast.IndexCmd, *ast.SetCmd:
return true
case *ast.ExprStmt:
if scanExprForXBase(v.X) {
return true
}
case *ast.IfStmt:
if scanStmtsForXBase(v.Body) || scanStmtsForXBase(v.ElseBody) {
return true
}
for _, ei := range v.ElseIfs {
if scanStmtsForXBase(ei.Body) {
return true
}
}
case *ast.ForStmt:
if scanStmtsForXBase(v.Body) {
return true
}
case *ast.ForEachStmt:
if scanStmtsForXBase(v.Body) {
return true
}
case *ast.DoWhileStmt:
if scanStmtsForXBase(v.Body) {
return true
}
case *ast.SeqStmt:
if scanStmtsForXBase(v.Body) || scanStmtsForXBase(v.RecoverBody) {
return true
}
case *ast.SwitchStmt:
for _, c := range v.Cases {
if scanStmtsForXBase(c.Body) {
return true
}
}
}
}
return false
}