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:
2026-04-18 16:02:16 +09:00
parent 66f045b97e
commit e089c81bcd
3 changed files with 77 additions and 29 deletions

View File

@@ -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