feat(macro): &var / &(expr) runtime compilation
Harbour's macro operator was a stub: hbrt.MacroCompile only resolved
bare identifier names to memvars/functions and returned the source
string unchanged for any non-trivial expression. The gengo emit was
also broken — `t.MacroPush() + t.PushNil()` never pushed the inner
expression's value, so MacroPush popped whatever happened to be on
the stack.
Wire it up properly:
1. Gengo fix: `case *ast.MacroExpr` now emits `emitExpr(e.Expr);
t.MacroPush()`. The inner expression produces the source string;
MacroPush consumes it and pushes the evaluated result.
2. Hook pattern in hbrt: `SetMacroEvalHook(fn)` lets hbrtl install
the real evaluator without creating an import cycle (genpc
already imports hbrt). MacroPush delegates to the hook when
installed; otherwise falls back to the legacy stub for hbrt
unit tests.
3. hbrtl.init registers macroEval, which reuses compileExprSource
(factored out of PcCompile) so macro lookups share the same
sync.Map-backed pcode cache — repeat evaluations of the same
macro source are free after the first hit.
4. ExecPcode leaves the result in retVal; macroEval copies it to
the operand stack via PushRetValue.
Tested (/tmp/test_macro.prg):
&"10 + 20" → 30
&"Sqrt(16)" → 4
&"Upper('hello')" → HELLO
&("30 * " + Str(nX, 1)) → 210 (runtime-built source)
&"5 > 3 .AND. .T." → .T.
&("Str(" + Str(nX*10,2) + ",2)") → 70
FiveSql2 43/43, Harbour compat 56/56, Go test ALL PASS.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2420,8 +2420,11 @@ func (g *Generator) emitExpr(expr ast.Expr) {
|
||||
// Already converted to fmt.Sprintf CallExpr by parser
|
||||
g.emitExpr(e.Parts[0]) // shouldn't reach here normally
|
||||
case *ast.MacroExpr:
|
||||
g.writeln("t.MacroPush() // runtime macro compilation")
|
||||
g.writeln("t.PushNil()")
|
||||
// &ident / &(expr) — evaluate the inner expression to get the
|
||||
// source string, then MacroPush compiles and runs it via the
|
||||
// hbrtl-installed evaluator hook.
|
||||
g.emitExpr(e.Expr)
|
||||
g.writeln("t.MacroPush()")
|
||||
case *ast.AliasExpr:
|
||||
g.emitAliasExpr(e)
|
||||
case *ast.RefExpr:
|
||||
|
||||
@@ -85,9 +85,36 @@ func (t *Thread) MacroCompile(expr string) Value {
|
||||
return MakeString(expr)
|
||||
}
|
||||
|
||||
// macroEvalHook is installed by hbrtl at init time. It handles the
|
||||
// real work: parse the source via compiler/parser, compile to pcode
|
||||
// via compiler/genpc, execute with ExecPcode. We can't call those
|
||||
// directly from hbrt because genpc imports hbrt — the hook pattern
|
||||
// keeps hbrt's core independent of the compiler packages.
|
||||
//
|
||||
// Stack contract matches MacroPush: pops the source string value,
|
||||
// pushes the evaluated result.
|
||||
var macroEvalHook func(*Thread)
|
||||
|
||||
// SetMacroEvalHook wires in the full macro evaluator. Called by
|
||||
// hbrtl.init(). Without the hook installed, MacroPush falls back to
|
||||
// the legacy stub that only resolves bare identifier names.
|
||||
func SetMacroEvalHook(fn func(*Thread)) {
|
||||
macroEvalHook = fn
|
||||
}
|
||||
|
||||
// MacroPush compiles a macro and pushes the result on stack.
|
||||
// Harbour: HB_P_MACROPUSH
|
||||
//
|
||||
// Stack: [sourceString] → [result]. The caller emits the expression
|
||||
// that yields the source string first — gengo produces
|
||||
// `emitExpr(e.Expr); t.MacroPush()` for &<expr>.
|
||||
func (t *Thread) MacroPush() {
|
||||
if macroEvalHook != nil {
|
||||
macroEvalHook(t)
|
||||
return
|
||||
}
|
||||
// Fallback: legacy simple-ident lookup. Kept so hbrt tests (which
|
||||
// don't init hbrtl) still function for trivial cases.
|
||||
exprVal := t.pop()
|
||||
result := t.MacroCompile(exprVal.AsString())
|
||||
t.push(result)
|
||||
|
||||
@@ -55,70 +55,88 @@ func PcCompile(t *hbrt.Thread) {
|
||||
defer t.EndProc()
|
||||
|
||||
source := t.Local(1).AsString()
|
||||
if source == "" {
|
||||
fn := compileExprSource(source)
|
||||
if fn == nil {
|
||||
t.RetNil()
|
||||
return
|
||||
}
|
||||
t.RetPointer(fn)
|
||||
}
|
||||
|
||||
// Cache hit — skip parser/genpc entirely.
|
||||
// compileExprSource compiles a PRG expression string to a PcodeFunc,
|
||||
// memoising the result in pcCompileCache. Returns nil on parse or
|
||||
// empty-source errors. Shared by PcCompile (the RTL function) and
|
||||
// the macro evaluator (hbrt.MacroPush hook) so both paths benefit
|
||||
// from the same cache.
|
||||
func compileExprSource(source string) *hbrt.PcodeFunc {
|
||||
if source == "" {
|
||||
return nil
|
||||
}
|
||||
if cached, ok := pcCompileCache.Load(source); ok {
|
||||
if fn, ok := cached.(*hbrt.PcodeFunc); ok && fn != nil {
|
||||
t.RetPointer(fn)
|
||||
return
|
||||
return fn
|
||||
}
|
||||
}
|
||||
|
||||
// Wrap expression in a function stub so the parser can handle it.
|
||||
wrapped := "FUNCTION _EXPR()\nRETURN " + source + "\n"
|
||||
|
||||
// Preprocess
|
||||
pre := pp.New()
|
||||
processed, _ := pre.Process("_expr.prg", wrapped)
|
||||
|
||||
// Parse
|
||||
file, errs := parser.ParseWithGoDumps("_expr.prg", processed, pre.GoDumps)
|
||||
if len(errs) > 0 {
|
||||
for _, e := range errs {
|
||||
_, _ = os.Stderr.WriteString("PcCompile: " + e.Error() + "\n")
|
||||
}
|
||||
t.RetNil()
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
// Extract the RETURN expression from the first function
|
||||
if len(file.Decls) == 0 {
|
||||
t.RetNil()
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
// Compile the whole wrapped function to a PcodeModule, then extract
|
||||
// the _EXPR function. This reuses all of genpc's mature emit logic.
|
||||
mod := genpc.Generate(file)
|
||||
if mod == nil {
|
||||
t.RetNil()
|
||||
return
|
||||
return nil
|
||||
}
|
||||
fn, ok := mod.Funcs["_EXPR"]
|
||||
if !ok {
|
||||
// Try uppercase / case variations
|
||||
for name, f := range mod.Funcs {
|
||||
_ = name
|
||||
for _, f := range mod.Funcs {
|
||||
fn = f
|
||||
ok = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !ok || fn == nil {
|
||||
t.RetNil()
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
// Populate the cache. sync.Map.Store handles concurrent writers —
|
||||
// duplicate compilations of the same source waste a few µs but
|
||||
// don't corrupt the map; whichever compilation finishes second
|
||||
// overwrites with an identical value.
|
||||
pcCompileCache.Store(source, fn)
|
||||
t.RetPointer(fn)
|
||||
return fn
|
||||
}
|
||||
|
||||
// init registers the full macro evaluator with hbrt. Without this
|
||||
// hook, hbrt.MacroPush falls back to a stub that only resolves
|
||||
// bare identifiers.
|
||||
func init() {
|
||||
hbrt.SetMacroEvalHook(macroEval)
|
||||
}
|
||||
|
||||
// macroEval implements Harbour `&var` / `&(expr)`. Stack: [source] →
|
||||
// [result]. Compiles the source string to pcode (with the same cache
|
||||
// PcCompile uses), runs it, pushes the return value. Errors fall
|
||||
// through as NIL so malformed macros don't crash the VM.
|
||||
func macroEval(t *hbrt.Thread) {
|
||||
srcVal := t.Pop2()
|
||||
source := srcVal.AsString()
|
||||
fn := compileExprSource(source)
|
||||
if fn == nil {
|
||||
t.PushNil()
|
||||
return
|
||||
}
|
||||
hbrt.ExecPcode(t, fn, nil)
|
||||
// ExecPcode's RetValue opcode stores the result in the retVal slot;
|
||||
// PushRetValue copies it onto the operand stack for our caller.
|
||||
t.PushRetValue()
|
||||
}
|
||||
|
||||
// PcEval(pFunc) → xValue
|
||||
|
||||
Reference in New Issue
Block a user