From 08ad6f4761a84043f2fb57071e3fddad08af6edf Mon Sep 17 00:00:00 2001 From: CharlesKWON Date: Tue, 14 Apr 2026 09:20:26 +0900 Subject: [PATCH] fix(gengo): unresolved identifiers fall back to PushMemvar, not PushLocal(0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three emitIdent / emitIdentByName / emitPopByName call sites used `t.PushLocal(0)` as the fallback for compile-time-unresolved names (missing #include constants, undeclared globals, typos). PushLocal(0) crashes at runtime the moment that code path executes with "local variable index out of range: 0" — even when the identifier is dead code or behind a condition that's rarely true. Concrete bugs this hid: - TSqlIndex:FindExclusive referenced DBI_FULLPATH / DBI_SHARED from a non-existent dbinfo.ch include. The 43-test harness only reached FindExclusive with no Used workareas, so the reference was never evaluated. Any standalone PRG that called five_SQL after dbUseArea would trip it. - Prior session's BindColumns/ResolveCache experiment hit the same class of crash in the CLASS Send path — diagnosed as "Unresolved → PushLocal(0)" at the time but root cause deferred. Fix: use `t.PushMemvar(name)` / `t.PopMemvar(name)` instead. Matches Harbour semantics (undefined identifiers try PRIVATE/PUBLIC memvar tables at runtime, missing → NIL, assignment auto-creates PRIVATE). Harbour is forgiving about unresolved names; Five now is too. This doesn't silence the signal: the emitted comment still flags the reference as unresolved for grep-ability in generated Go. Validation: - FiveSql2 43/43 - Harbour compat 51/51 - go test ./... ALL PASS Co-Authored-By: Claude Opus 4.6 (1M context) --- compiler/gengo/gen_cmd.go | 6 ++++-- compiler/gengo/gengo.go | 9 +++++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/compiler/gengo/gen_cmd.go b/compiler/gengo/gen_cmd.go index 4a03156..fad6352 100644 --- a/compiler/gengo/gen_cmd.go +++ b/compiler/gengo/gen_cmd.go @@ -398,7 +398,8 @@ func (g *Generator) emitIdentByName(name string, locals localMap) { } else if goVar, found := g.staticVars[strings.ToUpper(name)]; found { g.writeln(fmt.Sprintf("t.PushValue(%s)", goVar)) } else { - g.writeln(fmt.Sprintf("t.PushLocal(0) // UNRESOLVED: %q", name)) + // Unresolved → runtime memvar lookup (returns NIL if missing). + g.writeln(fmt.Sprintf("t.PushMemvar(%q) // unresolved", name)) } } @@ -409,6 +410,7 @@ func (g *Generator) emitPopByName(name string, locals localMap) { } else if goVar, found := g.staticVars[strings.ToUpper(name)]; found { g.writeln(fmt.Sprintf("%s = t.Pop2()", goVar)) } else { - g.writeln(fmt.Sprintf("t.Pop() // cannot assign to UNRESOLVED: %q", name)) + // Unresolved → runtime memvar store (auto-creates PRIVATE). + g.writeln(fmt.Sprintf("t.PopMemvar(%q) // unresolved", name)) } } diff --git a/compiler/gengo/gengo.go b/compiler/gengo/gengo.go index d3997f7..44cb1e7 100644 --- a/compiler/gengo/gengo.go +++ b/compiler/gengo/gengo.go @@ -1882,8 +1882,13 @@ func (g *Generator) emitIdent(e *ast.IdentExpr) { // Module-level STATIC variable g.writeln(fmt.Sprintf("t.PushValue(%s)", goVar)) } else { - // Not a local — could be unresolved global variable or function ref - g.writeln(fmt.Sprintf("t.PushLocal(0) // UNRESOLVED: %q", e.Name)) + // Unresolved at compile time — fall back to runtime memvar lookup. + // Harbour semantics: undefined identifiers try PRIVATE/PUBLIC memvar + // tables at runtime; missing → NIL. Prior behavior was PushLocal(0) + // which crashed any code path that actually reached the reference + // (e.g. missing #include, typo'd constant). PushMemvar returns NIL + // for missing names, matching Harbour's forgiving semantics. + g.writeln(fmt.Sprintf("t.PushMemvar(%q) // unresolved: fall through to memvar", e.Name)) } }