Complete Harbour-compatible MEMVAR implementation: - PUBLIC: global scope, persist until program end - PRIVATE: function scope + called functions, auto-release on return - Shadowing: PRIVATE can shadow PUBLIC, restored on scope exit - Nested: multi-level PRIVATE scoping with save/restore stack - Thread.PushMemvar/PopMemvar: stack-based memvar access - Thread.DeclarePublic/DeclarePrivate: declaration helpers - MacroEval: &cVar now looks up memvars (was returning string) - Shutdown: Phase 4 clears all memvars on all threads - Case-insensitive: all lookups uppercased Tests: 12 tests including: PUBLIC create/update, case-insensitive, PRIVATE basic, shadow/restore, nested 3-level shadow, new var cleanup, release, releaseAll, names, thread integration, macro access Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
310 lines
6.8 KiB
Go
310 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.Keys = append(h.Keys, t.evalExpr(e.Keys[i]))
|
|
h.Values = append(h.Values, 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)
|
|
}
|
|
}
|