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:
Martino Ferrari
2026-06-24 14:53:40 +02:00
parent 3ddffc14d7
commit 519c1f2df4
4 changed files with 446 additions and 66 deletions
+1 -1
View File
@@ -897,7 +897,7 @@ func (cg *compiledGraph) activate(triggerID string) {
dt = float64(now-last) / 1e9 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 { switch ds {
case "sys": case "sys":
if name == "dt" { if name == "dt" {
+362 -54
View File
@@ -2,16 +2,15 @@
// //
// Supports numbers, booleans (true/false → 1/0), arithmetic (+ - * / %), // Supports numbers, booleans (true/false → 1/0), arithmetic (+ - * / %),
// comparison (< <= > >= == !=), boolean (&& || !), ternary (a ? b : c), // comparison (< <= > >= == !=), boolean (&& || !), ternary (a ? b : c),
// parentheses, and a handful of math functions. Two kinds of variable // parentheses, array literals ([a, b, c]), postfix indexing (arr[i]), and a set
// reference are resolved live at evaluation time: // 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 // {ds:name} a data-source signal value (brace content split on FIRST ':').
// FIRST ':' so EPICS PV names like "MY:PV:NAME" work).
// bareIdent a graph-local state variable (data source "local"). // bareIdent a graph-local state variable (data source "local").
// //
// Booleans are represented as numbers: comparisons / logical ops yield 1 or 0, // Values are either a scalar (float64; booleans 1/0) or an array ([]Value). The
// and any nonzero value is truthy. The evaluator never uses reflection or eval; // evaluator never uses reflection or eval; it walks a parsed AST against a
// it walks a parsed AST against a caller-supplied Resolver. // caller-supplied Resolver.
package controllogic package controllogic
import ( import (
@@ -22,8 +21,8 @@ import (
"sync" "sync"
) )
// Resolver returns the current numeric value of a signal/local reference. // Resolver returns the current value of a signal/local reference.
type Resolver func(ds, name string) float64 type Resolver func(ds, name string) Value
// RefLite identifies one signal/local reference read by an expression. // RefLite identifies one signal/local reference read by an expression.
type RefLite struct { type RefLite struct {
@@ -33,11 +32,13 @@ type RefLite struct {
// ── AST ────────────────────────────────────────────────────────────────────── // ── AST ──────────────────────────────────────────────────────────────────────
type exprNode interface{ eval(R Resolver) float64 } type exprNode interface{ eval(R Resolver) Value }
type numNode struct{ v float64 } type numNode struct{ v float64 }
type sigNode struct{ ds, name string } type sigNode struct{ ds, name string }
type varNode struct{ name string } type varNode struct{ name string }
type arrNode struct{ items []exprNode }
type indexNode struct{ a, i exprNode }
type unNode struct { type unNode struct {
op string op string
a exprNode a exprNode
@@ -52,33 +53,84 @@ type callNode struct {
args []exprNode args []exprNode
} }
func (n numNode) eval(R Resolver) float64 { return n.v } func mustNum(v Value) float64 {
func (n sigNode) eval(R Resolver) float64 { return R(n.ds, n.name) } f, err := asNum(v)
func (n varNode) eval(R Resolver) float64 { return R("local", n.name) } if err != nil {
func (n unNode) eval(R Resolver) float64 { 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 == "-" { if n.op == "-" {
return -n.a.eval(R) return -mustNum(n.a.eval(R))
} }
if n.a.eval(R) == 0 { if mustNum(n.a.eval(R)) == 0 {
return 1 return 1.0
} }
return 0 return 0.0
} }
func (n ternNode) eval(R Resolver) float64 { func (n ternNode) eval(R Resolver) Value {
if n.c.eval(R) != 0 { if mustNum(n.c.eval(R)) != 0 {
return n.a.eval(R) return n.a.eval(R)
} }
return n.b.eval(R) return n.b.eval(R)
} }
func (n callNode) eval(R Resolver) float64 { func (n callNode) eval(R Resolver) Value {
args := make([]float64, len(n.args)) args := make([]Value, len(n.args))
for i, a := range n.args { for i, a := range n.args {
args[i] = a.eval(R) 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)
} }
func (n binNode) eval(R Resolver) float64 { }
a, b := n.a.eval(R), n.b.eval(R) 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 { switch n.op {
case "+": case "+":
return a + b return a + b
@@ -107,7 +159,7 @@ func (n binNode) eval(R Resolver) float64 {
case "||": case "||":
return boolf(a != 0 || b != 0) return boolf(a != 0 || b != 0)
} }
return math.NaN() panic(fmt.Errorf("unknown operator %q", n.op))
} }
func boolf(b bool) float64 { func boolf(b bool) float64 {
@@ -117,15 +169,34 @@ func boolf(b bool) float64 {
return 0 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]) }, "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]) }, "sqrt": func(a []float64) float64 { return math.Sqrt(a[0]) },
"floor": func(a []float64) float64 { return math.Floor(a[0]) }, "floor": func(a []float64) float64 { return math.Floor(a[0]) },
"ceil": func(a []float64) float64 { return math.Ceil(a[0]) }, "ceil": func(a []float64) float64 { return math.Ceil(a[0]) },
"round": func(a []float64) float64 { return math.Round(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]) }, "pow": func(a []float64) float64 { return math.Pow(a[0], a[1]) },
"log": func(a []float64) float64 { return math.Log(a[0]) }, "log": func(a []float64) float64 { return math.Log(a[0]) },
"exp": func(a []float64) float64 { return math.Exp(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]) }, "cos": func(a []float64) float64 { return math.Cos(a[0]) },
} }
func minSlice(a []float64) float64 { var arrFuncs = map[string]func([]Value) Value{
if len(a) == 0 { "len": func(a []Value) Value { return float64(len(mustArr(a[0]))) },
return math.Inf(1) "sum": func(a []Value) Value {
s := 0.0
for _, x := range mustArr(a[0]) {
s += mustNum(x)
} }
m := a[0] return s
for _, x := range a[1:] { },
m = math.Min(m, x) "mean": func(a []Value) Value {
r := mustArr(a[0])
if len(r) == 0 {
return 0.0
} }
return m s := 0.0
for _, x := range r {
s += mustNum(x)
} }
func maxSlice(a []float64) float64 { return s / float64(len(r))
if len(a) == 0 { },
return math.Inf(-1) "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))
} }
m := a[0] if len(a) > 2 {
for _, x := range a[1:] { e = clampIdx(int(mustNum(a[2])), len(r))
m = math.Max(m, x)
} }
return m if s > e {
s = e
} }
func sign(x float64) int { 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 { switch {
case x > 0: case x > 0:
return 1 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 ──────────────────────────────────────────────────────────────── // ── Tokenizer ────────────────────────────────────────────────────────────────
type tok struct { type tok struct {
@@ -223,7 +455,7 @@ func tokenize(src string) ([]tok, error) {
continue continue
} }
} }
if strings.ContainsRune("+-*/%<>!()?:,", c) { if strings.ContainsRune("+-*/%<>!()?:,[]", c) {
toks = append(toks, tok{k: string(c)}) toks = append(toks, tok{k: string(c)})
i++ i++
continue continue
@@ -279,7 +511,7 @@ func parse(src string) (exprNode, error) {
return root, nil return root, nil
} }
func (ps *parser) primary() (exprNode, error) { func (ps *parser) atom() (exprNode, error) {
t, ok := ps.peek() t, ok := ps.peek()
if !ok { if !ok {
return nil, fmt.Errorf("unexpected end of expression") 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 nil, fmt.Errorf("bad number %q", t.v)
} }
return numNode{v: v}, nil 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": case "sig":
ps.eat("") ps.eat("")
idx := strings.IndexByte(t.v, ':') idx := strings.IndexByte(t.v, ':')
@@ -333,7 +591,7 @@ func (ps *parser) primary() (exprNode, error) {
if _, err := ps.eat(")"); err != nil { if _, err := ps.eat(")"); err != nil {
return nil, err return nil, err
} }
if _, ok := funcs[id]; !ok { if !knownFunc(id) {
return nil, fmt.Errorf("unknown function %q", id) return nil, fmt.Errorf("unknown function %q", id)
} }
return callNode{fn: id, args: args}, nil 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) 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) { func (ps *parser) unary() (exprNode, error) {
if t, ok := ps.peek(); ok && (t.k == "-" || t.k == "!") { if t, ok := ps.peek(); ok && (t.k == "-" || t.k == "!") {
ps.eat("") ps.eat("")
@@ -449,8 +740,9 @@ func parseCached(src string) (exprNode, error) {
return n, err return n, err
} }
// EvalExpr evaluates an expression string, returning NaN on parse/eval failure. // EvalValue evaluates an expression, returning the full Value (number or array).
func EvalExpr(src string, resolve Resolver) float64 { // Returns NaN on parse/eval failure.
func EvalValue(src string, resolve Resolver) Value {
n, err := parseCached(src) n, err := parseCached(src)
if err != nil { if err != nil {
return math.NaN() return math.NaN()
@@ -458,7 +750,7 @@ func EvalExpr(src string, resolve Resolver) float64 {
return safeEval(n, resolve) return safeEval(n, resolve)
} }
func safeEval(n exprNode, resolve Resolver) (out float64) { func safeEval(n exprNode, resolve Resolver) (out Value) {
defer func() { defer func() {
if recover() != nil { if recover() != nil {
out = math.NaN() out = math.NaN()
@@ -467,14 +759,23 @@ func safeEval(n exprNode, resolve Resolver) (out float64) {
return n.eval(resolve) 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 { func EvalBool(src string, resolve Resolver) bool {
v := EvalExpr(src, resolve) v := EvalExpr(src, resolve)
return !math.IsNaN(v) && v != 0 return !math.IsNaN(v) && v != 0
} }
// CollectRefs returns every signal/local reference an expression reads, for // CollectRefs returns every signal/local reference an expression reads.
// subscription. Returns nil for an unparseable expression.
func CollectRefs(src string) []RefLite { func CollectRefs(src string) []RefLite {
root, err := parseCached(src) root, err := parseCached(src)
if err != nil { if err != nil {
@@ -496,6 +797,13 @@ func CollectRefs(src string) []RefLite {
add(t.ds, t.name) add(t.ds, t.name)
case varNode: case varNode:
add("local", t.name) add("local", t.name)
case arrNode:
for _, it := range t.items {
walk(it)
}
case indexNode:
walk(t.a)
walk(t.i)
case unNode: case unNode:
walk(t.a) walk(t.a)
case binNode: case binNode:
+72 -2
View File
@@ -2,11 +2,12 @@ package controllogic
import ( import (
"math" "math"
"reflect"
"testing" "testing"
) )
func TestEvalExpr(t *testing.T) { func TestEvalExpr(t *testing.T) {
resolve := func(ds, name string) float64 { resolve := func(ds, name string) Value { // shim: was float64 (Task 4 removes)
switch { switch {
case ds == "stub" && name == "x": case ds == "stub" && name == "x":
return 10 return 10
@@ -45,7 +46,7 @@ func TestEvalExpr(t *testing.T) {
} }
func TestEvalExprErrors(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"} { for _, bad := range []string{"1 +", "(1", "1 2", "{unterminated"} {
if v := EvalExpr(bad, r); !math.IsNaN(v) { if v := EvalExpr(bad, r); !math.IsNaN(v) {
t.Errorf("EvalExpr(%q) = %v, want NaN", bad, 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) 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)
}
}
+4 -2
View File
@@ -61,8 +61,10 @@ func (lr *luaRuntime) ensure() error {
target := s.CheckString(1) target := s.CheckString(1)
ds, name, ok := parseRef(target) ds, name, ok := parseRef(target)
var v float64 var v float64
if ok && lr.curResolve != nil { if ok && lr.curResolve != nil { // shim: Task 6 finalizes lua Value handling
v = lr.curResolve(ds, name) if f, fok := lr.curResolve(ds, name).(float64); fok {
v = f
}
} }
s.Push(lua.LNumber(v)) s.Push(lua.LNumber(v))
return 1 return 1