fix(gengo): unresolved identifiers fall back to PushMemvar, not PushLocal(0)
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user