controllogic: add Value union + sizing helpers (port of arraypolicy.ts)

This commit is contained in:
Martino Ferrari
2026-06-24 14:42:40 +02:00
parent 5012511306
commit e76865d132
2 changed files with 223 additions and 0 deletions
+156
View File
@@ -0,0 +1,156 @@
// Value model for control-logic locals/expressions. A Value is either a scalar
// (float64; booleans are 1/0) or an array ([]Value). This is the Go port of
// web/src/lib/arraypolicy.ts (sizing) plus the asNum/asArr/idx narrowing from
// web/src/lib/expr.ts. Pure, dependency-free.
package controllogic
import (
"encoding/json"
"fmt"
"strings"
)
// Value is a scalar (float64) or an array ([]Value).
type Value = any
// ARRAY_MAX is the global hard cap on dynamic array length (drops oldest).
const ARRAY_MAX = 1_000_000
// StateVar declares a graph-local variable. Mirrors web/src/lib/types.ts StateVar.
type StateVar struct {
Name string `json:"name"`
Type string `json:"type,omitempty"` // number|bool|string|array (default number)
Initial string `json:"initial"` // initial value, stored as a string
Unit string `json:"unit,omitempty"`
Low float64 `json:"low,omitempty"`
High float64 `json:"high,omitempty"`
Elem string `json:"elem,omitempty"` // array-only: number|bool|array
Sizing string `json:"sizing,omitempty"` // array-only: dynamic|capped|fixed
Capacity int `json:"capacity,omitempty"` // array-only
}
func asNum(v Value) (float64, error) {
f, ok := v.(float64)
if !ok {
return 0, fmt.Errorf("expected a number, got an array")
}
return f, nil
}
func asArr(v Value) ([]Value, error) {
a, ok := v.([]Value)
if !ok {
return nil, fmt.Errorf("expected an array, got a number")
}
return a, nil
}
// idxResolve resolves a possibly-negative index against length; range-checked.
func idxResolve(i float64, length int) (int, error) {
k := int(i) // truncates toward zero, matching Math.trunc
if k < 0 {
k = length + k
}
if k < 0 || k >= length {
return 0, fmt.Errorf("index %v out of range (len %d)", i, length)
}
return k, nil
}
// normalizeValue coerces an arbitrary decoded value (e.g. from JSON: float64,
// bool, []interface{}) into a canonical Value (float64 leaves, []Value arrays).
func normalizeValue(v any) Value {
switch t := v.(type) {
case float64:
return t
case float32:
return float64(t)
case int:
return float64(t)
case int64:
return float64(t)
case bool:
if t {
return 1.0
}
return 0.0
case []interface{}:
out := make([]Value, len(t))
for i, e := range t {
out[i] = normalizeValue(e)
}
return out
default:
return 0.0
}
}
func zeroFill(n int) []Value {
if n < 0 {
n = 0
}
out := make([]Value, n)
for i := range out {
out[i] = 0.0
}
return out
}
// parseInitialArray returns the starting contents of an array local. Mirrors
// arraypolicy.ts parseInitialArray.
func parseInitialArray(sv StateVar) []Value {
cap := sv.Capacity
raw := strings.TrimSpace(sv.Initial)
var parsed []Value
if raw != "" {
var j interface{}
if err := json.Unmarshal([]byte(raw), &j); err == nil {
if arr, ok := j.([]interface{}); ok {
parsed = normalizeValue(arr).([]Value)
}
}
}
if sv.Sizing == "fixed" {
if parsed == nil {
return zeroFill(cap)
}
out := make([]Value, 0, cap)
for i := 0; i < len(parsed) && i < cap; i++ {
out = append(out, parsed[i])
}
for len(out) < cap {
out = append(out, 0.0)
}
return out
}
if parsed == nil {
return []Value{}
}
return parsed
}
// applySizing clamps arr to the declared sizing policy. Mirrors arraypolicy.ts.
func applySizing(arr []Value, sv StateVar) []Value {
cap := sv.Capacity
switch sv.Sizing {
case "fixed":
out := make([]Value, 0, cap)
for i := 0; i < len(arr) && i < cap; i++ {
out = append(out, arr[i])
}
for len(out) < cap {
out = append(out, 0.0)
}
return out
case "capped":
if len(arr) > cap {
return arr[len(arr)-cap:]
}
return arr
default:
if len(arr) > ARRAY_MAX {
return arr[len(arr)-ARRAY_MAX:]
}
return arr
}
}