Files
five/hbrt/macroeval.go
Charles KWON OhJun 207fa9f7dd fix: Phase 1 Step 0 cleanup + CRITICAL #3, MEDIUM #36-37, LOW #50
Files modified (5):
  hbrt/macro.go — Replace hand-rolled parseFloat/parseInt64 with strconv (#50)
                   Remove stale TODO, redundant TrimSpace
  hbrt/macroeval.go — Use strconv for literal parsing (was using removed functions)
  hbrt/class.go — CRITICAL #3: Change RWMutex to Mutex on classList
                   Prevents slice reallocation race on concurrent GetClass
  hbrt/goroutine.go — #36: Channel double-close protection (sync.Once)
                       #37: Send on closed channel recovery (defer/recover)
                       Add IsClosed(), safe Receive (handles closed channel)
  hbrt/gobridge.go — Already clean (confirmed)
  hbrt/hbfunc.go — Already clean (confirmed)

Issues resolved: #3 (CRITICAL), #36, #37 (MEDIUM), #50 (LOW)
Total fixed: 16/53

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 10:51:20 +09:00

307 lines
6.7 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 as function
sym := t.vm.FindSymbol(upper)
if sym != nil && sym.Func != nil {
// It's a function — don't call, return reference
// Unless it has no args, then call it
return MakeString(name)
}
// Return as string (field name, memvar name)
return MakeString(name)
}
// macroStoreIdent stores a value to a named variable.
func (t *Thread) macroStoreIdent(name string, val Value) {
// TODO: memvar system — for now no-op
_ = name
_ = val
}