Files
five/hbrt/macro.go
CharlesKWON e089c81bcd 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>
2026-04-18 16:02:16 +09:00

142 lines
4.3 KiB
Go

// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
// All rights reserved.
// Runtime macro compiler for Five.
// Implements &variable and &(expression) — runtime code compilation.
//
// Harbour has a full macro compiler (src/macro/macro.y) that parses
// and compiles expressions at runtime. Five uses a simplified approach:
// parse the expression string, then evaluate it using the existing
// lexer/parser/evaluator infrastructure.
//
// Usage:
// LOCAL cField := "salary"
// ? &cField → evaluates variable named "salary"
// ? &(cField + "_new") → evaluates variable named "salary_new"
//
// Reference: /mnt/d/harbour-core/src/macro/
package hbrt
import (
"strconv"
"strings"
)
// MacroCompile compiles and evaluates a macro expression string.
// Returns the result value.
//
// For simple variable references (&cVar):
// Looks up the variable name in memvars/locals.
//
// For complex expressions (&(expr)):
// Would need full expression parser — simplified for now.
func (t *Thread) MacroCompile(expr string) Value {
expr = strings.TrimSpace(expr)
if expr == "" {
return MakeNil()
}
// Simple case: expression is a variable name
// Look up in memvars first, then try as function call
if isSimpleIdent(expr) {
// Try calling as a function (memvar lookup deferred to MacroEval)
sym := t.vm.FindSymbol(strings.ToUpper(expr))
if sym != nil && sym.Func != nil {
t.PushSymbol(sym)
t.PushNil()
t.Function(0)
return t.pop()
}
return MakeString(expr) // return as string if not found
}
// Complex expression: try parsing as number, then as function call
// Full runtime expression parser would be needed for complete macro support.
// This handles common patterns: &("literal"), &(numericExpr)
// Try numeric (use stdlib strconv)
if len(expr) > 0 && (expr[0] >= '0' && expr[0] <= '9' || expr[0] == '-' || expr[0] == '+') {
if strings.Contains(expr, ".") {
if f, err := strconv.ParseFloat(expr, 64); err == nil {
return MakeDoubleAuto(f)
}
} else {
if n, err := strconv.ParseInt(expr, 10, 64); err == nil {
return MakeNumInt(n)
}
}
}
// Try string literal
if len(expr) >= 2 && (expr[0] == '"' && expr[len(expr)-1] == '"' || expr[0] == '\'' && expr[len(expr)-1] == '\'') {
return MakeString(expr[1 : len(expr)-1])
}
// Try .T./.F.
upper := strings.ToUpper(expr)
if upper == ".T." {
return MakeBool(true)
}
if upper == ".F." {
return MakeBool(false)
}
// Return as string (field name, variable name, etc.)
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)
}
// parseFloat and parseInt64 removed — using strconv.ParseFloat/ParseInt instead.
// isSimpleIdent checks if string is a valid simple identifier.
func isSimpleIdent(s string) bool {
if len(s) == 0 {
return false
}
ch := s[0]
if !((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch == '_') {
return false
}
for i := 1; i < len(s); i++ {
ch = s[i]
if !((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || ch == '_') {
return false
}
}
return true
}