Files
five/compiler/gengo/gen_cmd.go
CharlesKWON 08ad6f4761 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>
2026-04-14 09:20:26 +09:00

417 lines
12 KiB
Go

// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
// All rights reserved.
// xBase command code generation for Five.
// Generates Go code that calls hbrdd WorkAreaManager methods.
package gengo
import (
"five/compiler/ast"
"five/compiler/token"
"fmt"
"strings"
)
func (g *Generator) emitUseCmd(s *ast.UseCmd, locals localMap) {
if s.File == nil {
// USE without args = close current
g.writeln("{")
g.indent++
g.writeln("wa := t.WA.(*hbrdd.WorkAreaManager)")
g.writeln("wa.Close()")
g.indent--
g.writeln("}")
return
}
g.writeln("{")
g.indent++
g.writeln("wa := t.WA.(*hbrdd.WorkAreaManager)")
g.emitExpr(s.File)
g.writeln("_path := t.Pop2().AsString()")
via := "DBFNTX" // default
if s.Via != "" {
via = s.Via
}
shared := "false"
if s.Shared {
shared = "true"
}
readOnly := "false"
if s.ReadOnly {
readOnly = "true"
}
if s.AliasExpr != nil {
// Dynamic alias: ALIAS (expr)
g.emitExpr(s.AliasExpr)
g.writeln("_alias := t.Pop2().AsString()")
g.writeln(fmt.Sprintf("_, _err := wa.Open(%q, _path, _alias, %s, %s)", via, shared, readOnly))
} else {
g.writeln(fmt.Sprintf("_, _err := wa.Open(%q, _path, %q, %s, %s)", via, s.Alias, shared, readOnly))
}
g.writeln("if _err != nil { panic(&hbrt.HbError{Description: _err.Error(), Operation: \"USE\", SubSystem: \"BASE\"}) }")
g.indent--
g.writeln("}")
}
func (g *Generator) emitGoCmd(s *ast.GoCmd) {
// Use hoisted area if available
areaVar := "_area"
if g.hoistedDW || g.hoistedFields != nil {
areaVar = g.hoistedAreaVar()
g.writeln(fmt.Sprintf("if %s != nil {", areaVar))
g.indent++
} else {
g.writeln("{")
g.indent++
g.writeln("wa := t.WA.(*hbrdd.WorkAreaManager)")
g.writeln(fmt.Sprintf("if %s := wa.Current(); %s != nil {", areaVar, areaVar))
g.indent++
}
switch s.Direction {
case "TOP":
g.writeln(fmt.Sprintf("%s.GoTop()", areaVar))
case "BOTTOM":
g.writeln(fmt.Sprintf("%s.GoBottom()", areaVar))
default:
if s.RecNo != nil {
if lit, ok := s.RecNo.(*ast.LiteralExpr); ok && lit.Kind == token.INT {
g.writeln(fmt.Sprintf("%s.GoTo(uint32(%s))", areaVar, lit.Value))
} else {
g.emitExpr(s.RecNo)
g.writeln(fmt.Sprintf("%s.GoTo(uint32(t.Pop2().AsNumInt()))", areaVar))
}
}
}
g.indent--
g.writeln("}")
if !g.hoistedDW && g.hoistedFields == nil {
g.indent--
g.writeln("}")
}
}
func (g *Generator) emitSkipCmd(s *ast.SkipCmd, locals localMap) {
areaVar := "_area"
if g.hoistedDW || g.hoistedFields != nil {
areaVar = g.hoistedAreaVar()
g.writeln(fmt.Sprintf("if %s != nil {", areaVar))
g.indent++
} else {
g.writeln("{")
g.indent++
g.writeln("wa := t.WA.(*hbrdd.WorkAreaManager)")
g.writeln(fmt.Sprintf("if %s := wa.Current(); %s != nil {", areaVar, areaVar))
g.indent++
}
if s.Count != nil {
if lit, ok := s.Count.(*ast.LiteralExpr); ok && lit.Kind == token.INT {
g.writeln(fmt.Sprintf("%s.Skip(%s)", areaVar, lit.Value))
} else if unary, ok := s.Count.(*ast.UnaryExpr); ok {
if lit2, ok2 := unary.X.(*ast.LiteralExpr); ok2 && lit2.Kind == token.INT {
g.writeln(fmt.Sprintf("%s.Skip(-%s)", areaVar, lit2.Value))
} else {
g.emitExpr(s.Count)
g.writeln(fmt.Sprintf("%s.Skip(t.Pop2().AsNumInt())", areaVar))
}
} else {
g.emitExpr(s.Count)
g.writeln(fmt.Sprintf("%s.Skip(t.Pop2().AsNumInt())", areaVar))
}
} else {
g.writeln(fmt.Sprintf("%s.Skip(1)", areaVar))
}
g.indent--
g.writeln("}")
if !g.hoistedDW && g.hoistedFields == nil {
g.indent--
g.writeln("}")
}
}
// hoistedAreaVar returns the area variable name for the current hoisting context.
func (g *Generator) hoistedAreaVar() string {
if g.hoistedFields != nil {
return "_rarea"
}
return "_darea"
}
func (g *Generator) emitSeekCmd(s *ast.SeekCmd, locals localMap) {
areaVar := "area"
if g.hoistedDW || g.hoistedFields != nil {
areaVar = g.hoistedAreaVar()
g.writeln(fmt.Sprintf("if %s != nil {", areaVar))
g.indent++
} else {
g.writeln("{")
g.indent++
g.writeln("wa := t.WA.(*hbrdd.WorkAreaManager)")
g.writeln(fmt.Sprintf("if %s := wa.Current(); %s != nil {", areaVar, areaVar))
g.indent++
}
g.emitExpr(s.Key)
g.writeln("_key := t.Pop2()")
g.writeln(fmt.Sprintf("if _idx, ok := %s.(hbrdd.Indexer); ok {", areaVar))
g.indent++
if s.SoftSeek {
g.writeln("_found, _ := _idx.Seek(_key, true, false)")
} else {
g.writeln("_found, _ := _idx.Seek(_key, hbrtl.GetSetSoftSeek(), false)")
}
g.writeln("_ = _found")
g.indent--
g.writeln("}")
g.indent--
g.writeln("}")
if !g.hoistedDW && g.hoistedFields == nil {
g.indent--
g.writeln("}")
}
}
func (g *Generator) emitReplaceCmd(s *ast.ReplaceCmd, locals localMap) {
// Check if we're inside a hoisted FOR loop
if g.hoistedFields != nil {
g.emitReplaceCmdHoisted(s, locals)
return
}
g.writeln("{")
g.indent++
g.writeln("wa := t.WA.(*hbrdd.WorkAreaManager)")
g.writeln("if _area := wa.Current(); _area != nil {")
g.indent++
g.writeln("_dbf, _ := _area.(*dbf.DBFArea)")
g.writeln("if _dbf != nil {")
g.indent++
for _, rf := range s.Fields {
if ident, ok := rf.Field.(*ast.IdentExpr); ok {
g.emitExpr(rf.Value)
g.writeln(fmt.Sprintf("_dbf.PutValue(_dbf.FieldIndex(%q), t.Pop2())", ident.Name))
}
}
g.indent--
g.writeln("}")
g.indent--
g.writeln("}")
g.indent--
g.writeln("}")
}
// emitReplaceCmdHoisted emits REPLACE using pre-hoisted _rdbf and _rfiN variables.
func (g *Generator) emitReplaceCmdHoisted(s *ast.ReplaceCmd, locals localMap) {
g.writeln("if _rdbf != nil {")
g.indent++
for _, rf := range s.Fields {
if ident, ok := rf.Field.(*ast.IdentExpr); ok {
// Find cached field index variable
fiVar := ""
for i, fname := range g.hoistedFields {
if strings.EqualFold(fname, ident.Name) {
fiVar = fmt.Sprintf("_rfi%d", i)
break
}
}
if fiVar != "" {
g.emitExpr(rf.Value)
g.writeln(fmt.Sprintf("_rdbf.PutValue(%s, t.Pop2())", fiVar))
} else {
// Field not in hoisted set — fallback
g.emitExpr(rf.Value)
g.writeln(fmt.Sprintf("_rdbf.PutValue(_rdbf.FieldIndex(%q), t.Pop2())", ident.Name))
}
}
}
g.indent--
g.writeln("}")
}
// --- @ SAY / GET / READ commands ---
func (g *Generator) emitAtSayCmd(s *ast.AtSayCmd) {
// DevPos(row, col)
g.writeln(fmt.Sprintf("t.PushSymbol(t.VM().FindSymbol(\"DEVPOS\"))"))
g.writeln("t.PushNil()")
g.emitExpr(s.Row)
g.emitExpr(s.Col)
g.writeln("t.Do(2)")
if s.Picture != nil {
// DevOutPict(expr, pic)
g.writeln(fmt.Sprintf("t.PushSymbol(t.VM().FindSymbol(\"DEVOUTPICT\"))"))
g.writeln("t.PushNil()")
g.emitExpr(s.SayExpr)
g.emitExpr(s.Picture)
g.writeln("t.Do(2)")
} else {
// DevOut(expr)
g.writeln(fmt.Sprintf("t.PushSymbol(t.VM().FindSymbol(\"DEVOUT\"))"))
g.writeln("t.PushNil()")
g.emitExpr(s.SayExpr)
g.writeln("t.Do(1)")
}
}
func (g *Generator) emitAtGetCmd(s *ast.AtGetCmd, locals localMap) {
// AAdd(GetList, GetNew(row, col, {|_1| IIF(_1==NIL, var, var:=_1)}, "varname" [, pic] [, {valid}] [, {when}]))
g.writeln(fmt.Sprintf("t.PushSymbol(t.VM().FindSymbol(\"AADD\"))"))
g.writeln("t.PushNil()")
// Push GetList variable
g.emitIdentByName("GetList", locals)
// GetNew(row, col, block, name, ...)
g.writeln(fmt.Sprintf("t.PushSymbol(t.VM().FindSymbol(\"GETNEW\"))"))
g.writeln("t.PushNil()")
g.emitExpr(s.Row)
g.emitExpr(s.Col)
// GET/SET block: {|_1| IIF(_1 == NIL, var, var := _1)}
g.emitGetSetBlock(s.Var, s.VarName, locals)
// Variable name as string
g.writeln(fmt.Sprintf("t.PushString(%q)", s.VarName))
nArgs := 4
if s.Picture != nil {
g.emitExpr(s.Picture)
nArgs++
}
if s.Valid != nil {
if s.Picture == nil {
g.writeln("t.PushNil()") // placeholder for pic
nArgs++
}
g.emitExpr(s.Valid)
nArgs++
}
if s.When != nil {
if s.Picture == nil && s.Valid == nil {
g.writeln("t.PushNil()") // placeholder for pic
g.writeln("t.PushNil()") // placeholder for valid
nArgs += 2
} else if s.Valid == nil {
g.writeln("t.PushNil()") // placeholder for valid
nArgs++
}
g.emitExpr(s.When)
nArgs++
}
g.writeln(fmt.Sprintf("t.Function(%d)", nArgs))
// AAdd(GetList, getObj) — 2 args
g.writeln("t.Do(2)")
// ATail(GetList):Display()
g.writeln(fmt.Sprintf("t.PushSymbol(t.VM().FindSymbol(\"ATAIL\"))"))
g.writeln("t.PushNil()")
g.emitIdentByName("GetList", locals)
g.writeln("t.Function(1)")
g.writeln(fmt.Sprintf("t.Send(\"DISPLAY\", 0)"))
g.writeln("t.Pop() // discard Display result")
}
func (g *Generator) emitAtSayGetCmd(s *ast.AtSayGetCmd, locals localMap) {
// First: @ row, col SAY expr
g.emitAtSayCmd(&ast.AtSayCmd{AtPos: s.AtPos, Row: s.Row, Col: s.Col, SayExpr: s.SayExpr})
// Then: @ Row(), Col()+1 GET var ...
g.emitAtGetCmd(&ast.AtGetCmd{
AtPos: s.AtPos,
Row: &ast.CallExpr{Func: &ast.IdentExpr{Name: "Row"}, Args: nil},
Col: &ast.BinaryExpr{Left: &ast.CallExpr{Func: &ast.IdentExpr{Name: "Col"}, Args: nil}, Op: token.PLUS, Right: &ast.LiteralExpr{Kind: token.INT, Value: "1"}}, // Col()+1
Var: s.Var,
VarName: s.VarName,
Picture: s.Picture,
Valid: s.Valid,
When: s.When,
}, locals)
}
func (g *Generator) emitReadCmd(s *ast.ReadCmd, locals localMap) {
// ReadModal(GetList)
g.writeln(fmt.Sprintf("t.PushSymbol(t.VM().FindSymbol(\"READMODAL\"))"))
g.writeln("t.PushNil()")
g.emitIdentByName("GetList", locals)
g.writeln("t.Do(1)")
if !s.Save {
// GetList := {}
g.writeln("t.PushValue(hbrt.MakeArray(0))")
g.emitPopByName("GetList", locals)
}
}
// emitGetSetBlock generates a {|_1| IIF(_1 == NIL, var, var := _1)} code block.
// Uses captured frame base + local index to access the outer variable correctly
// even when the block is called from a different call depth (e.g., Eval inside GetNew).
func (g *Generator) emitGetSetBlock(varExpr ast.Expr, varName string, locals localMap) {
if idx, found := locals[strings.ToUpper(varName)]; found {
// Capture the frame's localBase and index at block creation time
g.writeln(fmt.Sprintf("{ // GET/SET block for %s", varName))
g.indent++
g.writeln(fmt.Sprintf("_getIdx := %d", idx))
g.writeln("_getFrame := t.CurFrame()")
g.writeln("_getLocals := t.LocalsSlice()")
g.writeln("t.PushBlock(func(t2 *hbrt.Thread) {")
g.indent++
g.writeln("t2.Frame(1, 0)")
g.writeln("defer t2.EndProc()")
g.writeln("if t2.Local(1).IsNil() {")
g.indent++
g.writeln("t2.PushValue(_getFrame.GetLocal(_getIdx, _getLocals))")
g.writeln("t2.RetValue()")
g.indent--
g.writeln("} else {")
g.indent++
g.writeln("_getFrame.SetLocal(_getIdx, t2.Local(1), _getLocals)")
g.writeln("t2.PushValue(t2.Local(1))")
g.writeln("t2.RetValue()")
g.indent--
g.writeln("}")
g.indent--
g.writeln("}, 0)")
g.indent--
g.writeln("}")
} else {
// Fallback: push NIL block
g.writeln("t.PushNil() // GET block for unresolved var")
}
}
// emitIdentByName pushes a variable by name onto the stack
func (g *Generator) emitIdentByName(name string, locals localMap) {
if idx, found := locals[strings.ToUpper(name)]; found {
g.writeln(fmt.Sprintf("t.PushLocalFast(%d)", idx))
} else if goVar, found := g.staticVars[strings.ToUpper(name)]; found {
g.writeln(fmt.Sprintf("t.PushValue(%s)", goVar))
} else {
// Unresolved → runtime memvar lookup (returns NIL if missing).
g.writeln(fmt.Sprintf("t.PushMemvar(%q) // unresolved", name))
}
}
// emitPopByName pops stack into a variable by name
func (g *Generator) emitPopByName(name string, locals localMap) {
if idx, found := locals[strings.ToUpper(name)]; found {
g.writeln(fmt.Sprintf("t.PopLocalFast(%d)", idx))
} else if goVar, found := g.staticVars[strings.ToUpper(name)]; found {
g.writeln(fmt.Sprintf("%s = t.Pop2()", goVar))
} else {
// Unresolved → runtime memvar store (auto-creates PRIVATE).
g.writeln(fmt.Sprintf("t.PopMemvar(%q) // unresolved", name))
}
}