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) {
|
||||
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)
|
||||
|
||||
@@ -117,7 +117,7 @@ func TestApplyConfigWritesEveryParam(t *testing.T) {
|
||||
|
||||
func TestApplyConfigUnknownInstanceNoop(t *testing.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")
|
||||
|
||||
@@ -144,7 +144,7 @@ func TestReadConfigParam(t *testing.T) {
|
||||
|
||||
func TestWriteConfigParam(t *testing.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)
|
||||
|
||||
@@ -163,7 +163,7 @@ func TestWriteConfigParam(t *testing.T) {
|
||||
|
||||
func TestCreateConfigInstance(t *testing.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)
|
||||
if err != nil {
|
||||
@@ -197,7 +197,7 @@ func TestCreateConfigInstance(t *testing.T) {
|
||||
|
||||
func TestSnapshotConfig(t *testing.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.
|
||||
_ = 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
|
||||
// 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)
|
||||
if !ok || math.IsNaN(val) {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if ds == "local" {
|
||||
cg.setLocal(name, val)
|
||||
return
|
||||
}
|
||||
f, isNum := val.(float64)
|
||||
if !isNum || math.IsNaN(f) {
|
||||
return
|
||||
}
|
||||
if cg.dryRun {
|
||||
return // simulate: no real data-source write
|
||||
}
|
||||
@@ -301,11 +305,11 @@ func (e *Engine) write(cg *compiledGraph, target string, val float64) {
|
||||
Action: "signal.write",
|
||||
DS: ds,
|
||||
Signal: name,
|
||||
Value: strconv.FormatFloat(val, 'g', -1, 64),
|
||||
Value: strconv.FormatFloat(f, 'g', -1, 64),
|
||||
Detail: "control logic: " + cg.name,
|
||||
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)
|
||||
ev.Outcome = audit.OutcomeError
|
||||
ev.Error = err.Error()
|
||||
@@ -608,7 +612,8 @@ type compiledGraph struct {
|
||||
prevVal map[string]float64 // last value for change triggers
|
||||
hasVal map[string]bool
|
||||
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 {
|
||||
@@ -626,12 +631,21 @@ func compile(g Graph) *compiledGraph {
|
||||
prevVal: map[string]float64{},
|
||||
hasVal: map[string]bool{},
|
||||
lastFire: map[string]int64{},
|
||||
locals: map[string]float64{},
|
||||
locals: map[string]Value{},
|
||||
decls: map[string]StateVar{},
|
||||
fireCh: make(chan string, 16),
|
||||
}
|
||||
for _, n := range g.Nodes {
|
||||
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 {
|
||||
port := w.FromPort
|
||||
if port == "" {
|
||||
@@ -682,18 +696,38 @@ func compile(g Graph) *compiledGraph {
|
||||
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()
|
||||
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.stateMu.Unlock()
|
||||
}
|
||||
|
||||
func (cg *compiledGraph) getLocal(name string) float64 {
|
||||
func (cg *compiledGraph) getLocal(name string) Value {
|
||||
cg.stateMu.Lock()
|
||||
defer cg.stateMu.Unlock()
|
||||
v, ok := cg.locals[name]
|
||||
if !ok {
|
||||
return 0
|
||||
return 0.0
|
||||
}
|
||||
return v
|
||||
}
|
||||
@@ -897,7 +931,7 @@ func (cg *compiledGraph) activate(triggerID string) {
|
||||
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 {
|
||||
case "sys":
|
||||
if name == "dt" {
|
||||
@@ -986,8 +1020,12 @@ func (cg *compiledGraph) run(nodeID string, ctx *runCtx) {
|
||||
cg.follow(node.ID, "done", ctx)
|
||||
|
||||
case "action.write":
|
||||
val := EvalExpr(node.param("expr"), ctx.resolve)
|
||||
cg.emitDebug(node.ID, val, true)
|
||||
val := EvalValue(node.param("expr"), ctx.resolve)
|
||||
f, isNum := val.(float64)
|
||||
if !isNum {
|
||||
f = math.NaN()
|
||||
}
|
||||
cg.emitDebug(node.ID, f, true)
|
||||
cg.engine.write(cg, node.param("target"), val)
|
||||
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) {
|
||||
resolve := func(ds, name string) Value { // shim: was float64 (Task 4 removes)
|
||||
resolve := func(ds, name string) Value {
|
||||
switch {
|
||||
case ds == "stub" && name == "x":
|
||||
return 10
|
||||
@@ -46,7 +46,7 @@ func TestEvalExpr(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"} {
|
||||
if v := EvalExpr(bad, r); !math.IsNaN(v) {
|
||||
t.Errorf("EvalExpr(%q) = %v, want NaN", bad, v)
|
||||
|
||||
Reference in New Issue
Block a user