controllogic: make expr evaluator value-polymorphic (arrays, indexing, array funcs)
Rewrites internal/controllogic/expr.go to evaluate to Value (scalar float64 or []Value array): adds array literals [a,b], postfix indexing arr[i], and array functions (len, sum, mean, push, pop, slice, concat, sort, …). Resolver type changes from func(...) float64 to func(...) Value; EvalValue added; EvalExpr/EvalBool retain scalar float64/bool returns. Three temporary shims added in engine.go, lua.go, and expr_test.go pending Tasks 4 and 6. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -897,7 +897,7 @@ func (cg *compiledGraph) activate(triggerID string) {
|
||||
dt = float64(now-last) / 1e9
|
||||
}
|
||||
|
||||
resolve := func(ds, name string) float64 {
|
||||
resolve := func(ds, name string) Value { // shim: was float64 (Task 4 removes)
|
||||
switch ds {
|
||||
case "sys":
|
||||
if name == "dt" {
|
||||
|
||||
+369
-61
@@ -2,16 +2,15 @@
|
||||
//
|
||||
// Supports numbers, booleans (true/false → 1/0), arithmetic (+ - * / %),
|
||||
// comparison (< <= > >= == !=), boolean (&& || !), ternary (a ? b : c),
|
||||
// parentheses, and a handful of math functions. Two kinds of variable
|
||||
// reference are resolved live at evaluation time:
|
||||
// parentheses, array literals ([a, b, c]), postfix indexing (arr[i]), and a set
|
||||
// of math + array functions. Two kinds of variable reference are resolved live:
|
||||
//
|
||||
// {ds:name} a data-source signal value (the brace content is split on the
|
||||
// FIRST ':' so EPICS PV names like "MY:PV:NAME" work).
|
||||
// {ds:name} a data-source signal value (brace content split on FIRST ':').
|
||||
// bareIdent a graph-local state variable (data source "local").
|
||||
//
|
||||
// Booleans are represented as numbers: comparisons / logical ops yield 1 or 0,
|
||||
// and any nonzero value is truthy. The evaluator never uses reflection or eval;
|
||||
// it walks a parsed AST against a caller-supplied Resolver.
|
||||
// Values are either a scalar (float64; booleans 1/0) or an array ([]Value). The
|
||||
// evaluator never uses reflection or eval; it walks a parsed AST against a
|
||||
// caller-supplied Resolver.
|
||||
package controllogic
|
||||
|
||||
import (
|
||||
@@ -22,8 +21,8 @@ import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Resolver returns the current numeric value of a signal/local reference.
|
||||
type Resolver func(ds, name string) float64
|
||||
// Resolver returns the current value of a signal/local reference.
|
||||
type Resolver func(ds, name string) Value
|
||||
|
||||
// RefLite identifies one signal/local reference read by an expression.
|
||||
type RefLite struct {
|
||||
@@ -33,11 +32,13 @@ type RefLite struct {
|
||||
|
||||
// ── AST ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
type exprNode interface{ eval(R Resolver) float64 }
|
||||
type exprNode interface{ eval(R Resolver) Value }
|
||||
|
||||
type numNode struct{ v float64 }
|
||||
type sigNode struct{ ds, name string }
|
||||
type varNode struct{ name string }
|
||||
type arrNode struct{ items []exprNode }
|
||||
type indexNode struct{ a, i exprNode }
|
||||
type unNode struct {
|
||||
op string
|
||||
a exprNode
|
||||
@@ -52,33 +53,84 @@ type callNode struct {
|
||||
args []exprNode
|
||||
}
|
||||
|
||||
func (n numNode) eval(R Resolver) float64 { return n.v }
|
||||
func (n sigNode) eval(R Resolver) float64 { return R(n.ds, n.name) }
|
||||
func (n varNode) eval(R Resolver) float64 { return R("local", n.name) }
|
||||
func (n unNode) eval(R Resolver) float64 {
|
||||
if n.op == "-" {
|
||||
return -n.a.eval(R)
|
||||
func mustNum(v Value) float64 {
|
||||
f, err := asNum(v)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if n.a.eval(R) == 0 {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
return f
|
||||
}
|
||||
func (n ternNode) eval(R Resolver) float64 {
|
||||
if n.c.eval(R) != 0 {
|
||||
func mustArr(v Value) []Value {
|
||||
a, err := asArr(v)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
func (n numNode) eval(R Resolver) Value { return n.v }
|
||||
func (n sigNode) eval(R Resolver) Value { return R(n.ds, n.name) }
|
||||
func (n varNode) eval(R Resolver) Value { return R("local", n.name) }
|
||||
func (n arrNode) eval(R Resolver) Value {
|
||||
out := make([]Value, len(n.items))
|
||||
for i, it := range n.items {
|
||||
out[i] = it.eval(R)
|
||||
}
|
||||
return out
|
||||
}
|
||||
func (n indexNode) eval(R Resolver) Value {
|
||||
arr := mustArr(n.a.eval(R))
|
||||
k, err := idxResolve(mustNum(n.i.eval(R)), len(arr))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return arr[k]
|
||||
}
|
||||
func (n unNode) eval(R Resolver) Value {
|
||||
if n.op == "-" {
|
||||
return -mustNum(n.a.eval(R))
|
||||
}
|
||||
if mustNum(n.a.eval(R)) == 0 {
|
||||
return 1.0
|
||||
}
|
||||
return 0.0
|
||||
}
|
||||
func (n ternNode) eval(R Resolver) Value {
|
||||
if mustNum(n.c.eval(R)) != 0 {
|
||||
return n.a.eval(R)
|
||||
}
|
||||
return n.b.eval(R)
|
||||
}
|
||||
func (n callNode) eval(R Resolver) float64 {
|
||||
args := make([]float64, len(n.args))
|
||||
func (n callNode) eval(R Resolver) Value {
|
||||
args := make([]Value, len(n.args))
|
||||
for i, a := range n.args {
|
||||
args[i] = a.eval(R)
|
||||
}
|
||||
return funcs[n.fn](args)
|
||||
// min/max: scalar-variadic OR single-array form.
|
||||
if n.fn == "min" || n.fn == "max" {
|
||||
if len(args) == 1 {
|
||||
if arr, ok := args[0].([]Value); ok {
|
||||
return reduceMinMax(n.fn, arr)
|
||||
}
|
||||
}
|
||||
nums := make([]Value, len(args))
|
||||
copy(nums, args)
|
||||
return reduceMinMax(n.fn, nums)
|
||||
}
|
||||
if af, ok := arrFuncs[n.fn]; ok {
|
||||
return af(args)
|
||||
}
|
||||
if sf, ok := scalarFuncs[n.fn]; ok {
|
||||
nums := make([]float64, len(args))
|
||||
for i, a := range args {
|
||||
nums[i] = mustNum(a)
|
||||
}
|
||||
return sf(nums)
|
||||
}
|
||||
panic(fmt.Errorf("unknown function %q", n.fn))
|
||||
}
|
||||
func (n binNode) eval(R Resolver) float64 {
|
||||
a, b := n.a.eval(R), n.b.eval(R)
|
||||
func (n binNode) eval(R Resolver) Value {
|
||||
a, b := mustNum(n.a.eval(R)), mustNum(n.b.eval(R))
|
||||
switch n.op {
|
||||
case "+":
|
||||
return a + b
|
||||
@@ -107,7 +159,7 @@ func (n binNode) eval(R Resolver) float64 {
|
||||
case "||":
|
||||
return boolf(a != 0 || b != 0)
|
||||
}
|
||||
return math.NaN()
|
||||
panic(fmt.Errorf("unknown operator %q", n.op))
|
||||
}
|
||||
|
||||
func boolf(b bool) float64 {
|
||||
@@ -117,15 +169,34 @@ func boolf(b bool) float64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
var funcs = map[string]func([]float64) float64{
|
||||
func reduceMinMax(fn string, arr []Value) Value {
|
||||
if len(arr) == 0 {
|
||||
if fn == "min" {
|
||||
return math.Inf(1)
|
||||
}
|
||||
return math.Inf(-1)
|
||||
}
|
||||
m := mustNum(arr[0])
|
||||
for _, x := range arr[1:] {
|
||||
v := mustNum(x)
|
||||
if fn == "min" {
|
||||
m = math.Min(m, v)
|
||||
} else {
|
||||
m = math.Max(m, v)
|
||||
}
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// ── Functions ────────────────────────────────────────────────────────────────
|
||||
|
||||
var scalarFuncs = map[string]func([]float64) float64{
|
||||
"abs": func(a []float64) float64 { return math.Abs(a[0]) },
|
||||
"min": func(a []float64) float64 { return minSlice(a) },
|
||||
"max": func(a []float64) float64 { return maxSlice(a) },
|
||||
"sqrt": func(a []float64) float64 { return math.Sqrt(a[0]) },
|
||||
"floor": func(a []float64) float64 { return math.Floor(a[0]) },
|
||||
"ceil": func(a []float64) float64 { return math.Ceil(a[0]) },
|
||||
"round": func(a []float64) float64 { return math.Round(a[0]) },
|
||||
"sign": func(a []float64) float64 { return float64(sign(a[0])) },
|
||||
"sign": func(a []float64) float64 { return float64(signOf(a[0])) },
|
||||
"pow": func(a []float64) float64 { return math.Pow(a[0], a[1]) },
|
||||
"log": func(a []float64) float64 { return math.Log(a[0]) },
|
||||
"exp": func(a []float64) float64 { return math.Exp(a[0]) },
|
||||
@@ -133,27 +204,145 @@ var funcs = map[string]func([]float64) float64{
|
||||
"cos": func(a []float64) float64 { return math.Cos(a[0]) },
|
||||
}
|
||||
|
||||
func minSlice(a []float64) float64 {
|
||||
if len(a) == 0 {
|
||||
return math.Inf(1)
|
||||
}
|
||||
m := a[0]
|
||||
for _, x := range a[1:] {
|
||||
m = math.Min(m, x)
|
||||
}
|
||||
return m
|
||||
var arrFuncs = map[string]func([]Value) Value{
|
||||
"len": func(a []Value) Value { return float64(len(mustArr(a[0]))) },
|
||||
"sum": func(a []Value) Value {
|
||||
s := 0.0
|
||||
for _, x := range mustArr(a[0]) {
|
||||
s += mustNum(x)
|
||||
}
|
||||
return s
|
||||
},
|
||||
"mean": func(a []Value) Value {
|
||||
r := mustArr(a[0])
|
||||
if len(r) == 0 {
|
||||
return 0.0
|
||||
}
|
||||
s := 0.0
|
||||
for _, x := range r {
|
||||
s += mustNum(x)
|
||||
}
|
||||
return s / float64(len(r))
|
||||
},
|
||||
"slice": func(a []Value) Value {
|
||||
r := mustArr(a[0])
|
||||
s := 0
|
||||
e := len(r)
|
||||
if len(a) > 1 {
|
||||
s = clampIdx(int(mustNum(a[1])), len(r))
|
||||
}
|
||||
if len(a) > 2 {
|
||||
e = clampIdx(int(mustNum(a[2])), len(r))
|
||||
}
|
||||
if s > e {
|
||||
s = e
|
||||
}
|
||||
out := make([]Value, 0, e-s)
|
||||
out = append(out, r[s:e]...)
|
||||
return out
|
||||
},
|
||||
"concat": func(a []Value) Value { return append(append([]Value{}, mustArr(a[0])...), mustArr(a[1])...) },
|
||||
"reverse": func(a []Value) Value { r := append([]Value{}, mustArr(a[0])...); reverse(r); return r },
|
||||
"sort": func(a []Value) Value {
|
||||
r := append([]Value{}, mustArr(a[0])...)
|
||||
sortNum(r)
|
||||
return r
|
||||
},
|
||||
"scale": func(a []Value) Value {
|
||||
r := mustArr(a[0])
|
||||
k := mustNum(a[1])
|
||||
out := make([]Value, len(r))
|
||||
for i, x := range r {
|
||||
out[i] = mustNum(x) * k
|
||||
}
|
||||
return out
|
||||
},
|
||||
"add": func(a []Value) Value {
|
||||
return zipNum(mustArr(a[0]), mustArr(a[1]), func(x, y float64) float64 { return x + y })
|
||||
},
|
||||
"sub": func(a []Value) Value {
|
||||
return zipNum(mustArr(a[0]), mustArr(a[1]), func(x, y float64) float64 { return x - y })
|
||||
},
|
||||
"push": func(a []Value) Value {
|
||||
return append(append([]Value{}, mustArr(a[0])...), a[1])
|
||||
},
|
||||
"set": func(a []Value) Value {
|
||||
r := append([]Value{}, mustArr(a[0])...)
|
||||
k, err := idxResolve(mustNum(a[1]), len(r))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
r[k] = a[2]
|
||||
return r
|
||||
},
|
||||
"insert": func(a []Value) Value {
|
||||
r := append([]Value{}, mustArr(a[0])...)
|
||||
k := int(mustNum(a[1]))
|
||||
if k < 0 {
|
||||
k = 0
|
||||
}
|
||||
if k > len(r) {
|
||||
k = len(r)
|
||||
}
|
||||
r = append(r, nil)
|
||||
copy(r[k+1:], r[k:])
|
||||
r[k] = a[2]
|
||||
return r
|
||||
},
|
||||
"remove": func(a []Value) Value {
|
||||
r := append([]Value{}, mustArr(a[0])...)
|
||||
k, err := idxResolve(mustNum(a[1]), len(r))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return append(r[:k], r[k+1:]...)
|
||||
},
|
||||
"pop": func(a []Value) Value {
|
||||
r := mustArr(a[0])
|
||||
if len(r) == 0 {
|
||||
return []Value{}
|
||||
}
|
||||
return append([]Value{}, r[:len(r)-1]...)
|
||||
},
|
||||
"shift": func(a []Value) Value {
|
||||
r := mustArr(a[0])
|
||||
if len(r) == 0 {
|
||||
return []Value{}
|
||||
}
|
||||
return append([]Value{}, r[1:]...)
|
||||
},
|
||||
"indexOf": func(a []Value) Value {
|
||||
r := mustArr(a[0])
|
||||
for i, x := range r {
|
||||
if valEq(x, a[1]) {
|
||||
return float64(i)
|
||||
}
|
||||
}
|
||||
return -1.0
|
||||
},
|
||||
"contains": func(a []Value) Value {
|
||||
r := mustArr(a[0])
|
||||
for _, x := range r {
|
||||
if valEq(x, a[1]) {
|
||||
return 1.0
|
||||
}
|
||||
}
|
||||
return 0.0
|
||||
},
|
||||
"fill": func(a []Value) Value {
|
||||
n := int(mustNum(a[0]))
|
||||
if n < 0 {
|
||||
n = 0
|
||||
}
|
||||
out := make([]Value, n)
|
||||
for i := range out {
|
||||
out[i] = a[1]
|
||||
}
|
||||
return out
|
||||
},
|
||||
}
|
||||
func maxSlice(a []float64) float64 {
|
||||
if len(a) == 0 {
|
||||
return math.Inf(-1)
|
||||
}
|
||||
m := a[0]
|
||||
for _, x := range a[1:] {
|
||||
m = math.Max(m, x)
|
||||
}
|
||||
return m
|
||||
}
|
||||
func sign(x float64) int {
|
||||
|
||||
func signOf(x float64) int {
|
||||
switch {
|
||||
case x > 0:
|
||||
return 1
|
||||
@@ -164,6 +353,49 @@ func sign(x float64) int {
|
||||
}
|
||||
}
|
||||
|
||||
// sign is an alias for signOf, kept for backward compatibility with existing tests.
|
||||
func sign(x float64) int { return signOf(x) }
|
||||
func clampIdx(i, length int) int {
|
||||
if i < 0 {
|
||||
i = length + i
|
||||
}
|
||||
if i < 0 {
|
||||
i = 0
|
||||
}
|
||||
if i > length {
|
||||
i = length
|
||||
}
|
||||
return i
|
||||
}
|
||||
func reverse(r []Value) {
|
||||
for i, j := 0, len(r)-1; i < j; i, j = i+1, j-1 {
|
||||
r[i], r[j] = r[j], r[i]
|
||||
}
|
||||
}
|
||||
func sortNum(r []Value) {
|
||||
for i := 1; i < len(r); i++ {
|
||||
for j := i; j > 0 && mustNum(r[j-1]) > mustNum(r[j]); j-- {
|
||||
r[j-1], r[j] = r[j], r[j-1]
|
||||
}
|
||||
}
|
||||
}
|
||||
func zipNum(x, y []Value, f func(a, b float64) float64) []Value {
|
||||
n := len(x)
|
||||
if len(y) < n {
|
||||
n = len(y)
|
||||
}
|
||||
out := make([]Value, 0, n)
|
||||
for i := 0; i < n; i++ {
|
||||
out = append(out, f(mustNum(x[i]), mustNum(y[i])))
|
||||
}
|
||||
return out
|
||||
}
|
||||
func valEq(a, b Value) bool {
|
||||
af, aok := a.(float64)
|
||||
bf, bok := b.(float64)
|
||||
return aok && bok && af == bf
|
||||
}
|
||||
|
||||
// ── Tokenizer ────────────────────────────────────────────────────────────────
|
||||
|
||||
type tok struct {
|
||||
@@ -223,7 +455,7 @@ func tokenize(src string) ([]tok, error) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if strings.ContainsRune("+-*/%<>!()?:,", c) {
|
||||
if strings.ContainsRune("+-*/%<>!()?:,[]", c) {
|
||||
toks = append(toks, tok{k: string(c)})
|
||||
i++
|
||||
continue
|
||||
@@ -279,7 +511,7 @@ func parse(src string) (exprNode, error) {
|
||||
return root, nil
|
||||
}
|
||||
|
||||
func (ps *parser) primary() (exprNode, error) {
|
||||
func (ps *parser) atom() (exprNode, error) {
|
||||
t, ok := ps.peek()
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unexpected end of expression")
|
||||
@@ -292,6 +524,32 @@ func (ps *parser) primary() (exprNode, error) {
|
||||
return nil, fmt.Errorf("bad number %q", t.v)
|
||||
}
|
||||
return numNode{v: v}, nil
|
||||
case "[":
|
||||
ps.eat("[")
|
||||
var items []exprNode
|
||||
if nx, ok := ps.peek(); ok && nx.k != "]" {
|
||||
a, err := ps.ternary()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, a)
|
||||
for {
|
||||
nx2, ok := ps.peek()
|
||||
if !ok || nx2.k != "," {
|
||||
break
|
||||
}
|
||||
ps.eat(",")
|
||||
a, err := ps.ternary()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, a)
|
||||
}
|
||||
}
|
||||
if _, err := ps.eat("]"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return arrNode{items: items}, nil
|
||||
case "sig":
|
||||
ps.eat("")
|
||||
idx := strings.IndexByte(t.v, ':')
|
||||
@@ -333,7 +591,7 @@ func (ps *parser) primary() (exprNode, error) {
|
||||
if _, err := ps.eat(")"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, ok := funcs[id]; !ok {
|
||||
if !knownFunc(id) {
|
||||
return nil, fmt.Errorf("unknown function %q", id)
|
||||
}
|
||||
return callNode{fn: id, args: args}, nil
|
||||
@@ -353,6 +611,39 @@ func (ps *parser) primary() (exprNode, error) {
|
||||
return nil, fmt.Errorf("unexpected token %q in expression", t.k)
|
||||
}
|
||||
|
||||
func knownFunc(id string) bool {
|
||||
if id == "min" || id == "max" {
|
||||
return true
|
||||
}
|
||||
if _, ok := arrFuncs[id]; ok {
|
||||
return true
|
||||
}
|
||||
_, ok := scalarFuncs[id]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (ps *parser) primary() (exprNode, error) {
|
||||
n, err := ps.atom()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for {
|
||||
nx, ok := ps.peek()
|
||||
if !ok || nx.k != "[" {
|
||||
return n, nil
|
||||
}
|
||||
ps.eat("[")
|
||||
i, err := ps.ternary()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := ps.eat("]"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
n = indexNode{a: n, i: i}
|
||||
}
|
||||
}
|
||||
|
||||
func (ps *parser) unary() (exprNode, error) {
|
||||
if t, ok := ps.peek(); ok && (t.k == "-" || t.k == "!") {
|
||||
ps.eat("")
|
||||
@@ -449,8 +740,9 @@ func parseCached(src string) (exprNode, error) {
|
||||
return n, err
|
||||
}
|
||||
|
||||
// EvalExpr evaluates an expression string, returning NaN on parse/eval failure.
|
||||
func EvalExpr(src string, resolve Resolver) float64 {
|
||||
// EvalValue evaluates an expression, returning the full Value (number or array).
|
||||
// Returns NaN on parse/eval failure.
|
||||
func EvalValue(src string, resolve Resolver) Value {
|
||||
n, err := parseCached(src)
|
||||
if err != nil {
|
||||
return math.NaN()
|
||||
@@ -458,7 +750,7 @@ func EvalExpr(src string, resolve Resolver) float64 {
|
||||
return safeEval(n, resolve)
|
||||
}
|
||||
|
||||
func safeEval(n exprNode, resolve Resolver) (out float64) {
|
||||
func safeEval(n exprNode, resolve Resolver) (out Value) {
|
||||
defer func() {
|
||||
if recover() != nil {
|
||||
out = math.NaN()
|
||||
@@ -467,14 +759,23 @@ func safeEval(n exprNode, resolve Resolver) (out float64) {
|
||||
return n.eval(resolve)
|
||||
}
|
||||
|
||||
// EvalBool reports whether the expression evaluates to a nonzero, non-NaN value.
|
||||
// EvalExpr evaluates an expression to a scalar; returns NaN on parse/eval
|
||||
// failure OR when the result is an array.
|
||||
func EvalExpr(src string, resolve Resolver) float64 {
|
||||
v := EvalValue(src, resolve)
|
||||
if f, ok := v.(float64); ok {
|
||||
return f
|
||||
}
|
||||
return math.NaN()
|
||||
}
|
||||
|
||||
// EvalBool reports whether the expression evaluates to a nonzero, non-NaN scalar.
|
||||
func EvalBool(src string, resolve Resolver) bool {
|
||||
v := EvalExpr(src, resolve)
|
||||
return !math.IsNaN(v) && v != 0
|
||||
}
|
||||
|
||||
// CollectRefs returns every signal/local reference an expression reads, for
|
||||
// subscription. Returns nil for an unparseable expression.
|
||||
// CollectRefs returns every signal/local reference an expression reads.
|
||||
func CollectRefs(src string) []RefLite {
|
||||
root, err := parseCached(src)
|
||||
if err != nil {
|
||||
@@ -496,6 +797,13 @@ func CollectRefs(src string) []RefLite {
|
||||
add(t.ds, t.name)
|
||||
case varNode:
|
||||
add("local", t.name)
|
||||
case arrNode:
|
||||
for _, it := range t.items {
|
||||
walk(it)
|
||||
}
|
||||
case indexNode:
|
||||
walk(t.a)
|
||||
walk(t.i)
|
||||
case unNode:
|
||||
walk(t.a)
|
||||
case binNode:
|
||||
|
||||
@@ -2,11 +2,12 @@ package controllogic
|
||||
|
||||
import (
|
||||
"math"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEvalExpr(t *testing.T) {
|
||||
resolve := func(ds, name string) float64 {
|
||||
resolve := func(ds, name string) Value { // shim: was float64 (Task 4 removes)
|
||||
switch {
|
||||
case ds == "stub" && name == "x":
|
||||
return 10
|
||||
@@ -45,7 +46,7 @@ func TestEvalExpr(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestEvalExprErrors(t *testing.T) {
|
||||
r := func(ds, name string) float64 { return 0 }
|
||||
r := func(ds, name string) Value { return 0 } // shim: was float64 (Task 4 removes)
|
||||
for _, bad := range []string{"1 +", "(1", "1 2", "{unterminated"} {
|
||||
if v := EvalExpr(bad, r); !math.IsNaN(v) {
|
||||
t.Errorf("EvalExpr(%q) = %v, want NaN", bad, v)
|
||||
@@ -84,3 +85,72 @@ func TestEpicsRefSplitFirstColon(t *testing.T) {
|
||||
t.Errorf("got %+v, want epics / SR:BPM:01:X", refs)
|
||||
}
|
||||
}
|
||||
|
||||
// ── New tests for value-polymorphic evaluator ─────────────────────────────────
|
||||
|
||||
func numResolver(vals map[string]Value) Resolver {
|
||||
return func(ds, name string) Value {
|
||||
if v, ok := vals[ds+":"+name]; ok {
|
||||
return v
|
||||
}
|
||||
return math.NaN()
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvalValueScalar(t *testing.T) {
|
||||
R := numResolver(nil)
|
||||
if got := EvalExpr("2 + 3 * 4", R); got != 14 {
|
||||
t.Fatalf("scalar = %v", got)
|
||||
}
|
||||
if !EvalBool("1 < 2 && 3 >= 3", R) {
|
||||
t.Fatal("bool expr should be true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvalValueArrayLiteralAndIndex(t *testing.T) {
|
||||
R := numResolver(nil)
|
||||
got := EvalValue("[1, 2, 3]", R)
|
||||
if !reflect.DeepEqual(got, []Value{1.0, 2.0, 3.0}) {
|
||||
t.Fatalf("array literal = %#v", got)
|
||||
}
|
||||
if v := EvalExpr("[10,20,30][-1]", R); v != 30 {
|
||||
t.Fatalf("index -1 = %v", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvalArrayFuncs(t *testing.T) {
|
||||
R := numResolver(map[string]Value{"local:buf": []Value{3.0, 1.0, 2.0}})
|
||||
if v := EvalExpr("len(buf)", R); v != 3 {
|
||||
t.Fatalf("len = %v", v)
|
||||
}
|
||||
if v := EvalExpr("sum(buf)", R); v != 6 {
|
||||
t.Fatalf("sum = %v", v)
|
||||
}
|
||||
if v := EvalExpr("max(buf)", R); v != 3 {
|
||||
t.Fatalf("max(array) = %v", v)
|
||||
}
|
||||
if v := EvalExpr("max(1, 9, 4)", R); v != 9 {
|
||||
t.Fatalf("max(scalars) = %v", v)
|
||||
}
|
||||
got := EvalValue("push(buf, 7)", R)
|
||||
if !reflect.DeepEqual(got, []Value{3.0, 1.0, 2.0, 7.0}) {
|
||||
t.Fatalf("push = %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvalExprArrayYieldsNaN(t *testing.T) {
|
||||
if v := EvalExpr("[1,2]", numResolver(nil)); !math.IsNaN(v) {
|
||||
t.Fatalf("array via EvalExpr should be NaN, got %v", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectRefsArray(t *testing.T) {
|
||||
refs := CollectRefs("[{ds:a}, b[0]] ")
|
||||
keys := map[string]bool{}
|
||||
for _, r := range refs {
|
||||
keys[r.DS+":"+r.Name] = true
|
||||
}
|
||||
if !keys["ds:a"] || !keys["local:b"] {
|
||||
t.Fatalf("refs = %#v", refs)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,8 +61,10 @@ func (lr *luaRuntime) ensure() error {
|
||||
target := s.CheckString(1)
|
||||
ds, name, ok := parseRef(target)
|
||||
var v float64
|
||||
if ok && lr.curResolve != nil {
|
||||
v = lr.curResolve(ds, name)
|
||||
if ok && lr.curResolve != nil { // shim: Task 6 finalizes lua Value handling
|
||||
if f, fok := lr.curResolve(ds, name).(float64); fok {
|
||||
v = f
|
||||
}
|
||||
}
|
||||
s.Push(lua.LNumber(v))
|
||||
return 1
|
||||
|
||||
Reference in New Issue
Block a user