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:
2026-04-14 09:20:26 +09:00
parent 8aaed994f4
commit 08ad6f4761
2 changed files with 11 additions and 4 deletions

View File

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

View File

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