519c1f2df4
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>
836 lines
18 KiB
Go
836 lines
18 KiB
Go
// Small, safe expression evaluator — a Go port of web/src/lib/expr.ts.
|
|
//
|
|
// Supports numbers, booleans (true/false → 1/0), arithmetic (+ - * / %),
|
|
// comparison (< <= > >= == !=), boolean (&& || !), ternary (a ? b : c),
|
|
// 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 (brace content split on FIRST ':').
|
|
// bareIdent a graph-local state variable (data source "local").
|
|
//
|
|
// 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 (
|
|
"fmt"
|
|
"math"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
// 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 {
|
|
DS string
|
|
Name string
|
|
}
|
|
|
|
// ── AST ──────────────────────────────────────────────────────────────────────
|
|
|
|
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
|
|
}
|
|
type binNode struct {
|
|
op string
|
|
a, b exprNode
|
|
}
|
|
type ternNode struct{ c, a, b exprNode }
|
|
type callNode struct {
|
|
fn string
|
|
args []exprNode
|
|
}
|
|
|
|
func mustNum(v Value) float64 {
|
|
f, err := asNum(v)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return f
|
|
}
|
|
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) Value {
|
|
args := make([]Value, len(n.args))
|
|
for i, a := range n.args {
|
|
args[i] = a.eval(R)
|
|
}
|
|
// 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) Value {
|
|
a, b := mustNum(n.a.eval(R)), mustNum(n.b.eval(R))
|
|
switch n.op {
|
|
case "+":
|
|
return a + b
|
|
case "-":
|
|
return a - b
|
|
case "*":
|
|
return a * b
|
|
case "/":
|
|
return a / b
|
|
case "%":
|
|
return math.Mod(a, b)
|
|
case "<":
|
|
return boolf(a < b)
|
|
case "<=":
|
|
return boolf(a <= b)
|
|
case ">":
|
|
return boolf(a > b)
|
|
case ">=":
|
|
return boolf(a >= b)
|
|
case "==":
|
|
return boolf(a == b)
|
|
case "!=":
|
|
return boolf(a != b)
|
|
case "&&":
|
|
return boolf(a != 0 && b != 0)
|
|
case "||":
|
|
return boolf(a != 0 || b != 0)
|
|
}
|
|
panic(fmt.Errorf("unknown operator %q", n.op))
|
|
}
|
|
|
|
func boolf(b bool) float64 {
|
|
if b {
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
|
|
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]) },
|
|
"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(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]) },
|
|
"sin": func(a []float64) float64 { return math.Sin(a[0]) },
|
|
"cos": func(a []float64) float64 { return math.Cos(a[0]) },
|
|
}
|
|
|
|
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 signOf(x float64) int {
|
|
switch {
|
|
case x > 0:
|
|
return 1
|
|
case x < 0:
|
|
return -1
|
|
default:
|
|
return 0
|
|
}
|
|
}
|
|
|
|
// 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 {
|
|
k string
|
|
v string
|
|
}
|
|
|
|
func tokenize(src string) ([]tok, error) {
|
|
var toks []tok
|
|
two := map[string]bool{"<=": true, ">=": true, "==": true, "!=": true, "&&": true, "||": true}
|
|
r := []rune(src)
|
|
i := 0
|
|
for i < len(r) {
|
|
c := r[i]
|
|
switch {
|
|
case c == ' ' || c == '\t' || c == '\n' || c == '\r':
|
|
i++
|
|
continue
|
|
case c == '{':
|
|
end := -1
|
|
for j := i + 1; j < len(r); j++ {
|
|
if r[j] == '}' {
|
|
end = j
|
|
break
|
|
}
|
|
}
|
|
if end < 0 {
|
|
return nil, fmt.Errorf("unterminated { in expression")
|
|
}
|
|
toks = append(toks, tok{k: "sig", v: string(r[i+1 : end])})
|
|
i = end + 1
|
|
continue
|
|
}
|
|
if isDigit(c) || (c == '.' && i+1 < len(r) && isDigit(r[i+1])) {
|
|
j := i + 1
|
|
for j < len(r) && (isDigit(r[j]) || r[j] == '.') {
|
|
j++
|
|
}
|
|
toks = append(toks, tok{k: "num", v: string(r[i:j])})
|
|
i = j
|
|
continue
|
|
}
|
|
if isIdentStart(c) {
|
|
j := i + 1
|
|
for j < len(r) && isIdentPart(r[j]) {
|
|
j++
|
|
}
|
|
toks = append(toks, tok{k: "ident", v: string(r[i:j])})
|
|
i = j
|
|
continue
|
|
}
|
|
if i+1 < len(r) {
|
|
pair := string(r[i : i+2])
|
|
if two[pair] {
|
|
toks = append(toks, tok{k: pair})
|
|
i += 2
|
|
continue
|
|
}
|
|
}
|
|
if strings.ContainsRune("+-*/%<>!()?:,[]", c) {
|
|
toks = append(toks, tok{k: string(c)})
|
|
i++
|
|
continue
|
|
}
|
|
return nil, fmt.Errorf("unexpected character %q in expression", string(c))
|
|
}
|
|
return toks, nil
|
|
}
|
|
|
|
func isDigit(c rune) bool { return c >= '0' && c <= '9' }
|
|
func isIdentStart(c rune) bool { return c == '_' || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') }
|
|
func isIdentPart(c rune) bool { return isIdentStart(c) || isDigit(c) }
|
|
|
|
// ── Parser (recursive descent) ────────────────────────────────────────────────
|
|
|
|
type parser struct {
|
|
toks []tok
|
|
p int
|
|
}
|
|
|
|
func (ps *parser) peek() (tok, bool) {
|
|
if ps.p < len(ps.toks) {
|
|
return ps.toks[ps.p], true
|
|
}
|
|
return tok{}, false
|
|
}
|
|
|
|
func (ps *parser) eat(k string) (tok, error) {
|
|
if ps.p >= len(ps.toks) {
|
|
return tok{}, fmt.Errorf("unexpected end of expression")
|
|
}
|
|
t := ps.toks[ps.p]
|
|
if k != "" && t.k != k {
|
|
return tok{}, fmt.Errorf("expected %q in expression", k)
|
|
}
|
|
ps.p++
|
|
return t, nil
|
|
}
|
|
|
|
func parse(src string) (exprNode, error) {
|
|
toks, err := tokenize(src)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ps := &parser{toks: toks}
|
|
root, err := ps.ternary()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if ps.p < len(ps.toks) {
|
|
return nil, fmt.Errorf("trailing tokens in expression")
|
|
}
|
|
return root, nil
|
|
}
|
|
|
|
func (ps *parser) atom() (exprNode, error) {
|
|
t, ok := ps.peek()
|
|
if !ok {
|
|
return nil, fmt.Errorf("unexpected end of expression")
|
|
}
|
|
switch t.k {
|
|
case "num":
|
|
ps.eat("")
|
|
v, err := strconv.ParseFloat(t.v, 64)
|
|
if err != nil {
|
|
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, ':')
|
|
if idx < 0 {
|
|
return sigNode{ds: t.v, name: ""}, nil
|
|
}
|
|
return sigNode{ds: t.v[:idx], name: t.v[idx+1:]}, nil
|
|
case "ident":
|
|
ps.eat("")
|
|
id := t.v
|
|
if id == "true" {
|
|
return numNode{v: 1}, nil
|
|
}
|
|
if id == "false" {
|
|
return numNode{v: 0}, nil
|
|
}
|
|
if nx, ok := ps.peek(); ok && nx.k == "(" {
|
|
ps.eat("(")
|
|
var args []exprNode
|
|
if nx2, ok := ps.peek(); ok && nx2.k != ")" {
|
|
a, err := ps.ternary()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
args = append(args, a)
|
|
for {
|
|
nx3, ok := ps.peek()
|
|
if !ok || nx3.k != "," {
|
|
break
|
|
}
|
|
ps.eat(",")
|
|
a, err := ps.ternary()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
args = append(args, a)
|
|
}
|
|
}
|
|
if _, err := ps.eat(")"); err != nil {
|
|
return nil, err
|
|
}
|
|
if !knownFunc(id) {
|
|
return nil, fmt.Errorf("unknown function %q", id)
|
|
}
|
|
return callNode{fn: id, args: args}, nil
|
|
}
|
|
return varNode{name: id}, nil
|
|
case "(":
|
|
ps.eat("(")
|
|
e, err := ps.ternary()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if _, err := ps.eat(")"); err != nil {
|
|
return nil, err
|
|
}
|
|
return e, nil
|
|
}
|
|
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("")
|
|
a, err := ps.unary()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return unNode{op: t.k, a: a}, nil
|
|
}
|
|
return ps.primary()
|
|
}
|
|
|
|
func (ps *parser) binLevel(next func() (exprNode, error), ops ...string) (exprNode, error) {
|
|
a, err := next()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for {
|
|
t, ok := ps.peek()
|
|
if !ok || !contains(ops, t.k) {
|
|
return a, nil
|
|
}
|
|
op, _ := ps.eat("")
|
|
b, err := next()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
a = binNode{op: op.k, a: a, b: b}
|
|
}
|
|
}
|
|
|
|
func (ps *parser) mul() (exprNode, error) { return ps.binLevel(ps.unary, "*", "/", "%") }
|
|
func (ps *parser) add() (exprNode, error) { return ps.binLevel(ps.mul, "+", "-") }
|
|
func (ps *parser) cmp() (exprNode, error) { return ps.binLevel(ps.add, "<", "<=", ">", ">=") }
|
|
func (ps *parser) eq() (exprNode, error) { return ps.binLevel(ps.cmp, "==", "!=") }
|
|
func (ps *parser) and() (exprNode, error) { return ps.binLevel(ps.eq, "&&") }
|
|
func (ps *parser) or() (exprNode, error) { return ps.binLevel(ps.and, "||") }
|
|
|
|
func (ps *parser) ternary() (exprNode, error) {
|
|
c, err := ps.or()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if t, ok := ps.peek(); ok && t.k == "?" {
|
|
ps.eat("?")
|
|
a, err := ps.ternary()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if _, err := ps.eat(":"); err != nil {
|
|
return nil, err
|
|
}
|
|
b, err := ps.ternary()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return ternNode{c: c, a: a, b: b}, nil
|
|
}
|
|
return c, nil
|
|
}
|
|
|
|
func contains(s []string, v string) bool {
|
|
for _, x := range s {
|
|
if x == v {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// ── Cache + public API ─────────────────────────────────────────────────────────
|
|
|
|
type cacheEntry struct {
|
|
node exprNode
|
|
err error
|
|
}
|
|
|
|
var (
|
|
cacheMu sync.Mutex
|
|
cache = map[string]cacheEntry{}
|
|
)
|
|
|
|
func parseCached(src string) (exprNode, error) {
|
|
cacheMu.Lock()
|
|
e, ok := cache[src]
|
|
cacheMu.Unlock()
|
|
if ok {
|
|
return e.node, e.err
|
|
}
|
|
n, err := parse(src)
|
|
cacheMu.Lock()
|
|
cache[src] = cacheEntry{node: n, err: err}
|
|
cacheMu.Unlock()
|
|
return n, err
|
|
}
|
|
|
|
// 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()
|
|
}
|
|
return safeEval(n, resolve)
|
|
}
|
|
|
|
func safeEval(n exprNode, resolve Resolver) (out Value) {
|
|
defer func() {
|
|
if recover() != nil {
|
|
out = math.NaN()
|
|
}
|
|
}()
|
|
return n.eval(resolve)
|
|
}
|
|
|
|
// 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.
|
|
func CollectRefs(src string) []RefLite {
|
|
root, err := parseCached(src)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
var out []RefLite
|
|
seen := map[string]bool{}
|
|
add := func(ds, name string) {
|
|
k := ds + "\x00" + name
|
|
if !seen[k] {
|
|
seen[k] = true
|
|
out = append(out, RefLite{DS: ds, Name: name})
|
|
}
|
|
}
|
|
var walk func(n exprNode)
|
|
walk = func(n exprNode) {
|
|
switch t := n.(type) {
|
|
case sigNode:
|
|
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:
|
|
walk(t.a)
|
|
walk(t.b)
|
|
case ternNode:
|
|
walk(t.c)
|
|
walk(t.a)
|
|
walk(t.b)
|
|
case callNode:
|
|
for _, a := range t.args {
|
|
walk(a)
|
|
}
|
|
}
|
|
}
|
|
walk(root)
|
|
return out
|
|
}
|
|
|
|
// CheckExpr validates an expression; returns an error message or "" if it parses.
|
|
func CheckExpr(src string) string {
|
|
if strings.TrimSpace(src) == "" {
|
|
return ""
|
|
}
|
|
if _, err := parse(src); err != nil {
|
|
return err.Error()
|
|
}
|
|
return ""
|
|
}
|