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:
Martino Ferrari
2026-06-24 19:40:29 +02:00
parent 519c1f2df4
commit a6fa4e7c7c
4 changed files with 114 additions and 19 deletions
+50 -12
View File
@@ -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)