///|
pub struct ControlBinding {
key : String
node_index : Int
slot : GraphParamSlot
} derive(Debug, Eq)
///|
#alias(new)
pub fn ControlBinding::ControlBinding(
key~ : String,
node_index~ : Int,
slot~ : GraphParamSlot,
) -> ControlBinding {
{ key, node_index, slot }
}
///|
pub(all) enum ControlBindingError {
InvalidNodeIndex(Int)
InvalidSlotForNode(Int, GraphParamSlot)
DuplicateKey(String)
/// Binding (key, node_index) targets a template node that
/// optimize_graph eliminated. Prevents silent set_param no-ops.
OrphanBinding(String, Int)
} derive(Debug, Eq)
///|
pub struct ControlBindingBuilder {
priv bindings : Array[ControlBinding]
} derive(Debug)
///|
#alias(new)
pub fn ControlBindingBuilder::ControlBindingBuilder() -> ControlBindingBuilder {
{ bindings: [] }
}
///|
/// Add a binding. Mutates internal array, returns self for chaining.
pub fn ControlBindingBuilder::bind(
self : ControlBindingBuilder,
key~ : String,
node_index~ : Int,
slot~ : GraphParamSlot,
) -> ControlBindingBuilder {
self.bindings.push(ControlBinding::new(key~, node_index~, slot~))
self
}
///|
/// Proven-valid control bindings. Validated against a specific
/// CompiledTemplate at build time (bounds + slot compatibility +
/// orphan detection + key uniqueness).
///
/// No public constructor — only reachable through ControlBindingBuilder::build().
///
/// WARNING: A ControlBindingMap's validity is tied to the template it was
/// built against. After VoicePool::set_template swaps to a new template,
/// bindings validated against the prior template remain type-level valid
/// but may silently retarget the wrong kind of node or no-op against
/// nodes the new template's optimize_graph eliminated. Rebuild the
/// ControlBindingMap whenever the template changes. Structural staleness
/// detection is tracked as a follow-up.
pub struct ControlBindingMap {
priv bindings : Array[ControlBinding]
} derive(Debug, Eq)
///|
/// Number of validated bindings in this map.
pub fn ControlBindingMap::length(self : ControlBindingMap) -> Int {
self.bindings.length()
}
///|
/// Validate all bindings against the compiled template and transition
/// to the proven-valid ControlBindingMap. Per-binding checks in order:
/// node index bounds, slot compatibility with the authoring node kind,
/// post-optimization liveness (rejects bindings on nodes eliminated by
/// optimize_graph), and key uniqueness. Returns the first error found.
pub fn ControlBindingBuilder::build(
self : ControlBindingBuilder,
compiled_template : CompiledTemplate,
) -> Result[ControlBindingMap, ControlBindingError] {
let seen_keys : Map[String, Bool] = {}
for i in 0..= compiled_template.length() {
return Err(ControlBindingError::InvalidNodeIndex(binding.node_index))
}
if !node_accepts_slot(
compiled_template.node_at(binding.node_index),
binding.slot,
) {
return Err(
ControlBindingError::InvalidSlotForNode(
binding.node_index,
binding.slot,
),
)
}
if !compiled_template.is_node_live(binding.node_index) {
return Err(
ControlBindingError::OrphanBinding(binding.key, binding.node_index),
)
}
if seen_keys.contains(binding.key) {
return Err(ControlBindingError::DuplicateKey(binding.key))
}
seen_keys[binding.key] = true
}
// WHY copy: self.bindings is a mutable Array shared with the builder.
// Without copying, post-build bind() calls would mutate the validated map,
// breaking the proven-valid invariant.
Ok({ bindings: self.bindings.copy() })
}
///|
/// Convert pattern controls to graph controls using the validated bindings.
/// Emits GraphControl::set_param for each bound key found in the input map,
/// in binding insertion order. Missing keys are skipped; unrecognized keys
/// are ignored. Values are passed through without domain validation.
pub fn ControlBindingMap::resolve_controls(
self : ControlBindingMap,
controls : Map[String, Double],
) -> Array[GraphControl] {
let result = Array::new(capacity=self.bindings.length())
for i in 0..
result.push(
GraphControl::set_param(binding.node_index, binding.slot, value),
)
None => ()
}
}
result
}
///|
pub impl Show for ControlBinding with output(self, logger) {
logger.write_string(@debug.to_string(self))
}
///|
pub impl Show for ControlBindingError with output(self, logger) {
logger.write_string(@debug.to_string(self))
}
///|
pub impl Show for ControlBindingBuilder with output(self, logger) {
logger.write_string(@debug.to_string(self))
}
///|
pub impl Show for ControlBindingMap with output(self, logger) {
logger.write_string(@debug.to_string(self))
}