controllogic: locals as Value, init from declarations, value-aware write
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -103,7 +103,7 @@ func setupConfigEngine(t *testing.T) (*Engine, *writableSource, string) {
|
|||||||
|
|
||||||
func TestApplyConfigWritesEveryParam(t *testing.T) {
|
func TestApplyConfigWritesEveryParam(t *testing.T) {
|
||||||
e, src, instID := setupConfigEngine(t)
|
e, src, instID := setupConfigEngine(t)
|
||||||
cg := &compiledGraph{name: "flow", engine: e, locals: map[string]float64{}}
|
cg := &compiledGraph{name: "flow", engine: e, locals: map[string]Value{}}
|
||||||
|
|
||||||
e.applyConfig(cg, instID)
|
e.applyConfig(cg, instID)
|
||||||
|
|
||||||
@@ -117,7 +117,7 @@ func TestApplyConfigWritesEveryParam(t *testing.T) {
|
|||||||
|
|
||||||
func TestApplyConfigUnknownInstanceNoop(t *testing.T) {
|
func TestApplyConfigUnknownInstanceNoop(t *testing.T) {
|
||||||
e, src, _ := setupConfigEngine(t)
|
e, src, _ := setupConfigEngine(t)
|
||||||
cg := &compiledGraph{name: "flow", engine: e, locals: map[string]float64{}}
|
cg := &compiledGraph{name: "flow", engine: e, locals: map[string]Value{}}
|
||||||
|
|
||||||
e.applyConfig(cg, "does-not-exist")
|
e.applyConfig(cg, "does-not-exist")
|
||||||
|
|
||||||
@@ -144,7 +144,7 @@ func TestReadConfigParam(t *testing.T) {
|
|||||||
|
|
||||||
func TestWriteConfigParam(t *testing.T) {
|
func TestWriteConfigParam(t *testing.T) {
|
||||||
e, _, instID := setupConfigEngine(t)
|
e, _, instID := setupConfigEngine(t)
|
||||||
cg := &compiledGraph{name: "flow", engine: e, locals: map[string]float64{}}
|
cg := &compiledGraph{name: "flow", engine: e, locals: map[string]Value{}}
|
||||||
|
|
||||||
e.writeConfigParam(cg, instID, "gain", 7.5)
|
e.writeConfigParam(cg, instID, "gain", 7.5)
|
||||||
|
|
||||||
@@ -163,7 +163,7 @@ func TestWriteConfigParam(t *testing.T) {
|
|||||||
|
|
||||||
func TestCreateConfigInstance(t *testing.T) {
|
func TestCreateConfigInstance(t *testing.T) {
|
||||||
e, _, srcID := setupConfigEngine(t)
|
e, _, srcID := setupConfigEngine(t)
|
||||||
cg := &compiledGraph{name: "flow", engine: e, locals: map[string]float64{}}
|
cg := &compiledGraph{name: "flow", engine: e, locals: map[string]Value{}}
|
||||||
|
|
||||||
src, err := e.cfg.GetInstance(srcID)
|
src, err := e.cfg.GetInstance(srcID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -197,7 +197,7 @@ func TestCreateConfigInstance(t *testing.T) {
|
|||||||
|
|
||||||
func TestSnapshotConfig(t *testing.T) {
|
func TestSnapshotConfig(t *testing.T) {
|
||||||
e, src, instID := setupConfigEngine(t)
|
e, src, instID := setupConfigEngine(t)
|
||||||
cg := &compiledGraph{name: "flow", engine: e, locals: map[string]float64{}}
|
cg := &compiledGraph{name: "flow", engine: e, locals: map[string]Value{}}
|
||||||
|
|
||||||
// Seed current live values for the set's target signals.
|
// Seed current live values for the set's target signals.
|
||||||
_ = src.Write(context.Background(), "GAIN", 3.5)
|
_ = src.Write(context.Background(), "GAIN", 3.5)
|
||||||
|
|||||||
@@ -278,15 +278,19 @@ func (e *Engine) liveGet(ds, name string) float64 {
|
|||||||
|
|
||||||
// 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 float64) {
|
func (e *Engine) write(cg *compiledGraph, target string, val Value) {
|
||||||
ds, name, ok := parseRef(target)
|
ds, name, ok := parseRef(target)
|
||||||
if !ok || math.IsNaN(val) {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if ds == "local" {
|
if ds == "local" {
|
||||||
cg.setLocal(name, val)
|
cg.setLocal(name, val)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
f, isNum := val.(float64)
|
||||||
|
if !isNum || math.IsNaN(f) {
|
||||||
|
return
|
||||||
|
}
|
||||||
if cg.dryRun {
|
if cg.dryRun {
|
||||||
return // simulate: no real data-source write
|
return // simulate: no real data-source write
|
||||||
}
|
}
|
||||||
@@ -301,11 +305,11 @@ func (e *Engine) write(cg *compiledGraph, target string, val float64) {
|
|||||||
Action: "signal.write",
|
Action: "signal.write",
|
||||||
DS: ds,
|
DS: ds,
|
||||||
Signal: name,
|
Signal: name,
|
||||||
Value: strconv.FormatFloat(val, 'g', -1, 64),
|
Value: strconv.FormatFloat(f, 'g', -1, 64),
|
||||||
Detail: "control logic: " + cg.name,
|
Detail: "control logic: " + cg.name,
|
||||||
Outcome: audit.OutcomeOK,
|
Outcome: audit.OutcomeOK,
|
||||||
}
|
}
|
||||||
if err := src.Write(e.root, name, val); err != nil {
|
if err := src.Write(e.root, name, f); err != nil {
|
||||||
e.log.Warn("control logic: write failed", "ds", ds, "signal", name, "err", err)
|
e.log.Warn("control logic: write failed", "ds", ds, "signal", name, "err", err)
|
||||||
ev.Outcome = audit.OutcomeError
|
ev.Outcome = audit.OutcomeError
|
||||||
ev.Error = err.Error()
|
ev.Error = err.Error()
|
||||||
@@ -608,7 +612,8 @@ type compiledGraph struct {
|
|||||||
prevVal map[string]float64 // last value for change triggers
|
prevVal map[string]float64 // last value for change triggers
|
||||||
hasVal map[string]bool
|
hasVal map[string]bool
|
||||||
lastFire map[string]int64 // ns wall clock each trigger last fired
|
lastFire map[string]int64 // ns wall clock each trigger last fired
|
||||||
locals map[string]float64
|
locals map[string]Value
|
||||||
|
decls map[string]StateVar
|
||||||
}
|
}
|
||||||
|
|
||||||
func compile(g Graph) *compiledGraph {
|
func compile(g Graph) *compiledGraph {
|
||||||
@@ -626,12 +631,21 @@ func compile(g Graph) *compiledGraph {
|
|||||||
prevVal: map[string]float64{},
|
prevVal: map[string]float64{},
|
||||||
hasVal: map[string]bool{},
|
hasVal: map[string]bool{},
|
||||||
lastFire: map[string]int64{},
|
lastFire: map[string]int64{},
|
||||||
locals: map[string]float64{},
|
locals: map[string]Value{},
|
||||||
|
decls: map[string]StateVar{},
|
||||||
fireCh: make(chan string, 16),
|
fireCh: make(chan string, 16),
|
||||||
}
|
}
|
||||||
for _, n := range g.Nodes {
|
for _, n := range g.Nodes {
|
||||||
cg.byId[n.ID] = n
|
cg.byId[n.ID] = n
|
||||||
}
|
}
|
||||||
|
for _, sv := range g.StateVars {
|
||||||
|
cg.decls[sv.Name] = sv
|
||||||
|
if sv.Type == "array" {
|
||||||
|
cg.locals[sv.Name] = applySizing(parseInitialArray(sv), sv)
|
||||||
|
} else {
|
||||||
|
cg.locals[sv.Name] = parseScalarInitial(sv)
|
||||||
|
}
|
||||||
|
}
|
||||||
for _, w := range g.Wires {
|
for _, w := range g.Wires {
|
||||||
port := w.FromPort
|
port := w.FromPort
|
||||||
if port == "" {
|
if port == "" {
|
||||||
@@ -682,18 +696,38 @@ func compile(g Graph) *compiledGraph {
|
|||||||
return cg
|
return cg
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cg *compiledGraph) setLocal(name string, v float64) {
|
func parseScalarInitial(sv StateVar) float64 {
|
||||||
|
s := strings.TrimSpace(sv.Initial)
|
||||||
|
switch s {
|
||||||
|
case "true":
|
||||||
|
return 1
|
||||||
|
case "false":
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
f, err := strconv.ParseFloat(s, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cg *compiledGraph) setLocal(name string, v Value) {
|
||||||
cg.stateMu.Lock()
|
cg.stateMu.Lock()
|
||||||
|
if sv, ok := cg.decls[name]; ok && sv.Type == "array" {
|
||||||
|
if arr, isArr := v.([]Value); isArr {
|
||||||
|
v = applySizing(arr, sv)
|
||||||
|
}
|
||||||
|
}
|
||||||
cg.locals[name] = v
|
cg.locals[name] = v
|
||||||
cg.stateMu.Unlock()
|
cg.stateMu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cg *compiledGraph) getLocal(name string) float64 {
|
func (cg *compiledGraph) getLocal(name string) Value {
|
||||||
cg.stateMu.Lock()
|
cg.stateMu.Lock()
|
||||||
defer cg.stateMu.Unlock()
|
defer cg.stateMu.Unlock()
|
||||||
v, ok := cg.locals[name]
|
v, ok := cg.locals[name]
|
||||||
if !ok {
|
if !ok {
|
||||||
return 0
|
return 0.0
|
||||||
}
|
}
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
@@ -897,7 +931,7 @@ func (cg *compiledGraph) activate(triggerID string) {
|
|||||||
dt = float64(now-last) / 1e9
|
dt = float64(now-last) / 1e9
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve := func(ds, name string) Value { // shim: was float64 (Task 4 removes)
|
resolve := func(ds, name string) Value {
|
||||||
switch ds {
|
switch ds {
|
||||||
case "sys":
|
case "sys":
|
||||||
if name == "dt" {
|
if name == "dt" {
|
||||||
@@ -986,8 +1020,12 @@ func (cg *compiledGraph) run(nodeID string, ctx *runCtx) {
|
|||||||
cg.follow(node.ID, "done", ctx)
|
cg.follow(node.ID, "done", ctx)
|
||||||
|
|
||||||
case "action.write":
|
case "action.write":
|
||||||
val := EvalExpr(node.param("expr"), ctx.resolve)
|
val := EvalValue(node.param("expr"), ctx.resolve)
|
||||||
cg.emitDebug(node.ID, val, true)
|
f, isNum := val.(float64)
|
||||||
|
if !isNum {
|
||||||
|
f = math.NaN()
|
||||||
|
}
|
||||||
|
cg.emitDebug(node.ID, f, true)
|
||||||
cg.engine.write(cg, node.param("target"), val)
|
cg.engine.write(cg, node.param("target"), val)
|
||||||
cg.follow(node.ID, "out", ctx)
|
cg.follow(node.ID, "out", ctx)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package controllogic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCompileInitsLocalsFromDecls(t *testing.T) {
|
||||||
|
g := Graph{
|
||||||
|
ID: "g", Name: "n",
|
||||||
|
StateVars: []StateVar{
|
||||||
|
{Name: "count", Type: "number", Initial: "7"},
|
||||||
|
{Name: "flag", Type: "bool", Initial: "true"},
|
||||||
|
{Name: "buf", Type: "array", Initial: "[1,2,3]", Sizing: "capped", Capacity: 4},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
cg := compile(g)
|
||||||
|
if got := cg.getLocal("count"); got != Value(7.0) {
|
||||||
|
t.Fatalf("count = %#v", got)
|
||||||
|
}
|
||||||
|
if got := cg.getLocal("flag"); got != Value(1.0) {
|
||||||
|
t.Fatalf("flag = %#v", got)
|
||||||
|
}
|
||||||
|
if got := cg.getLocal("buf"); !reflect.DeepEqual(got, []Value{1.0, 2.0, 3.0}) {
|
||||||
|
t.Fatalf("buf = %#v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetLocalAppliesSizing(t *testing.T) {
|
||||||
|
g := Graph{ID: "g", Name: "n", StateVars: []StateVar{
|
||||||
|
{Name: "buf", Type: "array", Initial: "[]", Sizing: "capped", Capacity: 2},
|
||||||
|
}}
|
||||||
|
cg := compile(g)
|
||||||
|
cg.setLocal("buf", []Value{1.0, 2.0, 3.0, 4.0})
|
||||||
|
if got := cg.getLocal("buf"); !reflect.DeepEqual(got, []Value{3.0, 4.0}) {
|
||||||
|
t.Fatalf("sized buf = %#v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolverReturnsLocalValue(t *testing.T) {
|
||||||
|
g := Graph{ID: "g", Name: "n", StateVars: []StateVar{
|
||||||
|
{Name: "buf", Type: "array", Initial: "[10,20]", Sizing: "dynamic"},
|
||||||
|
}}
|
||||||
|
cg := compile(g)
|
||||||
|
R := func(ds, name string) Value {
|
||||||
|
if ds == "local" {
|
||||||
|
return cg.getLocal(name)
|
||||||
|
}
|
||||||
|
return 0.0
|
||||||
|
}
|
||||||
|
if v := EvalExpr("buf[1]", R); v != 20 {
|
||||||
|
t.Fatalf("buf[1] = %v", v)
|
||||||
|
}
|
||||||
|
if v := EvalExpr("sum(buf)", R); v != 30 {
|
||||||
|
t.Fatalf("sum(buf) = %v", v)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestEvalExpr(t *testing.T) {
|
func TestEvalExpr(t *testing.T) {
|
||||||
resolve := func(ds, name string) Value { // shim: was float64 (Task 4 removes)
|
resolve := func(ds, name string) Value {
|
||||||
switch {
|
switch {
|
||||||
case ds == "stub" && name == "x":
|
case ds == "stub" && name == "x":
|
||||||
return 10
|
return 10
|
||||||
@@ -46,7 +46,7 @@ func TestEvalExpr(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestEvalExprErrors(t *testing.T) {
|
func TestEvalExprErrors(t *testing.T) {
|
||||||
r := func(ds, name string) Value { return 0 } // shim: was float64 (Task 4 removes)
|
r := func(ds, name string) Value { return 0 }
|
||||||
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)
|
||||||
|
|||||||
Reference in New Issue
Block a user