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>
169 lines
4.7 KiB
Go
169 lines
4.7 KiB
Go
// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
|
|
// All rights reserved.
|
|
|
|
// Expression bytecode compilation — PcCompile/PcEval.
|
|
// FiveSql2 and other prepared-statement engines use this to compile
|
|
// hot-path expressions ONCE and execute them per row via bytecode
|
|
// interpreter, avoiding PRG AST tree-walk overhead.
|
|
|
|
package hbrtl
|
|
|
|
import (
|
|
"five/compiler/genpc"
|
|
"five/compiler/parser"
|
|
"five/compiler/pp"
|
|
"five/hbrt"
|
|
"os"
|
|
"sync"
|
|
)
|
|
|
|
// pcCompileCache stores compiled PcodeFunc keyed by the original PRG
|
|
// expression string. Compilation does parser + preprocessor + pcode
|
|
// generation per call (~50-200µs for small expressions); for repeated
|
|
// queries (same SQL template) every call after the first is a
|
|
// sync.Map hit and returns the cached pointer directly.
|
|
//
|
|
// Thread safety: PcodeFunc is immutable after compilation (no
|
|
// per-call mutable state — execution state lives on hbrt.Thread),
|
|
// so sharing the pointer across goroutines is safe.
|
|
//
|
|
// Unbounded: distinct SQL / expression text count is bounded by the
|
|
// caller's query set; for FiveSql2 workloads this is a small constant.
|
|
// Switch to LRU if a pathological caller emerges.
|
|
var pcCompileCache sync.Map // map[string]*hbrt.PcodeFunc
|
|
|
|
// PcCompile(cPrgExpr) → pFunc
|
|
//
|
|
// Compile a PRG expression to pcode. Returns an opaque pointer that can
|
|
// be passed to PcEval(). The expression is wrapped in a stub FUNCTION
|
|
// so the full PRG parser can handle it; then the single RETURN value
|
|
// node is extracted and compiled to a standalone PcodeFunc.
|
|
//
|
|
// Example:
|
|
// pc := PcCompile("FieldGet(4) > 50000")
|
|
// WHILE ! Eof()
|
|
// IF PcEval(pc)
|
|
// AAdd(aRows, ...)
|
|
// ENDIF
|
|
// dbSkip()
|
|
// ENDDO
|
|
//
|
|
// Performance: ~3-5x faster than MacroEval for hot loops because the
|
|
// expression AST is walked once at compile time, not per row.
|
|
func PcCompile(t *hbrt.Thread) {
|
|
t.Frame(1, 0)
|
|
defer t.EndProc()
|
|
|
|
source := t.Local(1).AsString()
|
|
fn := compileExprSource(source)
|
|
if fn == nil {
|
|
t.RetNil()
|
|
return
|
|
}
|
|
t.RetPointer(fn)
|
|
}
|
|
|
|
// 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 {
|
|
return fn
|
|
}
|
|
}
|
|
|
|
wrapped := "FUNCTION _EXPR()\nRETURN " + source + "\n"
|
|
|
|
pre := pp.New()
|
|
processed, _ := pre.Process("_expr.prg", wrapped)
|
|
|
|
file, errs := parser.ParseWithGoDumps("_expr.prg", processed, pre.GoDumps)
|
|
if len(errs) > 0 {
|
|
for _, e := range errs {
|
|
_, _ = os.Stderr.WriteString("PcCompile: " + e.Error() + "\n")
|
|
}
|
|
return nil
|
|
}
|
|
if len(file.Decls) == 0 {
|
|
return nil
|
|
}
|
|
|
|
mod := genpc.Generate(file)
|
|
if mod == nil {
|
|
return nil
|
|
}
|
|
fn, ok := mod.Funcs["_EXPR"]
|
|
if !ok {
|
|
for _, f := range mod.Funcs {
|
|
fn = f
|
|
ok = true
|
|
break
|
|
}
|
|
}
|
|
if !ok || fn == nil {
|
|
return nil
|
|
}
|
|
|
|
pcCompileCache.Store(source, 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
|
|
//
|
|
// Execute a compiled pcode function. Returns the value produced by the
|
|
// compiled expression via the retVal slot. The caller's workarea context
|
|
// is used for field access, so position the WA via GoTo first.
|
|
func PcEval(t *hbrt.Thread) {
|
|
t.Frame(1, 0)
|
|
defer t.EndProcFast()
|
|
|
|
ptr := t.Local(1).AsPointer()
|
|
if ptr == nil {
|
|
t.RetNil()
|
|
return
|
|
}
|
|
fn, ok := ptr.(*hbrt.PcodeFunc)
|
|
if !ok || fn == nil {
|
|
t.RetNil()
|
|
return
|
|
}
|
|
|
|
// Execute the pcode. The RetValue opcode inside the pcode sets
|
|
// t.retVal, and ExecPcode's EndProc preserves it across the frame
|
|
// transition. After ExecPcode returns, t.retVal contains the
|
|
// expression's value — our own EndProc will use it as PcEval's
|
|
// return value.
|
|
hbrt.ExecPcode(t, fn, nil)
|
|
}
|