Six audit-driven blockers landed together because they're tangled:
* MENU TO removed from std.ch — the rule expanded to a call to a
nonexistent __MenuTo() RTL symbol, so any user code with `MENU
TO choice` compiled clean and panicked at runtime. Behavior
pre-this-round was a parser silent no-op, which is at least
consistent. Restore that until @ PROMPT (the companion command)
actually lands.
* COUNT now requires `TO <var>`. The earlier `[TO <v>]` optional
bracket was a Harbour-pattern transcription error: the result
template references `<v>` unconditionally, so a bare `COUNT`
expanded to ungrammatical ` := 0 ; dbEval(...)` and the
PRG parser rejected it. Match Harbour's std.ch which makes TO
mandatory.
* UPDATE FROM ... REPLACE now requires `FROM`/`ON`/`REPLACE` all
three. Same root cause as COUNT: the result template uses
`<key>`, `<f1>`, `<x1>` unconditionally; missing any of them
produced broken syntax. Tightened to fail loudly rather than
silently mis-expand.
* CLOSE <unknown_alias> no longer closes the *current* workarea.
SelectByAlias was a silent no-op when the alias was missing,
leaving WASaveAndSelectAlias to evaluate the inner DbCloseArea()
against the originally-selected WA — a real data-loss footgun.
SelectByAlias now returns bool; WASaveAndSelectAlias switches to
the no-area sentinel (0) on miss so the inner expression's
Current() returns nil and short-circuits.
* SUM <x1>, <xN> TO <v1>, <vN> — multi-pair form supported.
Required two pieces:
1. matchSegment's regular-marker stop-boundary now combines
outerTail literals AND the segment's repeat boundary so
`[, <xN>]` doesn't let `<xN>` swallow past the next ','.
2. **Five parser miscompiled comma-separated expressions in
code blocks.** `{|| e1, e2, e3 }` kept only the last expr
and threw away earlier ones at *AST level*, so all their
side effects vanished. New SeqExpr AST node + emitter
(emit each, pop intermediate results) + folding/walk
updates fix the underlying bug, which also unbreaks any
other block that relied on comma sequencing.
* pp.go's `;` continuation joiner now strips exactly one trailing
`;` per iteration, preserving Harbour's `;;` convention (literal
`;` followed by a continuation marker). Without this the SUM
rule's chained `<v1> :=[ <vN> :=] 0 ; ; dbEval(...)` collapsed
to a missing statement separator.
* parseExprStmt's xBase fallback switch is back in sync with
parseIdentStmt — COPY/SORT/COUNT/SUM/AVERAGE/TOTAL/UPDATE/JOIN/
DISPLAY/LIST removed (std.ch handles all of them now). Leaving
them in the fallback masked typos as silent no-ops.
Gates green:
go test ./... : PASS
FiveSql2 SQL:1999 : 43/43
Harbour compat : 56/56
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
296 lines
9.2 KiB
Go
296 lines
9.2 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)
|
|
case *ast.SeqExpr:
|
|
// Comma-separated expressions inside a code block — recurse so
|
|
// every sub-expr's free variables are picked up for closure
|
|
// capture. Otherwise the second/third comma-statements would
|
|
// see uncaptured outer locals.
|
|
for _, item := range e.Items {
|
|
g.walkExprIdents(item, fn)
|
|
}
|
|
}
|
|
}
|