Cumulative season's silent-bug hunting (~62 fixes) across the FiveSql2 SQL engine, the Five compiler/runtime, and the hbrdd RDD layer. Saved as a single checkpoint before refactoring the parser to delegate xBase command translation to the preprocessor. Highlights: FiveSql2 engine (_FiveSql2/src/) - prefix-glob index attach -> explicit convention (<table>_pk.ntx, <table>_uq.ntx, <table>.cdx) — fixes silent multi-row INSERT row-drop - DROP/CREATE TABLE FErase chain extended (.cdx, .fsc, .fsv, .dbt, .fpt) - COUNT(DISTINCT col) parsed + aggregated via hSeen hash - UNION column-count mismatch returns SQL_ERR_GRAMMAR (was silent) - DISTINCT + ORDER BY hidden-col leak fixed (trim before DISTINCT) - Derived table FROM (SELECT...) + JOIN right-side derived - Self-FK CASCADE depth 2+ via SqlGetSingleColPK pre-collect - LAG/LEAD default arg uses SqlEvalRowExpr (handles -N const exprs) - DATE literal round-trip validation (Feb 29 non-leap rejected) - CREATE OR REPLACE VIEW; CREATE VIEW errors on already-exists - AlterTable type dispatcher comma-wrapped (1-char type "A" no longer matches CHARACTER) Compiler / runtime - gengo: HB_ -> FV_ prefix on emitted Go function names (Five identity) - gengo split: emit_block.go, emit_stmt.go, folding.go extracted - parser/stmtreg.go nudges - hbrt: debug TUI/CLI restructure (debugcmd, debugkey, termios_*), windows debug stubs collapsed - thread/vm/value/class/pcinterp tightening from panic traces RDD layer (hbrdd/) - dbf: null bitmap support (null.go + null_test.go), mmap split (mmap_posix.go / mmap_windows.go), byte-level numeric parse - ntx/cdx: windows mmap parity - workarea + mem RDD: cross-area state-bleed fixes RTL (hbrtl/) - errorlog rewrite with platform-specific FD (errorlog_fd_unix / errorlog_fd_other) - sqlscan, sqlhelpers, indexrtl, datetime extensions Gates green at checkpoint: - go test ./... : PASS - FiveSql2 SQL:1999 : 43/43 - Harbour compat : 56/56 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
288 lines
8.9 KiB
Go
288 lines
8.9 KiB
Go
// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
|
|
// All rights reserved.
|
|
|
|
// Block, alias, and method-send emission.
|
|
//
|
|
// Groups three related emitters that all cross the "ordinary local vs
|
|
// externally-addressable" boundary:
|
|
//
|
|
// - emitAliasExpr: workarea aliasing (`ALIAS->field`, `(expr)->(...)`,
|
|
// MEMVAR->name), including the save/select/restore dance used when
|
|
// an aliased expression switches the current workarea.
|
|
// - emitSendExpr: method dispatch (`obj:method()`, `::field`,
|
|
// `::super:method()`, Go-object reflect-bridge fallback).
|
|
// - emitBlock: code blocks `{|params| body}`, including
|
|
// RefCell-based mutable capture of outer locals.
|
|
//
|
|
// collectFreeVars / walkExprIdents are the shared walker that emitBlock
|
|
// uses to decide which outer locals to capture into the block.
|
|
|
|
package gengo
|
|
|
|
import (
|
|
"five/compiler/ast"
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
func (g *Generator) emitAliasExpr(e *ast.AliasExpr) {
|
|
fieldIdent, isFieldIdent := e.Field.(*ast.IdentExpr)
|
|
|
|
// Case 1: alias->field (static alias, simple field name)
|
|
if ident, ok := e.Alias.(*ast.IdentExpr); ok && isFieldIdent {
|
|
upper := strings.ToUpper(ident.Name)
|
|
// `M->name` / `MEMVAR->name` access the memvar namespace, not
|
|
// a database workarea. Harbour reserves both aliases for this.
|
|
if upper == "M" || upper == "MEMVAR" {
|
|
g.writeln(fmt.Sprintf(`t.PushMemvar(%q)`, fieldIdent.Name))
|
|
return
|
|
}
|
|
g.writeln(fmt.Sprintf(`t.PushAliasField(%q, %q)`, ident.Name, fieldIdent.Name))
|
|
return
|
|
}
|
|
|
|
// Case 2: (expr)->field (dynamic alias, simple field name)
|
|
if isFieldIdent {
|
|
g.emitExpr(e.Alias)
|
|
g.writeln(fmt.Sprintf(`t.PushDynAliasField(t.Pop2().AsString(), %q)`, fieldIdent.Name))
|
|
return
|
|
}
|
|
|
|
// Case 3: alias->(expr) or (expr)->(expr) — workarea context expression
|
|
// Harbour: save current WA, select new WA, evaluate expr, restore WA
|
|
// Example: (nArea)->(Used()) → evaluate Used() in workarea nArea
|
|
// Example: CUSTOMERS->(RecCount()) → evaluate RecCount() in CUSTOMERS workarea
|
|
if ident, ok := e.Alias.(*ast.IdentExpr); ok {
|
|
_, isLocal := g.curLocals[strings.ToUpper(ident.Name)]
|
|
if isLocal {
|
|
// Local variable: emit value (numeric area number)
|
|
g.emitExpr(e.Alias)
|
|
g.writeln(`t.WASaveAndSelect(int(t.Pop2().AsNumInt()))`)
|
|
} else {
|
|
// Static alias name: resolve by alias string
|
|
g.writeln(fmt.Sprintf(`t.WASaveAndSelectAlias(%q)`, ident.Name))
|
|
}
|
|
} else {
|
|
// Dynamic: numeric area from expression
|
|
g.emitExpr(e.Alias)
|
|
g.writeln(`t.WASaveAndSelect(int(t.Pop2().AsNumInt()))`)
|
|
}
|
|
g.emitExpr(e.Field)
|
|
g.writeln(`t.WARestore()`)
|
|
}
|
|
|
|
func (g *Generator) fieldName(expr ast.Expr) string {
|
|
if ident, ok := expr.(*ast.IdentExpr); ok {
|
|
return ident.Name
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (g *Generator) emitSendExpr(e *ast.SendExpr) {
|
|
// ::super:Method(args) — dispatch to parent class. The parse tree
|
|
// is nested: outer SendExpr.Object is itself a SendExpr whose
|
|
// Object is ::SELF and Method is "super". Detect that shape and
|
|
// route through SendSuper, which keeps Self bound to the child
|
|
// instance but looks the method up on Parent.
|
|
if sup, ok := e.Object.(*ast.SendExpr); ok {
|
|
if _, isSelf := sup.Object.(*ast.SelfExpr); isSelf &&
|
|
strings.EqualFold(sup.Method, "super") {
|
|
for _, arg := range e.Args {
|
|
g.emitExpr(arg)
|
|
}
|
|
// Emit defining-class name so runtime walks the right Parent
|
|
// chain — Self's class alone would infinite-loop on 3+ level
|
|
// hierarchies (Grand→Child→Base). See SendSuper comment.
|
|
g.writeln(fmt.Sprintf("t.SendSuper(%q, %q, %d)",
|
|
g.curMethodClass, e.Method, len(e.Args)))
|
|
return
|
|
}
|
|
}
|
|
|
|
// Self access: ::field (no parens) → PushSelfField
|
|
// Self method: ::method() (has parens) → Send on Self
|
|
if _, isSelf := e.Object.(*ast.SelfExpr); isSelf {
|
|
if !e.HasParens && len(e.Args) == 0 {
|
|
// ::field (getter, no parentheses)
|
|
g.writeln(fmt.Sprintf("t.PushSelfField(%q)", strings.ToUpper(e.Method)))
|
|
return
|
|
}
|
|
// ::method() or ::method(args) — method call on Self
|
|
g.writeln("t.PushSelf()")
|
|
for _, arg := range e.Args {
|
|
g.emitExpr(arg)
|
|
}
|
|
g.writeln(fmt.Sprintf("t.Send(%q, %d)", e.Method, len(e.Args)))
|
|
return
|
|
}
|
|
|
|
// General: obj:method(args) or obj:field
|
|
// Check at runtime: if Go object → GoCall, else Harbour Send
|
|
g.emitExpr(e.Object)
|
|
g.writeln("{")
|
|
g.indent++
|
|
g.writeln("_obj := t.Pop2()")
|
|
|
|
// Push args and capture them
|
|
argNames := make([]string, len(e.Args))
|
|
for i, arg := range e.Args {
|
|
argNames[i] = fmt.Sprintf("_sa%d", i)
|
|
g.emitExpr(arg)
|
|
g.writeln(fmt.Sprintf("%s := t.Pop2()", argNames[i]))
|
|
}
|
|
|
|
g.writeln("if hbrt.IsGoObject(_obj) {")
|
|
g.indent++
|
|
// Go object: use reflect bridge
|
|
argsStr := ""
|
|
for i, name := range argNames {
|
|
if i > 0 {
|
|
argsStr += ", "
|
|
}
|
|
argsStr += name
|
|
}
|
|
g.writeln(fmt.Sprintf("_gr := hbrt.GoCallCached(_obj, %q, %s)", e.Method, argsStr))
|
|
g.writeln("if len(_gr) > 0 { t.PushValue(_gr[0]) } else { t.PushNil() }")
|
|
g.indent--
|
|
g.writeln("} else {")
|
|
g.indent++
|
|
// Harbour object: use Send
|
|
g.writeln("t.PushValue(_obj)")
|
|
for _, name := range argNames {
|
|
g.writeln(fmt.Sprintf("t.PushValue(%s)", name))
|
|
}
|
|
g.writeln(fmt.Sprintf("t.Send(%q, %d)", e.Method, len(e.Args)))
|
|
g.indent--
|
|
g.writeln("}")
|
|
g.indent--
|
|
g.writeln("}")
|
|
}
|
|
|
|
func (g *Generator) emitBlock(e *ast.BlockExpr) {
|
|
// Code block: {|params| body}
|
|
// Block params are passed via Frame() from Eval/AEval.
|
|
nParams := len(e.Params)
|
|
|
|
// Collect free variables in the block body that reference outer locals.
|
|
// These need to be captured via Go closure variables.
|
|
outerLocals := g.curLocals
|
|
blockLocals := make(localMap)
|
|
for i, p := range e.Params {
|
|
blockLocals[strings.ToUpper(p)] = i + 1
|
|
}
|
|
|
|
// Find all idents in block body that are in outerLocals but NOT in blockLocals
|
|
freeVars := g.collectFreeVars(e.Body, blockLocals, outerLocals)
|
|
|
|
// Harbour: closures share outer locals via RefCell (mutable capture).
|
|
// Convert each captured outer local to a RefCell, then pass the RefCell
|
|
// into the block. Both outer function and block read/write through it.
|
|
for _, fv := range freeVars {
|
|
outerIdx := outerLocals[fv]
|
|
// Ensure outer local is a RefCell (PushLocalRef creates one if needed,
|
|
// but we do it inline to avoid stack ops).
|
|
g.writeln(fmt.Sprintf("t.EnsureLocalRef(%d) // share %s via RefCell", outerIdx, fv))
|
|
}
|
|
|
|
// Capture the RefCell values with unique names to avoid Go scope issues.
|
|
capSeq := g.blockSeq
|
|
g.blockSeq++
|
|
capNames := make(map[string]string) // fv → Go var name
|
|
for _, fv := range freeVars {
|
|
outerIdx := outerLocals[fv]
|
|
capName := fmt.Sprintf("_cap_%s_%d", fv, capSeq)
|
|
g.writeln(fmt.Sprintf("%s := t.LocalRaw(%d) // capture RefCell %s", capName, outerIdx, fv))
|
|
capNames[fv] = capName
|
|
}
|
|
|
|
g.writeln(fmt.Sprintf("t.PushBlock(func(t *hbrt.Thread) {"))
|
|
g.indent++
|
|
nLocals := len(freeVars)
|
|
g.writeln(fmt.Sprintf("t.Frame(%d, %d)", nParams, nLocals))
|
|
g.writeln("defer t.EndProc()")
|
|
|
|
// Inject RefCell values directly into block locals — reads/writes go through RefCell
|
|
for i, fv := range freeVars {
|
|
localIdx := nParams + i + 1
|
|
blockLocals[fv] = localIdx
|
|
g.writeln(fmt.Sprintf("t.SetLocalRaw(%d, %s) // inject shared RefCell %s", localIdx, capNames[fv], fv))
|
|
}
|
|
|
|
g.curLocals = blockLocals
|
|
g.emitExpr(e.Body)
|
|
g.writeln("t.RetValue()")
|
|
|
|
g.curLocals = outerLocals
|
|
g.indent--
|
|
g.writeln(fmt.Sprintf("}, %d)", 0))
|
|
}
|
|
|
|
// collectFreeVars finds identifier names in expr that exist in outerLocals but not blockLocals.
|
|
func (g *Generator) collectFreeVars(expr ast.Expr, blockLocals, outerLocals localMap) []string {
|
|
var result []string
|
|
seen := map[string]bool{}
|
|
g.walkExprIdents(expr, func(name string) {
|
|
upper := strings.ToUpper(name)
|
|
if seen[upper] {
|
|
return
|
|
}
|
|
if _, inBlock := blockLocals[upper]; inBlock {
|
|
return
|
|
}
|
|
if _, inOuter := outerLocals[upper]; inOuter {
|
|
seen[upper] = true
|
|
result = append(result, upper)
|
|
}
|
|
})
|
|
return result
|
|
}
|
|
|
|
// walkExprIdents calls fn for each IdentExpr in the expression tree.
|
|
func (g *Generator) walkExprIdents(expr ast.Expr, fn func(string)) {
|
|
if expr == nil {
|
|
return
|
|
}
|
|
switch e := expr.(type) {
|
|
case *ast.IdentExpr:
|
|
fn(e.Name)
|
|
case *ast.BinaryExpr:
|
|
g.walkExprIdents(e.Left, fn)
|
|
g.walkExprIdents(e.Right, fn)
|
|
case *ast.UnaryExpr:
|
|
g.walkExprIdents(e.X, fn)
|
|
case *ast.PostfixExpr:
|
|
g.walkExprIdents(e.X, fn)
|
|
case *ast.CallExpr:
|
|
g.walkExprIdents(e.Func, fn)
|
|
for _, a := range e.Args {
|
|
g.walkExprIdents(a, fn)
|
|
}
|
|
case *ast.IndexExpr:
|
|
g.walkExprIdents(e.X, fn)
|
|
g.walkExprIdents(e.Index, fn)
|
|
case *ast.DotExpr:
|
|
g.walkExprIdents(e.X, fn)
|
|
case *ast.AssignExpr:
|
|
g.walkExprIdents(e.Left, fn)
|
|
g.walkExprIdents(e.Right, fn)
|
|
case *ast.ArrayLitExpr:
|
|
for _, item := range e.Items {
|
|
g.walkExprIdents(item, fn)
|
|
}
|
|
case *ast.IIfExpr:
|
|
g.walkExprIdents(e.Cond, fn)
|
|
g.walkExprIdents(e.True, fn)
|
|
g.walkExprIdents(e.False, fn)
|
|
case *ast.SendExpr:
|
|
g.walkExprIdents(e.Object, fn)
|
|
for _, a := range e.Args {
|
|
g.walkExprIdents(a, fn)
|
|
}
|
|
case *ast.AliasExpr:
|
|
g.walkExprIdents(e.Alias, fn)
|
|
g.walkExprIdents(e.Field, fn)
|
|
case *ast.BlockExpr:
|
|
g.walkExprIdents(e.Body, fn)
|
|
}
|
|
}
|