diff --git a/compiler/gengo/gengo.go b/compiler/gengo/gengo.go index 4b7ddf6..4d43076 100644 --- a/compiler/gengo/gengo.go +++ b/compiler/gengo/gengo.go @@ -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: diff --git a/hbrt/macro.go b/hbrt/macro.go index a8b9165..747339c 100644 --- a/hbrt/macro.go +++ b/hbrt/macro.go @@ -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 &. 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) diff --git a/hbrtl/pcexpr.go b/hbrtl/pcexpr.go index 9e94eb5..a1096ad 100644 --- a/hbrtl/pcexpr.go +++ b/hbrtl/pcexpr.go @@ -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