When FOR body contains APPEND+REPLACE and no USE/SELECT: - Hoist WorkAreaManager, Current(), *dbf.DBFArea outside loop - Pre-compute FieldIndex for all REPLACE fields once - REPLACE inside loop uses cached _rdbf and _rfiN variables - APPEND inside loop uses cached _rarea (no WA lookup per iter) Safety: collectReplaceFields returns nil if USE/SELECT found in body (workarea may change → cannot safely cache). Falls back to normal emit. 10K APPEND benchmark: 28ms (Harbour 27ms — essentially equal!) 82/82 stress test PASS. 14 packages ALL PASS. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
368 lines
10 KiB
Go
368 lines
10 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
|
|
}
|
|
alias := s.Alias
|
|
|
|
g.writeln(fmt.Sprintf("_, _err := wa.Open(%q, _path, %q, false, false)", via, alias))
|
|
g.writeln("if _err != nil { panic(_err) }")
|
|
g.indent--
|
|
g.writeln("}")
|
|
}
|
|
|
|
func (g *Generator) emitGoCmd(s *ast.GoCmd) {
|
|
g.writeln("{")
|
|
g.indent++
|
|
g.writeln("wa := t.WA.(*hbrdd.WorkAreaManager)")
|
|
g.writeln("if _area := wa.Current(); _area != nil {")
|
|
g.indent++
|
|
|
|
switch s.Direction {
|
|
case "TOP":
|
|
g.writeln("_area.GoTop()")
|
|
case "BOTTOM":
|
|
g.writeln("_area.GoBottom()")
|
|
default:
|
|
if s.RecNo != nil {
|
|
// Optimize: literal integers skip stack ops
|
|
if lit, ok := s.RecNo.(*ast.LiteralExpr); ok && lit.Kind == token.INT {
|
|
g.writeln(fmt.Sprintf("_area.GoTo(uint32(%s))", lit.Value))
|
|
} else {
|
|
g.emitExpr(s.RecNo)
|
|
g.writeln("_area.GoTo(uint32(t.Pop2().AsNumInt()))")
|
|
}
|
|
}
|
|
}
|
|
|
|
g.indent--
|
|
g.writeln("}")
|
|
g.indent--
|
|
g.writeln("}")
|
|
}
|
|
|
|
func (g *Generator) emitSkipCmd(s *ast.SkipCmd, locals localMap) {
|
|
g.writeln("{")
|
|
g.indent++
|
|
g.writeln("wa := t.WA.(*hbrdd.WorkAreaManager)")
|
|
g.writeln("if _area := wa.Current(); _area != nil {")
|
|
g.indent++
|
|
|
|
if s.Count != nil {
|
|
// Optimize: literal skip count avoids stack ops
|
|
if lit, ok := s.Count.(*ast.LiteralExpr); ok && lit.Kind == token.INT {
|
|
g.writeln(fmt.Sprintf("_area.Skip(%s)", 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("_area.Skip(-%s)", lit2.Value))
|
|
} else {
|
|
g.emitExpr(s.Count)
|
|
g.writeln("_area.Skip(t.Pop2().AsNumInt())")
|
|
}
|
|
} else {
|
|
g.emitExpr(s.Count)
|
|
g.writeln("_area.Skip(t.Pop2().AsNumInt())")
|
|
}
|
|
} else {
|
|
g.writeln("_area.Skip(1)")
|
|
}
|
|
|
|
g.indent--
|
|
g.writeln("}")
|
|
g.indent--
|
|
g.writeln("}")
|
|
}
|
|
|
|
func (g *Generator) emitSeekCmd(s *ast.SeekCmd, locals localMap) {
|
|
g.writeln("{")
|
|
g.indent++
|
|
g.writeln("wa := t.WA.(*hbrdd.WorkAreaManager)")
|
|
g.writeln("if area := wa.Current(); area != nil {")
|
|
g.indent++
|
|
|
|
g.emitExpr(s.Key)
|
|
g.writeln("_key := t.Pop2()")
|
|
g.writeln("if _idx, ok := area.(hbrdd.Indexer); ok {")
|
|
g.indent++
|
|
// SoftSeek: from SEEK SOFT keyword OR runtime SET SOFTSEEK
|
|
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("}")
|
|
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[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[name]; found {
|
|
g.writeln(fmt.Sprintf("t.PushLocal(%d)", idx))
|
|
} 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))
|
|
}
|
|
}
|
|
|
|
// emitPopByName pops stack into a variable by name
|
|
func (g *Generator) emitPopByName(name string, locals localMap) {
|
|
if idx, found := locals[name]; found {
|
|
g.writeln(fmt.Sprintf("t.PopLocal(%d)", idx))
|
|
} 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))
|
|
}
|
|
}
|