controllogic: add action.array.* nodes (push/set/remove/pop/clear)
This commit is contained in:
@@ -276,6 +276,31 @@ func (e *Engine) liveGet(ds, name string) float64 {
|
|||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// setPath assigns v at the nested index path within arr, growing (zero-filling)
|
||||||
|
// as needed and resolving negative indices against length. Port of logic.ts.
|
||||||
|
func setPath(arr []Value, path []int, v Value) []Value {
|
||||||
|
if len(path) == 0 {
|
||||||
|
return arr
|
||||||
|
}
|
||||||
|
k := path[0]
|
||||||
|
if k < 0 {
|
||||||
|
k = len(arr) + k
|
||||||
|
}
|
||||||
|
if k < 0 {
|
||||||
|
return arr
|
||||||
|
}
|
||||||
|
for len(arr) <= k {
|
||||||
|
arr = append(arr, 0.0)
|
||||||
|
}
|
||||||
|
if len(path) == 1 {
|
||||||
|
arr[k] = v
|
||||||
|
return arr
|
||||||
|
}
|
||||||
|
sub, _ := arr[k].([]Value)
|
||||||
|
arr[k] = setPath(sub, path[1:], v)
|
||||||
|
return arr
|
||||||
|
}
|
||||||
|
|
||||||
// write applies an action.write/lua-set to a target: a bare name updates a
|
// write applies an action.write/lua-set to a target: a bare name updates a
|
||||||
// graph-local var; a ds:name target writes to the data source.
|
// graph-local var; a ds:name target writes to the data source.
|
||||||
func (e *Engine) write(cg *compiledGraph, target string, val Value) {
|
func (e *Engine) write(cg *compiledGraph, target string, val Value) {
|
||||||
@@ -686,6 +711,13 @@ func compile(g Graph) *compiledGraph {
|
|||||||
}
|
}
|
||||||
case "action.write", "action.log":
|
case "action.write", "action.log":
|
||||||
wantExpr(n.param("expr"))
|
wantExpr(n.param("expr"))
|
||||||
|
case "action.array.push":
|
||||||
|
wantExpr(n.param("expr"))
|
||||||
|
case "action.array.set":
|
||||||
|
wantExpr(n.param("expr"))
|
||||||
|
wantExpr(n.param("index"))
|
||||||
|
case "action.array.remove":
|
||||||
|
wantExpr(n.param("index"))
|
||||||
case "action.lua":
|
case "action.lua":
|
||||||
cg.luaNodes[n.ID] = newLuaRuntime(n.param("script"))
|
cg.luaNodes[n.ID] = newLuaRuntime(n.param("script"))
|
||||||
for _, r := range luaGetRefs(n.param("script")) {
|
for _, r := range luaGetRefs(n.param("script")) {
|
||||||
@@ -1095,6 +1127,74 @@ func (cg *compiledGraph) run(nodeID string, ctx *runCtx) {
|
|||||||
}
|
}
|
||||||
cg.follow(node.ID, "out", ctx)
|
cg.follow(node.ID, "out", ctx)
|
||||||
|
|
||||||
|
case "action.array.push":
|
||||||
|
name := strings.TrimSpace(node.param("array"))
|
||||||
|
if name != "" {
|
||||||
|
val := EvalValue(node.param("expr"), ctx.resolve)
|
||||||
|
cur, _ := cg.getLocal(name).([]Value)
|
||||||
|
cg.setLocal(name, append(append([]Value{}, cur...), val))
|
||||||
|
}
|
||||||
|
cg.follow(node.ID, "out", ctx)
|
||||||
|
|
||||||
|
case "action.array.set":
|
||||||
|
name := strings.TrimSpace(node.param("array"))
|
||||||
|
if name != "" {
|
||||||
|
cur, _ := cg.getLocal(name).([]Value)
|
||||||
|
arr := append([]Value{}, cur...)
|
||||||
|
var path []int
|
||||||
|
ok := true
|
||||||
|
for _, s := range strings.Split(node.param("index"), ",") {
|
||||||
|
f := EvalExpr(strings.TrimSpace(s), ctx.resolve)
|
||||||
|
if math.IsNaN(f) {
|
||||||
|
ok = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
path = append(path, int(f))
|
||||||
|
}
|
||||||
|
val := EvalValue(node.param("expr"), ctx.resolve)
|
||||||
|
if ok && len(path) > 0 {
|
||||||
|
arr = setPath(arr, path, val)
|
||||||
|
}
|
||||||
|
cg.setLocal(name, arr)
|
||||||
|
}
|
||||||
|
cg.follow(node.ID, "out", ctx)
|
||||||
|
|
||||||
|
case "action.array.remove":
|
||||||
|
name := strings.TrimSpace(node.param("array"))
|
||||||
|
if name != "" {
|
||||||
|
cur, _ := cg.getLocal(name).([]Value)
|
||||||
|
arr := append([]Value{}, cur...)
|
||||||
|
i := int(EvalExpr(node.param("index"), ctx.resolve))
|
||||||
|
k := i
|
||||||
|
if k < 0 {
|
||||||
|
k = len(arr) + k
|
||||||
|
}
|
||||||
|
if k >= 0 && k < len(arr) {
|
||||||
|
arr = append(arr[:k], arr[k+1:]...)
|
||||||
|
}
|
||||||
|
cg.setLocal(name, arr)
|
||||||
|
}
|
||||||
|
cg.follow(node.ID, "out", ctx)
|
||||||
|
|
||||||
|
case "action.array.pop":
|
||||||
|
name := strings.TrimSpace(node.param("array"))
|
||||||
|
if name != "" {
|
||||||
|
cur, _ := cg.getLocal(name).([]Value)
|
||||||
|
arr := append([]Value{}, cur...)
|
||||||
|
if len(arr) > 0 {
|
||||||
|
arr = arr[:len(arr)-1]
|
||||||
|
}
|
||||||
|
cg.setLocal(name, arr)
|
||||||
|
}
|
||||||
|
cg.follow(node.ID, "out", ctx)
|
||||||
|
|
||||||
|
case "action.array.clear":
|
||||||
|
name := strings.TrimSpace(node.param("array"))
|
||||||
|
if name != "" {
|
||||||
|
cg.setLocal(name, []Value{}) // setLocal applies sizing (fixed → zero-pad)
|
||||||
|
}
|
||||||
|
cg.follow(node.ID, "out", ctx)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
cg.follow(node.ID, "out", ctx)
|
cg.follow(node.ID, "out", ctx)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,65 @@
|
|||||||
package controllogic
|
package controllogic
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// runFlowOnce compiles g, wires a minimal engine/context onto the compiled graph
|
||||||
|
// (enough for the run/follow path and the lock-free emitDebug guard), then drives
|
||||||
|
// the flow from triggerID synchronously and returns the compiled graph so callers
|
||||||
|
// can inspect locals.
|
||||||
|
func runFlowOnce(t *testing.T, g Graph, triggerID string) *compiledGraph {
|
||||||
|
t.Helper()
|
||||||
|
cg := compile(g)
|
||||||
|
cg.engine = &Engine{}
|
||||||
|
cg.genCtx = context.Background()
|
||||||
|
R := func(ds, name string) Value {
|
||||||
|
switch ds {
|
||||||
|
case "local":
|
||||||
|
return cg.getLocal(name)
|
||||||
|
default:
|
||||||
|
return 0.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx := &runCtx{fired: triggerID, resolve: R}
|
||||||
|
cg.follow(triggerID, "out", ctx)
|
||||||
|
return cg
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestArrayPushNode(t *testing.T) {
|
||||||
|
g := Graph{
|
||||||
|
ID: "g", Name: "n",
|
||||||
|
StateVars: []StateVar{{Name: "buf", Type: "array", Initial: "[1]", Sizing: "dynamic"}},
|
||||||
|
Nodes: []Node{
|
||||||
|
{ID: "t", Kind: "trigger.timer", Params: map[string]string{"interval": "1000"}},
|
||||||
|
{ID: "p", Kind: "action.array.push", Params: map[string]string{"array": "buf", "expr": "5"}},
|
||||||
|
},
|
||||||
|
Wires: []Wire{{From: "t", To: "p"}},
|
||||||
|
}
|
||||||
|
cg := runFlowOnce(t, g, "t")
|
||||||
|
if got := cg.getLocal("buf"); !reflect.DeepEqual(got, []Value{1.0, 5.0}) {
|
||||||
|
t.Fatalf("after push buf = %#v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestArrayClearNode(t *testing.T) {
|
||||||
|
g := Graph{
|
||||||
|
ID: "g", Name: "n",
|
||||||
|
StateVars: []StateVar{{Name: "buf", Type: "array", Initial: "[1,2,3]", Sizing: "fixed", Capacity: 3}},
|
||||||
|
Nodes: []Node{
|
||||||
|
{ID: "t", Kind: "trigger.timer", Params: map[string]string{"interval": "1000"}},
|
||||||
|
{ID: "c", Kind: "action.array.clear", Params: map[string]string{"array": "buf"}},
|
||||||
|
},
|
||||||
|
Wires: []Wire{{From: "t", To: "c"}},
|
||||||
|
}
|
||||||
|
cg := runFlowOnce(t, g, "t")
|
||||||
|
if got := cg.getLocal("buf"); !reflect.DeepEqual(got, []Value{0.0, 0.0, 0.0}) {
|
||||||
|
t.Fatalf("after clear buf = %#v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestCompileInitsLocalsFromDecls(t *testing.T) {
|
func TestCompileInitsLocalsFromDecls(t *testing.T) {
|
||||||
g := Graph{
|
g := Graph{
|
||||||
ID: "g", Name: "n",
|
ID: "g", Name: "n",
|
||||||
|
|||||||
Reference in New Issue
Block a user