Files
five/hbrt/macroeval.go
CharlesKWON dd270d5d9d perf: RTL Go-native migration — 27 optimizations, DML up to 70-90x
Systematic pass through PRG hot paths, promoting them to Go RTL while
preserving Harbour/FiveSql2 semantics. Full log in
docs/RTL-Go-Native-Migration.md.

Bench (bench_sql) vs 2026-04-08 baseline
 - B1  SELECT *             2,192 → 114   µs   (19x)
 - B6  INNER JOIN           9,291 → 233   µs   (40x)
 - B7  CTE simple           8,037 → 129   µs   (62x)
 - B9  ROW_NUMBER           3,705 → 265   µs   (14x)
 - B10 RANK PARTITION       4,748 → 309   µs   (15x)
 - B12 INSERT (WA cache)    4,319 →  63   µs   (69x)
 - B13 UPDATE (WA cache)    6,144 →  68   µs   (90x)
 - B15 CTE+WIN+JOIN        18,395 → 1,873 µs   (10x)

Infrastructure
 - HbHash O(1) Index preserving insertion order (Harbour KEEPORDER)
 - HbDeepClone Go RTL (scalar-sharing, immutable hash keys)
 - MEMRDD auto-imported via gengo; all Five programs get mem:name driver
 - SQL plan + pcode caches (s_hPlanCache, s_hDmlPcodeCache)
 - Opt-in SqlWACacheEnable — dbUseArea/Close/Commit batched for DML

SQL engine
 - FiveSql2 lexer ported to Go (byte FSM) with combined automatic
   template parameterization (literals → ?, concat queries share plan)
 - Go RTL: SqlDistinct, SqlGroupRows, SqlWindowPartitions,
   SqlWindowSortPartition, SqlWindowAssignRank, SqlComputeAggSimple,
   SqlBulkInsert, SqlBulkUpdate, SqlExprHasAgg, SqlEvalHaving
 - CTE / subquery / driving-table materialize paths use MEMRDD
 - SqlCoerce/SqlCmp/SqlIsTrue helpers moved from PRG to Go
 - SqlBulkUpdate defers Flush when WA cache active (APFS fsync was
   dominant B13 cost — 1.6ms/call → gone)

Correctness fixes uncovered during migration
 - ASort default path now sorts dates/logicals/timestamps (was no-op)
 - ORDER BY default NULL placement matches PRG SqlRowCompare across
   Go fast path; explicit NULLS FIRST/LAST honored by both paths
 - SqlBulkUpdate respects EXCLUSIVE vs SHARED mode record locks
 - SqlCmp/SqlCmpEq normalize NumInt vs Double (caught by test 6b)

Verification
 - go test ./...              ALL PASS
 - FiveSql2 test_sql1999      43/43
 - tests/compat_harbour       56/56 (+5 new: ASort dates/logicals,
                              AScan int cross-type)
 - Regression test test_null_order.prg for ORDER BY NULL ordering

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 20:20:14 +09:00

309 lines
6.8 KiB
Go

// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
// All rights reserved.
// macroeval.go — Full runtime macro compiler for Five.
//
// Implements &(expression) by reusing Five's own lexer/parser at runtime.
// This is the key advantage of Five: since the compiler is in Go,
// it can be embedded in the runtime for macro compilation.
//
// Harbour's macro compiler (src/macro/macro.y) is a separate YACC grammar.
// Five simply reuses the same parser.
//
// Usage:
// &cVar → simple variable lookup
// &(cVar + "_name") → evaluate string expression, use result as name
// &("Upper(cName)") → evaluate function call at runtime
package hbrt
import (
"five/compiler/ast"
"five/compiler/parser"
"five/compiler/token"
"strconv"
"strings"
)
// MacroEval compiles and evaluates a Harbour expression string at runtime.
// This is the full macro compiler — uses Five's parser to parse the expression,
// then evaluates the AST directly.
func (t *Thread) MacroEval(exprStr string) Value {
exprStr = strings.TrimSpace(exprStr)
if exprStr == "" {
return MakeNil()
}
// Quick path: simple identifier → variable/function lookup
if isSimpleIdent(exprStr) {
return t.macroLookupIdent(exprStr)
}
// Full path: parse the expression and evaluate the AST
source := "FUNCTION __macro__()\nRETURN " + exprStr + "\n"
file, errs := parser.Parse("macro", source)
if len(errs) > 0 || len(file.Decls) == 0 {
// Parse failed — try as simple string
return t.MacroCompile(exprStr)
}
fn, ok := file.Decls[0].(*ast.FuncDecl)
if !ok || len(fn.Body) == 0 {
return t.MacroCompile(exprStr)
}
// Get the RETURN expression
ret, ok := fn.Body[0].(*ast.ReturnStmt)
if !ok || ret.Value == nil {
return t.MacroCompile(exprStr)
}
// Evaluate the AST expression
return t.evalExpr(ret.Value)
}
// evalExpr evaluates an AST expression at runtime.
func (t *Thread) evalExpr(expr ast.Expr) Value {
switch e := expr.(type) {
case *ast.LiteralExpr:
return t.evalLiteral(e)
case *ast.IdentExpr:
return t.macroLookupIdent(e.Name)
case *ast.BinaryExpr:
left := t.evalExpr(e.Left)
right := t.evalExpr(e.Right)
return t.evalBinaryOp(e.Op, left, right)
case *ast.UnaryExpr:
x := t.evalExpr(e.X)
return t.evalUnaryOp(e.Op, x)
case *ast.CallExpr:
return t.evalCall(e)
case *ast.SendExpr:
obj := t.evalExpr(e.Object)
args := make([]Value, len(e.Args))
for i, a := range e.Args {
args[i] = t.evalExpr(a)
}
t.push(obj)
for _, a := range args {
t.push(a)
}
t.Send(e.Method, len(args))
return t.pop()
case *ast.IndexExpr:
arr := t.evalExpr(e.X)
idx := t.evalExpr(e.Index)
if arr.IsArray() {
items := arr.AsArray().Items
i := idx.AsInt()
if i >= 1 && i <= len(items) {
return items[i-1]
}
}
return MakeNil()
case *ast.ArrayLitExpr:
items := make([]Value, len(e.Items))
for i, item := range e.Items {
items[i] = t.evalExpr(item)
}
return MakeArrayFrom(items)
case *ast.HashLitExpr:
h := &HbHash{}
for i := range e.Keys {
h.Set(t.evalExpr(e.Keys[i]), t.evalExpr(e.Values[i]))
}
return MakeHashFrom(h)
case *ast.BlockExpr:
// Return as code block
body := e.Body
return MakeBlock(func(bt *Thread) {
result := bt.evalExpr(body)
bt.push(result)
bt.RetValue()
}, len(e.Params))
case *ast.DotExpr:
// pkg.Func — try GoCallFunc
obj := t.evalExpr(e.X)
results := GoCall(obj, e.Member)
if len(results) > 0 {
return results[0]
}
return MakeNil()
case *ast.SelfExpr:
return t.self
case *ast.AliasExpr:
// alias->field
if ident, ok := e.Alias.(*ast.IdentExpr); ok {
if field, ok := e.Field.(*ast.IdentExpr); ok {
t.PushAliasField(ident.Name, field.Name)
return t.pop()
}
}
return MakeNil()
case *ast.AssignExpr:
val := t.evalExpr(e.Right)
// Assignment in macro — store to memvar or local
if ident, ok := e.Left.(*ast.IdentExpr); ok {
t.macroStoreIdent(ident.Name, val)
}
return val
default:
return MakeNil()
}
}
// evalLiteral converts an AST literal to a Value.
func (t *Thread) evalLiteral(e *ast.LiteralExpr) Value {
switch e.Kind {
case token.NIL_LIT:
return MakeNil()
case token.TRUE:
return MakeBool(true)
case token.FALSE:
return MakeBool(false)
case token.INT:
n, _ := strconv.ParseInt(e.Value, 10, 64)
return MakeNumInt(n)
case token.LONG:
n, _ := strconv.ParseInt(e.Value, 10, 64)
return MakeLong(n)
case token.DOUBLE:
f, _ := strconv.ParseFloat(e.Value, 64)
return MakeDoubleAuto(f)
case token.STRING:
return MakeString(e.Value)
default:
return MakeString(e.Value)
}
}
// evalBinaryOp evaluates a binary operation.
func (t *Thread) evalBinaryOp(op token.Kind, left, right Value) Value {
t.push(left)
t.push(right)
switch op {
case token.PLUS:
t.Plus()
case token.MINUS:
t.Minus()
case token.STAR:
t.Mult()
case token.SLASH:
t.Divide()
case token.PERCENT:
t.Modulus()
case token.POWER:
t.Power()
case token.EQ, token.EXEQ:
t.Equal()
case token.NEQ:
t.NotEqual()
case token.LT:
t.Less()
case token.GT:
t.Greater()
case token.LTE:
t.LessEqual()
case token.GTE:
t.GreaterEqual()
case token.AND:
t.And()
case token.OR:
t.Or()
case token.DOLLAR:
t.InString()
default:
return MakeNil()
}
return t.pop()
}
// evalUnaryOp evaluates a unary operation.
func (t *Thread) evalUnaryOp(op token.Kind, x Value) Value {
t.push(x)
switch op {
case token.MINUS:
t.Negate()
case token.NOT:
t.Not()
case token.INC:
t.Inc()
case token.DEC:
t.Dec()
default:
return x
}
return t.pop()
}
// evalCall evaluates a function call expression.
func (t *Thread) evalCall(e *ast.CallExpr) Value {
// Get function name
var funcName string
if ident, ok := e.Func.(*ast.IdentExpr); ok {
funcName = strings.ToUpper(ident.Name)
} else {
return MakeNil()
}
// Evaluate arguments
args := make([]Value, len(e.Args))
for i, a := range e.Args {
args[i] = t.evalExpr(a)
}
// Find and call function via VM
sym := t.vm.FindSymbol(funcName)
if sym == nil || sym.Func == nil {
return MakeNil()
}
t.PushSymbol(sym)
t.PushNil()
for _, a := range args {
t.push(a)
}
t.Function(len(args))
return t.pop()
}
// macroLookupIdent looks up a name: local → memvar → function.
func (t *Thread) macroLookupIdent(name string) Value {
upper := strings.ToUpper(name)
// Try memvar first (PUBLIC/PRIVATE)
if v, ok := t.Memvars.Get(upper); ok {
return v
}
// Try as function
sym := t.vm.FindSymbol(upper)
if sym != nil && sym.Func != nil {
return MakeString(name) // return name (function reference)
}
// Return as string (field name, unknown variable)
return MakeString(name)
}
// macroStoreIdent stores a value to a named variable (memvar).
func (t *Thread) macroStoreIdent(name string, val Value) {
if !t.Memvars.Set(name, val) {
t.Memvars.SetPrivate(name, val, t.callSP)
}
}