controllogic: fix data race in setPath nested-array assignment

setPath mutated nested sub-arrays in place; since graph-local slices (and
their nested sub-slices) may be read concurrently by other flow goroutines,
an action.array.set with a multi-level path raced readers of the same local.
The single-threaded TS source mutates in place safely, but the Go port runs
flows on concurrent goroutines, so setPath now copies on descent (every level
returns a freshly allocated slice). Adds a -race regression test that
concurrently drives nested set + inner-element reads, plus a nested-set
correctness test asserting the prior stored value is not mutated.

Found by whole-branch review (Opus); missed by the per-task reviews.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Martino Ferrari
2026-06-24 20:11:14 +02:00
parent b6bc9dc2f2
commit c53a49e540
2 changed files with 87 additions and 8 deletions
+13 -8
View File
@@ -277,7 +277,11 @@ func (e *Engine) liveGet(ds, name string) float64 {
}
// 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.
// as needed and resolving negative indices against length. Adapted from logic.ts;
// unlike the single-threaded TS source it copies on descent rather than mutating in
// place, because a graph-local slice (and its nested sub-slices) may be read
// concurrently by other flow goroutines — in-place mutation of shared backing would
// race. Every level returns a freshly allocated slice.
func setPath(arr []Value, path []int, v Value) []Value {
if len(path) == 0 {
return arr
@@ -289,16 +293,17 @@ func setPath(arr []Value, path []int, v Value) []Value {
if k < 0 {
return arr
}
for len(arr) <= k {
arr = append(arr, 0.0)
out := append([]Value{}, arr...) // copy: backing may be shared with the live local
for len(out) <= k {
out = append(out, 0.0)
}
if len(path) == 1 {
arr[k] = v
return arr
out[k] = v
return out
}
sub, _ := arr[k].([]Value)
arr[k] = setPath(sub, path[1:], v)
return arr
sub, _ := out[k].([]Value)
out[k] = setPath(sub, path[1:], v)
return out
}
// write applies an action.write/lua-set to a target: a bare name updates a