Skip to main content

quiver/
graph.rs

1//! Layer 3: Patch Graph
2//!
3//! This module provides the runtime graph-based patching system that allows
4//! arbitrary signal routing between modules. It handles topological sorting,
5//! execution ordering, and signal propagation.
6
7use crate::modules::common::{flush_denorm, sanitize_audio};
8use crate::port::{GraphModule, ParamId, PortId, PortSpec, PortValues, SignalKind};
9use crate::StdMap;
10use alloc::boxed::Box;
11use alloc::collections::VecDeque;
12use alloc::format;
13use alloc::string::{String, ToString};
14use alloc::vec::Vec;
15use serde::{Deserialize, Serialize};
16use slotmap::{DefaultKey, SlotMap};
17
18/// Signal validation strictness level
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
20pub enum ValidationMode {
21    /// No validation - allow any connections
22    None,
23    /// Warn on incompatible connections but allow them.
24    ///
25    /// This is the default: it surfaces likely mistakes (e.g. patching Audio into a Gate
26    /// input) as collectable [`Patch::warnings`] without blocking experimentation.
27    #[default]
28    Warn,
29    /// Error on incompatible connections
30    Strict,
31}
32
33/// Result of signal kind compatibility check
34#[derive(Debug, Clone)]
35pub struct CompatibilityResult {
36    pub compatible: bool,
37    pub warning: Option<String>,
38}
39
40impl SignalKind {
41    /// Check if this signal kind is compatible with another for connection
42    /// Returns a compatibility result with optional warning message
43    pub fn is_compatible_with(&self, other: &SignalKind) -> CompatibilityResult {
44        use SignalKind::*;
45
46        // Same types are always compatible
47        if self == other {
48            return CompatibilityResult {
49                compatible: true,
50                warning: None,
51            };
52        }
53
54        // Define compatibility rules
55        match (self, other) {
56            // Audio can connect to any CV for AM/ring mod effects
57            (Audio, CvBipolar) | (CvBipolar, Audio) => CompatibilityResult {
58                compatible: true,
59                warning: Some("Audio/CV connection - ensure this is intentional".to_string()),
60            },
61
62            // Bipolar and unipolar CV are generally compatible with a warning
63            (CvBipolar, CvUnipolar) | (CvUnipolar, CvBipolar) => CompatibilityResult {
64                compatible: true,
65                warning: Some(
66                    "Bipolar/Unipolar CV mismatch - signal may be clipped or offset".to_string(),
67                ),
68            },
69
70            // V/Oct can receive from bipolar CV (for pitch modulation)
71            (CvBipolar, VoltPerOctave) => CompatibilityResult {
72                compatible: true,
73                warning: None,
74            },
75
76            // V/Oct to bipolar CV (extracting pitch as modulation)
77            (VoltPerOctave, CvBipolar) => CompatibilityResult {
78                compatible: true,
79                warning: None,
80            },
81
82            // Gate/Trigger/Clock are interchangeable with warnings
83            (Gate, Trigger) | (Trigger, Gate) => CompatibilityResult {
84                compatible: true,
85                warning: Some("Gate/Trigger connection - timing behavior may differ".to_string()),
86            },
87
88            (Clock, Trigger) | (Trigger, Clock) => CompatibilityResult {
89                compatible: true,
90                warning: None,
91            },
92
93            (Clock, Gate) | (Gate, Clock) => CompatibilityResult {
94                compatible: true,
95                warning: Some("Clock/Gate connection - duty cycle may affect behavior".to_string()),
96            },
97
98            // Audio to V/Oct is unusual but can be used for audio-rate FM
99            (Audio, VoltPerOctave) => CompatibilityResult {
100                compatible: true,
101                warning: Some(
102                    "Audio-rate pitch modulation - ensure this is intentional".to_string(),
103                ),
104            },
105
106            // CV Unipolar can modulate V/Oct (for portamento, etc.)
107            (CvUnipolar, VoltPerOctave) => CompatibilityResult {
108                compatible: true,
109                warning: Some("Unipolar CV to V/Oct - may need offset adjustment".to_string()),
110            },
111
112            // V/Oct to unipolar (unusual)
113            (VoltPerOctave, CvUnipolar) => CompatibilityResult {
114                compatible: true,
115                warning: Some("V/Oct to Unipolar - negative voltages will be clipped".to_string()),
116            },
117
118            // Audio can be used as gate (for envelope followers, etc.)
119            (Audio, Gate) | (Audio, Trigger) => CompatibilityResult {
120                compatible: true,
121                warning: Some("Audio to Gate/Trigger - signal will be thresholded".to_string()),
122            },
123
124            // All other combinations are allowed but with strong warning
125            _ => CompatibilityResult {
126                compatible: true,
127                warning: Some(format!("Unusual connection: {:?} -> {:?}", self, other)),
128            },
129        }
130    }
131}
132
133/// Unique identifier for a node in the patch graph
134pub type NodeId = DefaultKey;
135
136/// Stable, unique identifier for a cable connection.
137///
138/// Assigned by [`Patch::connect`] (and its variants) from a monotonically increasing
139/// counter and stored inside the [`Cable`]. Unlike a positional index into the cable list,
140/// a `CableId` remains valid after other cables are disconnected/removed, so it is safe to
141/// hold and later pass to [`Patch::disconnect`].
142pub type CableId = usize;
143
144/// Reference to a specific port on a specific node
145#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
146pub struct PortRef {
147    pub node: NodeId,
148    pub port: PortId,
149}
150
151/// A cable connecting two ports
152#[derive(Debug, Clone, Serialize, Deserialize)]
153pub struct Cable {
154    /// Stable identifier assigned at connect time (see [`CableId`]).
155    #[serde(default)]
156    pub id: CableId,
157    pub from: PortRef,
158    pub to: PortRef,
159    /// Optional attenuation/gain (-2.0 to 2.0, where 1.0 = unity)
160    /// Negative values invert the signal (attenuverter behavior)
161    pub attenuation: Option<f64>,
162    /// Optional DC offset added after attenuation (-10.0 to 10.0V)
163    pub offset: Option<f64>,
164}
165
166/// Internal node representation.
167///
168/// The module itself lives in [`Patch`]'s `modules` vector rather than here: `compile`
169/// permutes that vector into execution order, so the per-sample loop can walk it in lockstep
170/// with the routing plan instead of resolving a slotmap key for every node on every sample.
171/// `module_slot` is this node's index into it, kept in sync by `add`, `remove` and `compile`.
172struct Node {
173    module_slot: usize,
174    name: String,
175    position: Option<(f32, f32)>,
176    /// Per-node overrides for the base (unpatched) value of control-input ports, keyed by
177    /// port name. Set through [`Patch::set_param_by_id`] and applied at [`Patch::compile`]
178    /// as the [`InputPlan`] default, so an unpatched knob-style input takes this value.
179    /// Empty for a freshly added node.
180    param_overrides: StdMap<String, f64>,
181}
182
183/// Editable, human-facing metadata for a [`Patch`].
184///
185/// Held on the live patch so it survives a `to_def`/`from_def` round-trip (the graph itself
186/// carries no name/author/tags). All fields are optional; a default `PatchMeta` is empty.
187#[derive(Debug, Clone, Default)]
188pub struct PatchMeta {
189    /// Patch name (falls back to the argument passed to [`Patch::to_def`] when `None`).
190    pub name: Option<String>,
191    /// Author / credit.
192    pub author: Option<String>,
193    /// Free-form description.
194    pub description: Option<String>,
195    /// Search / filter tags.
196    pub tags: Vec<String>,
197}
198
199/// Error types for patch operations.
200///
201/// Marked `#[non_exhaustive]`: downstream `match` expressions must include a wildcard arm,
202/// so new variants can be added in future without breaking callers.
203#[derive(Debug, Clone)]
204#[non_exhaustive]
205pub enum PatchError {
206    /// A referenced node does not exist in the patch.
207    InvalidNode {
208        node: NodeId,
209    },
210    /// A referenced port does not exist on the given node.
211    ///
212    /// Carries enough context to be actionable: the offending node, the requested port by
213    /// name and/or id (whichever the caller supplied), and the list of valid port names on
214    /// that node so the message can suggest the intended target.
215    InvalidPort {
216        node: NodeId,
217        name: Option<String>,
218        port: Option<PortId>,
219        available: Vec<String>,
220    },
221    InvalidCable,
222    /// A feedback cycle with no cycle-breaker (delay) was detected.
223    ///
224    /// `nodes` are the [`NodeId`]s stuck in the cycle; `names` are their resolved module
225    /// names captured at error-construction time so [`Display`](core::fmt::Display) can
226    /// print the actual path without a back-reference to the [`Patch`].
227    CycleDetected {
228        nodes: Vec<NodeId>,
229        names: Vec<String>,
230    },
231    CompilationFailed(String),
232    /// Signal type mismatch (only in Strict validation mode)
233    SignalMismatch {
234        from_kind: SignalKind,
235        to_kind: SignalKind,
236        message: String,
237    },
238}
239
240impl core::fmt::Display for PatchError {
241    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
242        match self {
243            PatchError::InvalidNode { node } => write!(f, "Invalid node: {:?}", node),
244            PatchError::InvalidPort {
245                node,
246                name,
247                port,
248                available,
249            } => {
250                write!(f, "Invalid port")?;
251                match (name, port) {
252                    (Some(n), _) => write!(f, " '{}'", n)?,
253                    (None, Some(p)) => write!(f, " #{}", p)?,
254                    (None, None) => {}
255                }
256                write!(f, " on node {:?}", node)?;
257                if available.is_empty() {
258                    write!(f, " (module exposes no matching ports)")
259                } else {
260                    write!(f, " (available ports: {})", available.join(", "))
261                }
262            }
263            PatchError::InvalidCable => write!(f, "Invalid cable"),
264            PatchError::CycleDetected { nodes, names } => {
265                if names.is_empty() {
266                    write!(f, "Cycle detected involving {} nodes", nodes.len())
267                } else {
268                    write!(f, "Cycle detected: {}", names.join(" -> "))
269                }
270            }
271            PatchError::CompilationFailed(msg) => write!(f, "Compilation failed: {}", msg),
272            PatchError::SignalMismatch {
273                from_kind,
274                to_kind,
275                message,
276            } => write!(
277                f,
278                "Signal mismatch: {:?} -> {:?}: {}",
279                from_kind, to_kind, message
280            ),
281        }
282    }
283}
284
285#[cfg(feature = "std")]
286impl std::error::Error for PatchError {}
287
288/// Handle to a node for ergonomic port references
289#[derive(Clone)]
290pub struct NodeHandle {
291    id: NodeId,
292    spec: PortSpec,
293}
294
295impl NodeHandle {
296    pub fn id(&self) -> NodeId {
297        self.id
298    }
299
300    /// Create a NodeHandle from a NodeId and module reference
301    pub fn from_module(id: NodeId, module: &dyn GraphModule) -> Self {
302        Self {
303            id,
304            spec: module.port_spec().clone(),
305        }
306    }
307
308    /// Reference an output port by name, returning an error if it does not exist.
309    ///
310    /// Prefer this over the panicking [`out`](Self::out) when the port name comes from
311    /// untrusted or dynamic input (e.g. deserializing a patch). The error lists the valid
312    /// output ports for this module.
313    pub fn output(&self, name: &str) -> Result<PortRef, PatchError> {
314        match self.spec.output_by_name(name) {
315            Some(port) => Ok(PortRef {
316                node: self.id,
317                port: port.id,
318            }),
319            None => Err(PatchError::InvalidPort {
320                node: self.id,
321                name: Some(name.to_string()),
322                port: None,
323                available: self.output_names().iter().map(|s| s.to_string()).collect(),
324            }),
325        }
326    }
327
328    /// Reference an input port by name, returning an error if it does not exist.
329    ///
330    /// The fallible companion to [`in_`](Self::in_); see [`output`](Self::output).
331    pub fn input(&self, name: &str) -> Result<PortRef, PatchError> {
332        match self.spec.input_by_name(name) {
333            Some(port) => Ok(PortRef {
334                node: self.id,
335                port: port.id,
336            }),
337            None => Err(PatchError::InvalidPort {
338                node: self.id,
339                name: Some(name.to_string()),
340                port: None,
341                available: self.input_names().iter().map(|s| s.to_string()).collect(),
342            }),
343        }
344    }
345
346    /// Reference an output port by name (panicking convenience).
347    ///
348    /// Panics with a message listing the valid output ports if `name` is unknown. For a
349    /// non-panicking version use [`output`](Self::output).
350    pub fn out(&self, name: &str) -> PortRef {
351        self.output(name).unwrap_or_else(|_| {
352            panic!(
353                "Unknown output port: '{}'. Valid output ports: [{}]",
354                name,
355                self.output_names().join(", ")
356            )
357        })
358    }
359
360    /// Reference an input port by name (panicking convenience).
361    ///
362    /// Panics with a message listing the valid input ports if `name` is unknown. For a
363    /// non-panicking version use [`input`](Self::input).
364    pub fn in_(&self, name: &str) -> PortRef {
365        self.input(name).unwrap_or_else(|_| {
366            panic!(
367                "Unknown input port: '{}'. Valid input ports: [{}]",
368                name,
369                self.input_names().join(", ")
370            )
371        })
372    }
373
374    /// List the names of this module's input ports (in spec order).
375    pub fn input_names(&self) -> Vec<&str> {
376        self.spec.inputs.iter().map(|p| p.name.as_str()).collect()
377    }
378
379    /// List the names of this module's output ports (in spec order).
380    pub fn output_names(&self) -> Vec<&str> {
381        self.spec.outputs.iter().map(|p| p.name.as_str()).collect()
382    }
383
384    /// Get the port specification
385    pub fn spec(&self) -> &PortSpec {
386        &self.spec
387    }
388}
389
390/// One precomputed incoming edge feeding an input port.
391///
392/// Built at [`Patch::compile`] from a [`Cable`]. `src_slot` is a dense index into
393/// [`Routing::out_buf`] identifying the source output value, so gathering an input is a
394/// direct array read rather than a scan of every cable in the patch.
395struct InEdge {
396    /// Dense index of the source output value in [`Routing::out_buf`].
397    src_slot: usize,
398    /// Attenuation/gain applied to the source value (see [`Cable::attenuation`]), with an
399    /// absent attenuation baked in as `1.0`.
400    attenuation: f64,
401    /// DC offset added after attenuation (see [`Cable::offset`]), with an absent offset
402    /// baked in as `0.0`.
403    offset: f64,
404}
405
406/// Compiled routing plan for a single input port (in [`PortSpec`] input order).
407struct InputPlan {
408    /// The input port's id (used as the [`PortValues`] key the module reads).
409    port_id: PortId,
410    /// Value used when the input is unpatched and not normalled.
411    default: f64,
412    /// Sibling input port this normals to when unpatched (see two-pass gather).
413    normalled_to: Option<PortId>,
414    /// Whether at least one cable targets this input. Mirrors the pre-compiled engine's
415    /// `has_connection`: when true the input is the sum of its `edges` (even if that sum is
416    /// 0.0), taking precedence over the default and any normalled fallback.
417    has_connection: bool,
418    /// Cables feeding this input, summed at runtime (hardware-style input mixing).
419    edges: Vec<InEdge>,
420}
421
422/// One input that `gather` pass 1 deliberately leaves unresolved: unpatched *and* normalled
423/// to a sibling that pass 1 does resolve.
424///
425/// Precomputing this (short, usually empty) list is what lets pass 2 iterate only the inputs
426/// it actually has work for, instead of testing every input for presence in `dst`.
427struct NormalledPlan {
428    /// The input port to fill in.
429    port_id: PortId,
430    /// The pass-1-resolved sibling input to copy from (already collapsed to the chain's
431    /// terminal by [`resolve_normalled_chains`]).
432    source: PortId,
433    /// Fallback if the sibling somehow has no value.
434    default: f64,
435}
436
437/// Compiled per-node execution record (parallel to [`Patch::execution_order`]).
438struct NodeExec {
439    /// Base index of this node's output values in [`Routing::out_buf`]; output port `k`
440    /// (in [`PortSpec`] order) lives at `out_base + k`.
441    out_base: usize,
442    /// Output port ids in [`PortSpec`] order (slot of `out_ids[k]` is `out_base + k`).
443    out_ids: Vec<PortId>,
444    /// Input plans in [`PortSpec`] input order.
445    inputs: Vec<InputPlan>,
446    /// Exactly the inputs `gather` pass 2 must resolve, in [`PortSpec`] input order. Empty
447    /// for the overwhelming majority of nodes.
448    normalled_pending: Vec<NormalledPlan>,
449    /// Which outputs anything in the patch reads: bit `k` ↔ output port `k` in [`PortSpec`]
450    /// order. Passed to [`GraphModule::tick_masked`] so a module can skip producing what
451    /// nobody consumes. All ones for nodes with more than 32 outputs.
452    wanted_outputs: u32,
453}
454
455impl NodeExec {
456    /// Resolve this node's input values into `dst` using the two-pass normalled rule,
457    /// reading source outputs from the dense `out_buf`. Semantically identical to the
458    /// previous cable-scanning `gather_inputs`, but does a single pass over exactly the
459    /// cables feeding each input.
460    fn gather(&self, out_buf: &[f64], dst: &mut PortValues) {
461        dst.clear();
462        // Pass 1: patched inputs (summed) and plain (non-normalled) defaults.
463        for plan in &self.inputs {
464            if plan.has_connection {
465                let mut sum = 0.0;
466                for e in &plan.edges {
467                    // Unconditional, because the identity coefficients are exact: `x * 1.0`
468                    // is `x` for every value `out_buf` can hold (scatter has already
469                    // sanitized non-finites away), and `+ 0.0` only ever differs by turning
470                    // a `-0.0` term into `+0.0` — which `sum` would do anyway, since it
471                    // starts at `+0.0` and no sum of the form `+0.0 + x` is `-0.0`.
472                    let attenuated = out_buf[e.src_slot] * e.attenuation;
473                    sum += attenuated + e.offset;
474                }
475                dst.set(plan.port_id, sum);
476            } else if plan.normalled_to.is_none() {
477                dst.set(plan.port_id, plan.default);
478            }
479            // else: normalled + unpatched -> resolved in pass 2 below.
480        }
481        // Pass 2: normalled-but-unpatched inputs read the *current-tick* value of the
482        // terminal sibling INPUT they normal to. `resolve_normalled_chains` (at
483        // compile time) collapsed each chain so the source points at a sibling pass 1
484        // already resolved, making this pass order-independent; a cycle or dangling
485        // reference was collapsed to `None`, which pass 1 then handles as a plain
486        // default. `normalled_pending` therefore holds exactly the inputs left unset by
487        // pass 1 — no presence test over every input is needed.
488        for plan in &self.normalled_pending {
489            let value = dst.get(plan.source).unwrap_or(plan.default);
490            dst.set(plan.port_id, value);
491        }
492    }
493
494    /// Scatter the module's outputs from `src` into the dense `out_buf`, flushing denormals
495    /// so inter-module feedback loops cannot circulate subnormal values. Only ports the
496    /// module actually wrote are updated; unwritten output slots retain their prior value
497    /// (matching the previous map-based scatter).
498    ///
499    /// Q199: non-finite outputs are also zeroed here, so a module that produces a
500    /// `NaN`/`Inf` (internal overflow, division by a zero-valued CV, …) cannot poison
501    /// *other* modules' recursive state through the routing buffers. Per-module input
502    /// sanitization (Q160) still matters for a module's own state; this is the
503    /// graph-level containment boundary.
504    ///
505    /// `src` is this node's scratch output buffer, warmed at compile time in [`PortSpec`]
506    /// output order — the same order as `out_ids` and as the `out_buf` window starting at
507    /// `out_base`. Slot `k` therefore lines up on both sides and the copy is an indexed
508    /// walk rather than a per-port lookup ([`PortValues::get_at`] re-checks the id and
509    /// falls back to a lookup if a module ever wrote an off-spec port, so the result is
510    /// identical either way).
511    fn scatter(&self, src: &PortValues, out_buf: &mut [f64]) {
512        for (k, &port_id) in self.out_ids.iter().enumerate() {
513            if let Some(value) = src.get_at(k, port_id) {
514                out_buf[self.out_base + k] = flush_denorm(sanitize_audio(value));
515            }
516        }
517    }
518}
519
520/// Collapse each normalled chain to a compile-time terminal so the runtime
521/// [`NodeExec::gather`] pass 2 is **order-independent**.
522///
523/// `gather` pass 1 resolves every patched input (its edge sum) and every
524/// unpatched **non-normalled** input (its default); pass 2 then resolves each
525/// unpatched **normalled** input by reading its `normalled_to` sibling. The
526/// single-pass, fixed-spec-order pass 2 only works when that sibling is already
527/// resolved — which fails for a forward-ordered or transitive chain (input A
528/// normals to B, B is itself unpatched-and-normalled to a resolved C, with A
529/// before B in spec order): when A is processed B is not yet set.
530///
531/// Rewriting each unpatched normalled input's `normalled_to` to point directly
532/// at its **terminal** sibling — the first one along the chain that pass 1 will
533/// resolve (a patched input, or an unpatched non-normalled input) — makes pass 2
534/// read an always-resolved value regardless of PortSpec order. A cycle or a
535/// dangling reference collapses to `None`, so the input falls back to its own
536/// default. Done once at compile time; the zero-alloc `tick` path is untouched.
537fn resolve_normalled_chains(inputs: &mut [InputPlan]) {
538    let n = inputs.len();
539    // Snapshot the original chain targets so each input resolves against a
540    // consistent view — the walk is independent of the order in which inputs are
541    // rewritten (so, e.g., a cycle collapses every member to its own default
542    // rather than to whichever member happened to be rewritten first).
543    let original: Vec<Option<PortId>> = inputs.iter().map(|p| p.normalled_to).collect();
544    for i in 0..n {
545        // Only unpatched, normalled inputs are resolved in pass 2.
546        if inputs[i].has_connection || original[i].is_none() {
547            continue;
548        }
549        let mut terminal = None;
550        let mut cursor = original[i];
551        // Bounded by input count: guarantees termination even for a cycle.
552        for _ in 0..n {
553            let Some(pid) = cursor else { break };
554            match inputs.iter().position(|p| p.port_id == pid) {
555                // Dangling sibling reference -> fall back to own default.
556                None => break,
557                Some(idx) => {
558                    if inputs[idx].has_connection || original[idx].is_none() {
559                        // Pass-1-resolvable terminal reached.
560                        terminal = Some(pid);
561                        break;
562                    }
563                    // Sibling is itself unpatched + normalled: keep walking.
564                    cursor = original[idx];
565                }
566            }
567        }
568        inputs[i].normalled_to = terminal;
569    }
570}
571
572/// Collect the inputs [`NodeExec::gather`] pass 1 leaves unset, in [`PortSpec`] input order.
573///
574/// Pass 1 resolves every patched input and every unpatched input whose `normalled_to` is
575/// `None` (including the ones [`resolve_normalled_chains`] collapsed to `None`), so the
576/// remainder — unpatched *and* still normalled — is exactly what pass 2 has to fill in.
577/// Hoisting that set to compile time replaces a per-input presence probe per node per sample
578/// with a walk over a list that is empty for nearly every node.
579///
580/// Must run *after* [`resolve_normalled_chains`], whose rewrite decides which inputs pass 1
581/// covers. A repeated `port_id` (a malformed spec) is skipped once already claimed, matching
582/// the previous `if dst.has(port_id) { continue }` guard exactly.
583fn collect_normalled_pending(inputs: &[InputPlan]) -> Vec<NormalledPlan> {
584    let mut claimed: Vec<PortId> = inputs
585        .iter()
586        .filter(|p| p.has_connection || p.normalled_to.is_none())
587        .map(|p| p.port_id)
588        .collect();
589    let mut pending = Vec::new();
590    for plan in inputs {
591        if plan.has_connection {
592            continue;
593        }
594        let Some(source) = plan.normalled_to else {
595            continue;
596        };
597        if claimed.contains(&plan.port_id) {
598            continue;
599        }
600        claimed.push(plan.port_id);
601        pending.push(NormalledPlan {
602            port_id: plan.port_id,
603            source,
604            default: plan.default,
605        });
606    }
607    pending
608}
609
610/// Compiled, allocation-free routing state produced by [`Patch::compile`].
611///
612/// All buffers are preallocated at compile time so [`Patch::tick`] performs no heap
613/// allocation. `out_buf` persists across ticks, which is what gives cycle-breaker
614/// (delay-style) modules their one-sample feedback: a downstream node not yet executed this
615/// tick still holds last tick's value.
616#[derive(Default)]
617struct Routing {
618    /// Per-node execution records, in topological (execution) order.
619    nodes: Vec<NodeExec>,
620    /// Dense storage for every node's output values (one slot per output port).
621    out_buf: Vec<f64>,
622    /// Maps an output [`PortRef`] to its dense slot in `out_buf` (for `get_output_value`
623    /// and compile-time edge resolution). Not touched on the hot path.
624    out_slot_index: StdMap<PortRef, usize>,
625    /// Precomputed `(left, right)` output slots for `read_output` (right = left when mono).
626    output_slots: Option<(usize, usize)>,
627    /// Reusable per-node input buffers (parallel to `nodes`), cleared and refilled per tick.
628    scratch_in: Vec<PortValues>,
629    /// Reusable per-node output buffers (parallel to `nodes`), cleared and written per tick.
630    scratch_out: Vec<PortValues>,
631}
632
633impl Routing {
634    /// Read the stereo output from the precomputed output slots (silence if uncompiled or
635    /// the patch has no output node).
636    fn read_output(&self) -> (f64, f64) {
637        match self.output_slots {
638            Some((left, right)) => (self.out_buf[left], self.out_buf[right]),
639            None => (0.0, 0.0),
640        }
641    }
642}
643
644/// The main patch graph containing modules and connections
645pub struct Patch {
646    nodes: SlotMap<NodeId, Node>,
647    /// Module storage, indexed by [`Node::module_slot`]. After a successful
648    /// [`compile`](Patch::compile) this is in execution order — element `i` is the module of
649    /// `routing.nodes[i]` — which is what lets [`tick_step`](Patch::tick_step) zip modules
650    /// and routing records together with no per-node lookup.
651    modules: Vec<Box<dyn GraphModule>>,
652    cables: Vec<Cable>,
653
654    /// Output ports that must be produced even when no cable reads them, so
655    /// [`get_output_value`](Patch::get_output_value) can meter them. Folded into
656    /// [`consumed_output_mask`](Patch::consumed_output_mask) at compile time. Empty for a
657    /// patch that never calls [`keep_output_live`](Patch::keep_output_live), which is why
658    /// the default masks (and therefore the rendered samples) are unaffected.
659    kept_live_outputs: Vec<PortRef>,
660
661    // Monotonic source of stable CableIds (never reused)
662    next_cable_id: CableId,
663
664    // Execution state
665    execution_order: Vec<NodeId>,
666    // Compiled, preallocated routing (dense buffers + adjacency). Rebuilt by compile().
667    routing: Routing,
668
669    // True when the graph has been mutated since the last successful compile().
670    // tick() checks this and recompiles lazily.
671    dirty: bool,
672    // Error from the most recent failed compile (auto or explicit), if any.
673    last_compile_error: Option<PatchError>,
674
675    // Configuration
676    sample_rate: f64,
677
678    // Output node
679    output_node: Option<NodeId>,
680
681    // Validation
682    validation_mode: ValidationMode,
683    warnings: Vec<String>,
684
685    // Human-facing metadata, preserved across to_def/from_def.
686    meta: PatchMeta,
687}
688
689impl Patch {
690    /// Create a new empty patch
691    pub fn new(sample_rate: f64) -> Self {
692        Self {
693            nodes: SlotMap::new(),
694            modules: Vec::new(),
695            cables: Vec::new(),
696            kept_live_outputs: Vec::new(),
697            next_cable_id: 0,
698            execution_order: Vec::new(),
699            routing: Routing::default(),
700            // A fresh patch is "dirty" so the first tick() compiles automatically even if
701            // the caller forgets to call compile().
702            dirty: true,
703            last_compile_error: None,
704            sample_rate,
705            output_node: None,
706            // Default is Warn (see ValidationMode): mismatched connections are flagged as
707            // warnings without blocking, matching the documented behavior.
708            validation_mode: ValidationMode::Warn,
709            warnings: Vec::new(),
710            meta: PatchMeta::default(),
711        }
712    }
713
714    /// Read the patch's editable metadata (name, author, description, tags).
715    pub fn meta(&self) -> &PatchMeta {
716        &self.meta
717    }
718
719    /// Mutable access to the patch metadata (see [`PatchMeta`]).
720    pub fn meta_mut(&mut self) -> &mut PatchMeta {
721        &mut self.meta
722    }
723
724    /// Replace the patch metadata wholesale.
725    pub fn set_meta(&mut self, meta: PatchMeta) {
726        self.meta = meta;
727    }
728
729    /// Set the signal validation mode
730    pub fn set_validation_mode(&mut self, mode: ValidationMode) {
731        self.validation_mode = mode;
732    }
733
734    /// Get the current validation mode
735    pub fn validation_mode(&self) -> ValidationMode {
736        self.validation_mode
737    }
738
739    /// Get all warnings generated during patching
740    pub fn warnings(&self) -> &[String] {
741        &self.warnings
742    }
743
744    /// Clear all warnings
745    pub fn clear_warnings(&mut self) {
746        self.warnings.clear();
747    }
748
749    /// Get the sample rate
750    pub fn sample_rate(&self) -> f64 {
751        self.sample_rate
752    }
753
754    /// Add a module to the patch
755    pub fn add<M: GraphModule + 'static>(
756        &mut self,
757        name: impl Into<String>,
758        module: M,
759    ) -> NodeHandle {
760        self.add_boxed(name, Box::new(module))
761    }
762
763    /// Add a boxed module to the patch
764    pub fn add_boxed(
765        &mut self,
766        name: impl Into<String>,
767        mut module: Box<dyn GraphModule>,
768    ) -> NodeHandle {
769        module.set_sample_rate(self.sample_rate);
770        let spec = module.port_spec().clone();
771        let module_slot = self.modules.len();
772        self.modules.push(module);
773        let id = self.nodes.insert(Node {
774            module_slot,
775            name: name.into(),
776            position: None,
777            param_overrides: StdMap::new(),
778        });
779        self.invalidate();
780        NodeHandle { id, spec }
781    }
782
783    /// Borrow a node's module.
784    fn module_of(&self, node: NodeId) -> Option<&dyn GraphModule> {
785        let slot = self.nodes.get(node)?.module_slot;
786        self.modules.get(slot).map(|m| m.as_ref())
787    }
788
789    /// Mutably borrow a node's module.
790    fn module_of_mut(&mut self, node: NodeId) -> Option<&mut (dyn GraphModule + '_)> {
791        let slot = self.nodes.get(node)?.module_slot;
792        Some(self.modules.get_mut(slot)?.as_mut())
793    }
794
795    /// Remove a module from the patch
796    pub fn remove(&mut self, node: NodeId) -> Result<(), PatchError> {
797        let Some(removed) = self.nodes.remove(node) else {
798            return Err(PatchError::InvalidNode { node });
799        };
800
801        // Close the gap in `modules` and slide every later node's slot down with it.
802        self.modules.remove(removed.module_slot);
803        for (_, other) in &mut self.nodes {
804            if other.module_slot > removed.module_slot {
805                other.module_slot -= 1;
806            }
807        }
808
809        // Remove all cables connected to this node
810        self.cables
811            .retain(|cable| cable.from.node != node && cable.to.node != node);
812
813        // Drop any keep-alive requests for ports that no longer exist. NodeIds are not
814        // reused by the slotmap, but leaving stale entries would grow the list without
815        // bound in a long-lived editor session.
816        self.kept_live_outputs.retain(|port| port.node != node);
817
818        if self.output_node == Some(node) {
819            self.output_node = None;
820        }
821
822        self.invalidate();
823        Ok(())
824    }
825
826    /// Force `port` to be produced on every sample even when no cable reads it.
827    ///
828    /// A module that opts into [`GraphModule::tick_masked`] (`Vco`, `Lfo`,
829    /// `NoiseGenerator`) is told at compile time which of its outputs the patch consumes
830    /// and may skip producing the rest, whose routing-buffer slots then keep their
831    /// initial `0.0`. That is invisible to the audio the patch renders, but it *is* visible
832    /// to [`get_output_value`](Self::get_output_value), and therefore to metering: a scope
833    /// or level meter on an unpatched `vco.sin` would read a flat zero.
834    ///
835    /// Marking a port live puts its bit back in the mask, so the module produces it exactly
836    /// as it would if a cable were attached — same code path, same values, bit for bit.
837    /// The port still feeds nothing, so the rendered output is unchanged; the only cost is
838    /// the skipped work being done again.
839    ///
840    /// Returns `true` if this newly marked the port (a repeat call is a no-op). Marking
841    /// marks the patch dirty, so the next [`tick`](Self::tick) recompiles with the wider
842    /// mask; call it outside the audio callback.
843    ///
844    /// `StateObserver::sync_output_keepalive` (with the `alloc` feature) applies this to
845    /// every port a subscription bus meters, which is what the WASM `Engine` uses to keep
846    /// `Engine.subscribe` working on unpatched ports.
847    ///
848    /// ```
849    /// use quiver::prelude::*;
850    ///
851    /// let mut patch = Patch::new(44100.0);
852    /// let vco = patch.add("vco", Vco::new(44100.0));
853    /// let out = patch.add("out", StereoOutput::new());
854    /// patch.connect(vco.out("saw"), out.in_("left")).unwrap();
855    /// patch.set_output(out.id());
856    ///
857    /// // `tri` (port 11) has no consumer, so it is not produced...
858    /// patch.compile().unwrap();
859    /// for _ in 0..64 {
860    ///     patch.tick();
861    /// }
862    /// assert_eq!(patch.get_output_value(vco.id(), 11), Some(0.0));
863    ///
864    /// // ...until it is kept live for metering.
865    /// assert!(patch.keep_output_live(vco.id(), 11));
866    /// patch.compile().unwrap();
867    /// let mut moved = false;
868    /// for _ in 0..64 {
869    ///     patch.tick();
870    ///     moved |= patch.get_output_value(vco.id(), 11) != Some(0.0);
871    /// }
872    /// assert!(moved);
873    /// ```
874    pub fn keep_output_live(&mut self, node: NodeId, port: PortId) -> bool {
875        let port_ref = PortRef { node, port };
876        if self.kept_live_outputs.contains(&port_ref) {
877            return false;
878        }
879        self.kept_live_outputs.push(port_ref);
880        // Only the compiled mask changes, so the existing routing stays readable until the
881        // next compile; no need for the buffer-dropping `invalidate`.
882        self.dirty = true;
883        true
884    }
885
886    /// Undo a [`keep_output_live`](Self::keep_output_live). Returns `true` if the port was
887    /// marked. Marks the patch dirty when it was.
888    pub fn release_output_live(&mut self, node: NodeId, port: PortId) -> bool {
889        let port_ref = PortRef { node, port };
890        let before = self.kept_live_outputs.len();
891        self.kept_live_outputs.retain(|kept| *kept != port_ref);
892        let removed = self.kept_live_outputs.len() != before;
893        if removed {
894            self.dirty = true;
895        }
896        removed
897    }
898
899    /// Drop every [`keep_output_live`](Self::keep_output_live) request. Returns `true` if
900    /// there was anything to drop. Marks the patch dirty when there was.
901    pub fn clear_kept_live_outputs(&mut self) -> bool {
902        if self.kept_live_outputs.is_empty() {
903            return false;
904        }
905        self.kept_live_outputs.clear();
906        self.dirty = true;
907        true
908    }
909
910    /// The output ports currently held live by [`keep_output_live`](Self::keep_output_live),
911    /// in the order they were marked.
912    pub fn kept_live_outputs(&self) -> &[PortRef] {
913        &self.kept_live_outputs
914    }
915
916    /// Allocate the next stable cable id.
917    fn alloc_cable_id(&mut self) -> CableId {
918        let id = self.next_cable_id;
919        self.next_cable_id += 1;
920        id
921    }
922
923    /// Connect an output port to an input port.
924    ///
925    /// Returns a stable [`CableId`] that remains valid for [`disconnect`](Self::disconnect)
926    /// even after other cables are removed.
927    pub fn connect(&mut self, from: PortRef, to: PortRef) -> Result<CableId, PatchError> {
928        self.validate_output_port(from)?;
929        self.validate_input_port(to)?;
930        self.validate_signal_compatibility(from, to)?;
931
932        let id = self.alloc_cable_id();
933        self.cables.push(Cable {
934            id,
935            from,
936            to,
937            attenuation: None,
938            offset: None,
939        });
940        self.invalidate();
941        Ok(id)
942    }
943
944    /// Connect with attenuation (0.0-1.0 range for backwards compatibility)
945    pub fn connect_attenuated(
946        &mut self,
947        from: PortRef,
948        to: PortRef,
949        attenuation: f64,
950    ) -> Result<CableId, PatchError> {
951        self.validate_output_port(from)?;
952        self.validate_input_port(to)?;
953        self.validate_signal_compatibility(from, to)?;
954
955        let id = self.alloc_cable_id();
956        self.cables.push(Cable {
957            id,
958            from,
959            to,
960            attenuation: Some(attenuation.clamp(0.0, 1.0)),
961            offset: None,
962        });
963        self.invalidate();
964        Ok(id)
965    }
966
967    /// Connect with full modulation controls (attenuverter and offset)
968    /// attenuation: -2.0 to 2.0 (negative inverts, >1.0 amplifies)
969    /// offset: -10.0 to 10.0V DC offset added after attenuation
970    pub fn connect_modulated(
971        &mut self,
972        from: PortRef,
973        to: PortRef,
974        attenuation: f64,
975        offset: f64,
976    ) -> Result<CableId, PatchError> {
977        self.validate_output_port(from)?;
978        self.validate_input_port(to)?;
979        self.validate_signal_compatibility(from, to)?;
980
981        let id = self.alloc_cable_id();
982        self.cables.push(Cable {
983            id,
984            from,
985            to,
986            attenuation: Some(attenuation.clamp(-2.0, 2.0)),
987            offset: Some(offset.clamp(-10.0, 10.0)),
988        });
989        self.invalidate();
990        Ok(id)
991    }
992
993    /// Validate signal kind compatibility between ports
994    fn validate_signal_compatibility(
995        &mut self,
996        from: PortRef,
997        to: PortRef,
998    ) -> Result<(), PatchError> {
999        if self.validation_mode == ValidationMode::None {
1000            return Ok(());
1001        }
1002
1003        // Get the signal kinds for both ports
1004        let from_kind = self.get_output_port_kind(from);
1005        let to_kind = self.get_input_port_kind(to);
1006
1007        if let (Some(from_kind), Some(to_kind)) = (from_kind, to_kind) {
1008            let result = from_kind.is_compatible_with(&to_kind);
1009
1010            if let Some(warning) = result.warning {
1011                let from_name = self.get_name(from.node).unwrap_or("unknown");
1012                let to_name = self.get_name(to.node).unwrap_or("unknown");
1013                let full_warning = format!(
1014                    "{}.{} -> {}.{}: {}",
1015                    from_name, from.port, to_name, to.port, warning
1016                );
1017
1018                match self.validation_mode {
1019                    ValidationMode::Warn => {
1020                        self.warnings.push(full_warning);
1021                    }
1022                    ValidationMode::Strict => {
1023                        return Err(PatchError::SignalMismatch {
1024                            from_kind,
1025                            to_kind,
1026                            message: warning,
1027                        });
1028                    }
1029                    ValidationMode::None => {}
1030                }
1031            }
1032        }
1033
1034        Ok(())
1035    }
1036
1037    /// Get the signal kind for an output port
1038    fn get_output_port_kind(&self, port_ref: PortRef) -> Option<SignalKind> {
1039        self.module_of(port_ref.node)?
1040            .port_spec()
1041            .outputs
1042            .iter()
1043            .find(|p| p.id == port_ref.port)
1044            .map(|p| p.kind)
1045    }
1046
1047    /// Get the signal kind for an input port
1048    fn get_input_port_kind(&self, port_ref: PortRef) -> Option<SignalKind> {
1049        self.module_of(port_ref.node)?
1050            .port_spec()
1051            .inputs
1052            .iter()
1053            .find(|p| p.id == port_ref.port)
1054            .map(|p| p.kind)
1055    }
1056
1057    /// Connect one output to multiple inputs (mult)
1058    pub fn mult(&mut self, from: PortRef, to: &[PortRef]) -> Result<Vec<CableId>, PatchError> {
1059        to.iter().map(|&dest| self.connect(from, dest)).collect()
1060    }
1061
1062    /// Disconnect a cable by its stable [`CableId`].
1063    ///
1064    /// Scans for the cable whose id matches (patch cable counts are small), so previously
1065    /// returned ids stay valid regardless of how many other cables have been removed.
1066    pub fn disconnect(&mut self, cable_id: CableId) -> Result<(), PatchError> {
1067        let idx = self
1068            .cables
1069            .iter()
1070            .position(|c| c.id == cable_id)
1071            .ok_or(PatchError::InvalidCable)?;
1072        self.cables.remove(idx);
1073        self.invalidate();
1074        Ok(())
1075    }
1076
1077    /// Set the output node for the patch (infallible convenience).
1078    ///
1079    /// Marks the patch dirty so the next [`tick`](Self::tick) reflects the new routing. If
1080    /// `node` is invalid or exposes no output ports, [`tick`](Self::tick) simply reads
1081    /// silence; use [`try_set_output`](Self::try_set_output) for a validated, fallible
1082    /// alternative.
1083    pub fn set_output(&mut self, node: NodeId) {
1084        self.output_node = Some(node);
1085        self.dirty = true;
1086    }
1087
1088    /// The node currently designated as the patch's stereo output, if any.
1089    pub fn output_node(&self) -> Option<NodeId> {
1090        self.output_node
1091    }
1092
1093    /// Set the output node, validating that it exists and exposes at least one output port.
1094    ///
1095    /// The checked companion to [`set_output`](Self::set_output), for callers (e.g. GUIs
1096    /// or loaders) that prefer a `Result` over silent misrouting.
1097    pub fn try_set_output(&mut self, node: NodeId) -> Result<(), PatchError> {
1098        let module = self
1099            .module_of(node)
1100            .ok_or(PatchError::InvalidNode { node })?;
1101        if module.port_spec().outputs.is_empty() {
1102            return Err(PatchError::InvalidPort {
1103                node,
1104                name: None,
1105                port: None,
1106                available: Vec::new(),
1107            });
1108        }
1109        self.output_node = Some(node);
1110        self.dirty = true;
1111        Ok(())
1112    }
1113
1114    /// Set a parameter on a module
1115    pub fn set_param(&mut self, node: NodeId, param: ParamId, value: f64) {
1116        if let Some(module) = self.module_of_mut(node) {
1117            module.set_param(param, value);
1118        }
1119    }
1120
1121    /// Get a parameter value from a module
1122    pub fn get_param(&self, node: NodeId, param: ParamId) -> Option<f64> {
1123        self.module_of(node).and_then(|m| m.get_param(param))
1124    }
1125
1126    /// Set module position (for UI)
1127    pub fn set_position(&mut self, node: NodeId, position: (f32, f32)) {
1128        if let Some(n) = self.nodes.get_mut(node) {
1129            n.position = Some(position);
1130        }
1131    }
1132
1133    /// Get module position (for UI/serialization)
1134    pub fn get_position(&self, node: NodeId) -> Option<(f32, f32)> {
1135        self.nodes.get(node).and_then(|n| n.position)
1136    }
1137
1138    /// Get module name
1139    pub fn get_name(&self, node: NodeId) -> Option<&str> {
1140        self.nodes.get(node).map(|n| n.name.as_str())
1141    }
1142
1143    /// Get number of nodes
1144    pub fn node_count(&self) -> usize {
1145        self.nodes.len()
1146    }
1147
1148    /// Get number of cables
1149    pub fn cable_count(&self) -> usize {
1150        self.cables.len()
1151    }
1152
1153    /// Get all cables
1154    pub fn cables(&self) -> &[Cable] {
1155        &self.cables
1156    }
1157
1158    /// Get execution order (after compile)
1159    pub fn execution_order(&self) -> &[NodeId] {
1160        &self.execution_order
1161    }
1162
1163    /// Mark the compiled schedule stale and drop stale output buffers.
1164    ///
1165    /// Called after every structural mutation. Clearing `buffers` here guarantees that a
1166    /// read after a mutation (before the next recompile) cannot leak the previous graph's
1167    /// last-tick values; `tick()` recompiles lazily via the `dirty` flag.
1168    fn invalidate(&mut self) {
1169        self.execution_order.clear();
1170        self.routing = Routing::default();
1171        self.dirty = true;
1172    }
1173
1174    fn validate_output_port(&self, port_ref: PortRef) -> Result<(), PatchError> {
1175        let spec = self
1176            .module_of(port_ref.node)
1177            .ok_or(PatchError::InvalidNode {
1178                node: port_ref.node,
1179            })?
1180            .port_spec();
1181        if spec.outputs.iter().any(|p| p.id == port_ref.port) {
1182            Ok(())
1183        } else {
1184            Err(PatchError::InvalidPort {
1185                node: port_ref.node,
1186                name: None,
1187                port: Some(port_ref.port),
1188                available: spec.outputs.iter().map(|p| p.name.clone()).collect(),
1189            })
1190        }
1191    }
1192
1193    fn validate_input_port(&self, port_ref: PortRef) -> Result<(), PatchError> {
1194        let spec = self
1195            .module_of(port_ref.node)
1196            .ok_or(PatchError::InvalidNode {
1197                node: port_ref.node,
1198            })?
1199            .port_spec();
1200        if spec.inputs.iter().any(|p| p.id == port_ref.port) {
1201            Ok(())
1202        } else {
1203            Err(PatchError::InvalidPort {
1204                node: port_ref.node,
1205                name: None,
1206                port: Some(port_ref.port),
1207                available: spec.inputs.iter().map(|p| p.name.clone()).collect(),
1208            })
1209        }
1210    }
1211
1212    /// Compile the patch into an executable order.
1213    ///
1214    /// On success clears the dirty flag and any previous compile error. On failure
1215    /// (e.g. an unbroken feedback cycle) the stale schedule and buffers are dropped so a
1216    /// subsequent [`tick`](Self::tick) outputs silence, the error is stored (retrievable
1217    /// via [`last_compile_error`](Self::last_compile_error)), and the same error is
1218    /// returned.
1219    pub fn compile(&mut self) -> Result<(), PatchError> {
1220        let order = match self.topological_sort() {
1221            Ok(order) => order,
1222            Err(e) => {
1223                self.execution_order.clear();
1224                self.routing = Routing::default();
1225                // Do not stay dirty: avoid re-running a known-failing sort every tick.
1226                // A later structural mutation re-sets dirty via invalidate().
1227                self.dirty = false;
1228                self.last_compile_error = Some(e.clone());
1229                return Err(e);
1230            }
1231        };
1232        self.execution_order = order;
1233
1234        // Put the modules in execution order so the per-sample loop can walk them in
1235        // lockstep with the routing plan. Must precede build_routing, which reads modules
1236        // through the (now updated) node slots.
1237        self.reorder_modules_for_execution();
1238
1239        // Build the dense, preallocated routing plan (adjacency + buffers).
1240        self.build_routing();
1241
1242        self.dirty = false;
1243        self.last_compile_error = None;
1244        Ok(())
1245    }
1246
1247    /// Which of `node`'s outputs (in [`PortSpec`] order) anything in the patch reads.
1248    ///
1249    /// An output is consumed when a cable leaves it, when it has been pinned by
1250    /// [`keep_output_live`](Self::keep_output_live), or when it belongs to the patch's
1251    /// output node — [`Routing::read_output`] reads that node's first two output slots, and
1252    /// the whole spec is marked rather than just those two so a later change to
1253    /// `set_output` semantics cannot silently blank a port. Everything else is dead for this
1254    /// compile, and is reported to the module through [`GraphModule::tick_masked`].
1255    ///
1256    /// A module with more than 32 outputs gets an all-ones mask, since the bit vector cannot
1257    /// address the rest.
1258    ///
1259    /// A dead output's [`Routing::out_buf`] slot then keeps whatever it last held, so
1260    /// [`get_output_value`](Self::get_output_value) on an *unpatched* output of a mask-aware
1261    /// module (`Vco`, `Lfo`, `NoiseGenerator`) reports its initial `0.0` rather than a live
1262    /// sample. Patch the port, pin it with [`keep_output_live`](Self::keep_output_live), or
1263    /// tick the module directly if you need to meter it.
1264    fn consumed_output_mask(&self, node: NodeId, out_ids: &[PortId]) -> u32 {
1265        if out_ids.len() > u32::BITS as usize || self.output_node == Some(node) {
1266            return u32::MAX;
1267        }
1268        let mut wanted = 0u32;
1269        for (k, &port) in out_ids.iter().enumerate() {
1270            let port_ref = PortRef { node, port };
1271            let consumed = self.cables.iter().any(|cable| cable.from == port_ref)
1272                || self.kept_live_outputs.contains(&port_ref);
1273            if consumed {
1274                wanted |= 1 << k;
1275            }
1276        }
1277        wanted
1278    }
1279
1280    /// Permute `modules` into `execution_order`, rewriting each node's `module_slot`.
1281    ///
1282    /// This is what makes `modules[i]` the module of `routing.nodes[i]`, so `tick_step` can
1283    /// zip the two instead of resolving a slotmap key per node per sample. The topological
1284    /// sort visits every live node exactly once, so the permutation is total; anything not
1285    /// named by it (which cannot happen for a successful sort) is appended afterwards so no
1286    /// module is ever dropped.
1287    fn reorder_modules_for_execution(&mut self) {
1288        let mut taken: Vec<Option<Box<dyn GraphModule>>> = core::mem::take(&mut self.modules)
1289            .into_iter()
1290            .map(Some)
1291            .collect();
1292        let mut old_to_new: Vec<Option<usize>> = taken.iter().map(|_| None).collect();
1293        let mut ordered: Vec<Box<dyn GraphModule>> = Vec::with_capacity(taken.len());
1294
1295        for &node_id in &self.execution_order {
1296            let Some(node) = self.nodes.get(node_id) else {
1297                continue;
1298            };
1299            let old = node.module_slot;
1300            let Some(module) = taken.get_mut(old).and_then(Option::take) else {
1301                continue;
1302            };
1303            old_to_new[old] = Some(ordered.len());
1304            ordered.push(module);
1305        }
1306        // Defensive: keep any module the order did not name (impossible after a successful
1307        // topological sort, which covers every live node) rather than dropping it.
1308        for (old, slot) in taken.iter_mut().enumerate() {
1309            if let Some(module) = slot.take() {
1310                old_to_new[old] = Some(ordered.len());
1311                ordered.push(module);
1312            }
1313        }
1314
1315        self.modules = ordered;
1316        // Remap by old slot, so every node follows its own module wherever it landed.
1317        for (_, node) in &mut self.nodes {
1318            if let Some(new) = old_to_new.get(node.module_slot).copied().flatten() {
1319                node.module_slot = new;
1320            }
1321        }
1322    }
1323
1324    /// Build the compiled [`Routing`] from the current `execution_order`, cables and output
1325    /// node. Runs only at compile time; all per-tick buffers are preallocated here so that
1326    /// [`tick`](Self::tick) never allocates.
1327    fn build_routing(&mut self) {
1328        let mut routing = Routing::default();
1329
1330        // Pass A: assign a dense output slot to every node's output ports (in PortSpec
1331        // order) and record each node's output base/ids. Slot order is stable and used by
1332        // both edge resolution and get_output_value.
1333        let mut slot: usize = 0;
1334        for &node_id in &self.execution_order {
1335            let spec = self
1336                .module_of(node_id)
1337                .expect("execution_order only holds live nodes")
1338                .port_spec();
1339            let out_base = slot;
1340            let mut out_ids = Vec::with_capacity(spec.outputs.len());
1341            for output in &spec.outputs {
1342                routing.out_slot_index.insert(
1343                    PortRef {
1344                        node: node_id,
1345                        port: output.id,
1346                    },
1347                    slot,
1348                );
1349                out_ids.push(output.id);
1350                slot += 1;
1351            }
1352            routing.nodes.push(NodeExec {
1353                out_base,
1354                out_ids,
1355                inputs: Vec::new(),
1356                normalled_pending: Vec::new(),
1357                wanted_outputs: u32::MAX,
1358            });
1359        }
1360        routing.out_buf.resize(slot, 0.0);
1361
1362        // Pass B: resolve each input's incoming cables into dense edges and preallocate the
1363        // reusable scratch buffers (keys inserted here so the hot path never grows them).
1364        for (exec_idx, &node_id) in self.execution_order.iter().enumerate() {
1365            let node = self
1366                .nodes
1367                .get(node_id)
1368                .expect("execution_order only holds live nodes");
1369            let spec = self.modules[node.module_slot].port_spec();
1370
1371            let mut scratch_in = PortValues::new();
1372            let mut scratch_out = PortValues::new();
1373            let mut inputs = Vec::with_capacity(spec.inputs.len());
1374
1375            for input in &spec.inputs {
1376                let port_ref = PortRef {
1377                    node: node_id,
1378                    port: input.id,
1379                };
1380                let mut edges = Vec::new();
1381                let mut has_connection = false;
1382                for cable in &self.cables {
1383                    if cable.to == port_ref {
1384                        has_connection = true;
1385                        // A validated cable's source output always has a slot; guard anyway.
1386                        if let Some(&src_slot) = routing.out_slot_index.get(&cable.from) {
1387                            edges.push(InEdge {
1388                                src_slot,
1389                                attenuation: cable.attenuation.unwrap_or(1.0),
1390                                offset: cable.offset.unwrap_or(0.0),
1391                            });
1392                        }
1393                    }
1394                }
1395                // A per-node parameter override (set via `set_param_by_id`) replaces the
1396                // spec's static default for this unpatched control input. Baked in here at
1397                // compile time so the zero-alloc tick path is untouched.
1398                let default = node
1399                    .param_overrides
1400                    .get(&input.name)
1401                    .copied()
1402                    .unwrap_or(input.default);
1403                inputs.push(InputPlan {
1404                    port_id: input.id,
1405                    default,
1406                    normalled_to: input.normalled_to,
1407                    has_connection,
1408                    edges,
1409                });
1410                scratch_in.set(input.id, 0.0);
1411            }
1412            // Collapse each normalled chain to its pass-1-resolvable terminal so the
1413            // runtime two-pass `gather` is order-independent (see the fn's docs).
1414            resolve_normalled_chains(&mut inputs);
1415            let normalled_pending = collect_normalled_pending(&inputs);
1416            for output in &spec.outputs {
1417                scratch_out.set(output.id, 0.0);
1418            }
1419
1420            routing.nodes[exec_idx].wanted_outputs =
1421                self.consumed_output_mask(node_id, &routing.nodes[exec_idx].out_ids);
1422            routing.nodes[exec_idx].normalled_pending = normalled_pending;
1423            routing.nodes[exec_idx].inputs = inputs;
1424            routing.scratch_in.push(scratch_in);
1425            routing.scratch_out.push(scratch_out);
1426        }
1427
1428        // Pass C: precompute the stereo output read slots (right = left when mono).
1429        routing.output_slots = self.output_node.and_then(|out_node| {
1430            let outputs = &self.module_of(out_node)?.port_spec().outputs;
1431            let left_id = outputs.first()?.id;
1432            let left = *routing.out_slot_index.get(&PortRef {
1433                node: out_node,
1434                port: left_id,
1435            })?;
1436            let right = outputs
1437                .get(1)
1438                .and_then(|p| {
1439                    routing
1440                        .out_slot_index
1441                        .get(&PortRef {
1442                            node: out_node,
1443                            port: p.id,
1444                        })
1445                        .copied()
1446                })
1447                .unwrap_or(left);
1448            Some((left, right))
1449        });
1450
1451        self.routing = routing;
1452    }
1453
1454    /// Whether the module at `node` is a feedback cycle-breaker (delay-style).
1455    fn node_breaks_feedback(&self, node: NodeId) -> bool {
1456        self.module_of(node)
1457            .map(|m| m.breaks_feedback_cycle())
1458            .unwrap_or(false)
1459    }
1460
1461    fn topological_sort(&self) -> Result<Vec<NodeId>, PatchError> {
1462        let mut in_degree: StdMap<NodeId, usize> = self.nodes.keys().map(|k| (k, 0)).collect();
1463        let mut successors: StdMap<NodeId, Vec<NodeId>> =
1464            self.nodes.keys().map(|k| (k, Vec::new())).collect();
1465
1466        for cable in &self.cables {
1467            // Feedback support: exclude edges feeding INTO a cycle-breaker (delay) node.
1468            // Such a node is scheduled without waiting for its upstream producers and, at
1469            // runtime, reads their previous-tick output buffers — a one-sample feedback
1470            // delay. This lets loops routed through a UnitDelay/DelayLine compile while
1471            // genuine breakerless cycles are still rejected below.
1472            if self.node_breaks_feedback(cable.to.node) {
1473                continue;
1474            }
1475            if let Some(deg) = in_degree.get_mut(&cable.to.node) {
1476                *deg += 1;
1477            }
1478            if let Some(succ) = successors.get_mut(&cable.from.node) {
1479                succ.push(cable.to.node);
1480            }
1481        }
1482
1483        // Kahn's algorithm, seeded in deterministic slotmap (insertion) order so the
1484        // resulting execution_order is reproducible across runs/builds (no HashMap
1485        // iteration order dependence).
1486        let mut queue: VecDeque<NodeId> = VecDeque::new();
1487        for id in self.nodes.keys() {
1488            if in_degree.get(&id).copied().unwrap_or(0) == 0 {
1489                queue.push_back(id);
1490            }
1491        }
1492
1493        let mut result = Vec::with_capacity(self.nodes.len());
1494
1495        while let Some(node) = queue.pop_front() {
1496            result.push(node);
1497            // Successor lists are built in cable order (a Vec), keeping this deterministic.
1498            if let Some(succ) = successors.get(&node) {
1499                for &s in succ {
1500                    if let Some(deg) = in_degree.get_mut(&s) {
1501                        *deg -= 1;
1502                        if *deg == 0 {
1503                            queue.push_back(s);
1504                        }
1505                    }
1506                }
1507            }
1508        }
1509
1510        if result.len() != self.nodes.len() {
1511            // Collect stuck nodes deterministically and capture their names for Display.
1512            let nodes: Vec<NodeId> = self
1513                .nodes
1514                .keys()
1515                .filter(|k| in_degree.get(k).copied().unwrap_or(0) > 0)
1516                .collect();
1517            let names = nodes
1518                .iter()
1519                .map(|&id| self.get_name(id).unwrap_or("<unknown>").to_string())
1520                .collect();
1521            return Err(PatchError::CycleDetected { nodes, names });
1522        }
1523
1524        Ok(result)
1525    }
1526
1527    /// The error from the most recent failed compile (auto or explicit), if any.
1528    ///
1529    /// Cleared by the next successful [`compile`](Self::compile) or [`tick`](Self::tick).
1530    /// After [`tick`](Self::tick) unexpectedly returns silence, check this to learn why the
1531    /// graph did not compile (e.g. [`PatchError::CycleDetected`]).
1532    pub fn last_compile_error(&self) -> Option<&PatchError> {
1533        self.last_compile_error.as_ref()
1534    }
1535
1536    /// Process a single sample, returning stereo output.
1537    ///
1538    /// # Lazy (re)compilation
1539    ///
1540    /// `tick` is self-healing. Every structural mutation
1541    /// (`add`/`connect`/`disconnect`/`remove`/`set_output`) marks the patch dirty; `tick`
1542    /// detects this and recompiles automatically before processing, so the output always
1543    /// reflects the *current* graph — you never have to remember to call
1544    /// [`compile`](Self::compile) again after an edit, and a `tick` before the first
1545    /// `compile` works too.
1546    ///
1547    /// If the automatic recompile fails (for example a mutation introduced a feedback
1548    /// cycle with no delay to break it), `tick` outputs silence `(0.0, 0.0)` and the error
1549    /// is retained in [`last_compile_error`](Self::last_compile_error). A patch with no
1550    /// output node, or an empty graph, likewise ticks to silence.
1551    pub fn tick(&mut self) -> (f64, f64) {
1552        // Lazily (re)compile if the graph was mutated since the last compile. On failure
1553        // compile() records last_compile_error and leaves an empty schedule, so the step
1554        // below is a no-op and we fall through to silence.
1555        if self.dirty {
1556            let _ = self.compile();
1557        }
1558        self.tick_step()
1559    }
1560
1561    /// Process a block of samples into stereo `out_left`/`out_right` slices with **no
1562    /// per-frame heap allocation**.
1563    ///
1564    /// This is the allocation-free block entry point (in contrast to the default
1565    /// [`GraphModule::process_block`], which builds a fresh [`PortValues`] per frame). It
1566    /// (re)compiles once if needed, then drives the same per-sample engine over
1567    /// `n = out_left.len().min(out_right.len())` frames, reusing the preallocated routing
1568    /// buffers across every frame. Full SIMD-vectorized block execution is not performed;
1569    /// the guarantee here is zero allocation, not vectorization.
1570    pub fn tick_block(&mut self, out_left: &mut [f64], out_right: &mut [f64]) {
1571        if self.dirty {
1572            let _ = self.compile();
1573        }
1574        let frames = out_left.len().min(out_right.len());
1575        for frame in 0..frames {
1576            let (left, right) = self.tick_step();
1577            out_left[frame] = left;
1578            out_right[frame] = right;
1579        }
1580    }
1581
1582    /// Execute one sample of the already-compiled schedule (no dirty/recompile check).
1583    ///
1584    /// Allocation-free, and lookup-free: a successful `compile` leaves `modules` permuted
1585    /// into execution order, so the plan, the modules and the two scratch buffers are four
1586    /// parallel vectors walked by one zipped iterator — no slotmap key resolution, and no
1587    /// re-indexing, per node per sample. `modules` and `routing` are disjoint fields of
1588    /// `self`, so the module borrow and the routing-buffer borrows coexist without conflict.
1589    fn tick_step(&mut self) -> (f64, f64) {
1590        let Routing {
1591            nodes,
1592            out_buf,
1593            scratch_in,
1594            scratch_out,
1595            ..
1596        } = &mut self.routing;
1597        let modules = &mut self.modules;
1598
1599        // Lockstep is established by compile(). An uncompiled or failed compile leaves the
1600        // plan empty, which this guard turns into silence rather than a mispaired walk.
1601        if nodes.len() != modules.len() {
1602            debug_assert!(nodes.is_empty(), "routing plan is out of step with modules");
1603            return (0.0, 0.0);
1604        }
1605
1606        for (((exec, module), inputs), outputs) in nodes
1607            .iter()
1608            .zip(modules.iter_mut())
1609            .zip(scratch_in.iter_mut())
1610            .zip(scratch_out.iter_mut())
1611        {
1612            // Gather this node's inputs from the dense output buffer via precompiled edges.
1613            exec.gather(out_buf, inputs);
1614
1615            // Run the module. scratch_out is cleared first so unwritten outputs are absent,
1616            // matching the previous "fresh PortValues per tick" semantics.
1617            outputs.clear();
1618            module.tick_masked(inputs, outputs, exec.wanted_outputs);
1619
1620            // Scatter outputs back into the dense buffer (with denormal flushing).
1621            exec.scatter(outputs, out_buf);
1622        }
1623
1624        self.routing.read_output()
1625    }
1626
1627    /// Reset all modules in the patch
1628    pub fn reset(&mut self) {
1629        for module in &mut self.modules {
1630            module.reset();
1631        }
1632        for value in self.routing.out_buf.iter_mut() {
1633            *value = 0.0;
1634        }
1635    }
1636
1637    /// Iterate over all nodes
1638    pub fn nodes(&self) -> impl Iterator<Item = (NodeId, &str, &dyn GraphModule)> {
1639        self.nodes.iter().map(|(id, node)| {
1640            (
1641                id,
1642                node.name.as_str(),
1643                self.modules[node.module_slot].as_ref(),
1644            )
1645        })
1646    }
1647
1648    /// Get a NodeId by module name
1649    pub fn get_node_id_by_name(&self, name: &str) -> Option<NodeId> {
1650        self.nodes
1651            .iter()
1652            .find(|(_, node)| node.name == name)
1653            .map(|(id, _)| id)
1654    }
1655
1656    /// Get a NodeHandle by module name
1657    pub fn get_handle_by_name(&self, name: &str) -> Option<NodeHandle> {
1658        self.nodes
1659            .iter()
1660            .find(|(_, node)| node.name == name)
1661            .map(|(id, node)| NodeHandle::from_module(id, self.modules[node.module_slot].as_ref()))
1662    }
1663
1664    /// Disconnect a cable by finding matching port refs
1665    pub fn disconnect_ports(&mut self, from: PortRef, to: PortRef) -> Result<(), PatchError> {
1666        let idx = self
1667            .cables
1668            .iter()
1669            .position(|c| c.from == from && c.to == to)
1670            .ok_or(PatchError::InvalidCable)?;
1671
1672        self.cables.remove(idx);
1673        self.invalidate();
1674        Ok(())
1675    }
1676
1677    /// Get all module names
1678    pub fn module_names(&self) -> Vec<&str> {
1679        self.nodes
1680            .iter()
1681            .map(|(_, node)| node.name.as_str())
1682            .collect()
1683    }
1684
1685    /// Get the current output buffer value for a specific port
1686    ///
1687    /// This is used by the observer to collect real-time values for metering,
1688    /// scope display, and other visualizations.
1689    ///
1690    /// # Unpatched ports of mask-aware modules
1691    ///
1692    /// A module that implements [`GraphModule::tick_masked`] (`Vco`, `Lfo`,
1693    /// `NoiseGenerator`) is told which of its outputs the compiled patch reads and may skip
1694    /// producing the rest, whose routing slots then keep their initial `0.0`. Metering such
1695    /// a port therefore requires that something in the patch consume it: patch it
1696    /// somewhere, or pin it with [`keep_output_live`](Self::keep_output_live), which forces
1697    /// the module to produce it for exactly this reason. Every port of every other module,
1698    /// and every *patched* port, reports live values as before.
1699    ///
1700    /// `StateObserver::sync_output_keepalive` (with the `alloc` feature) does the pinning
1701    /// for a whole subscription bus in one call.
1702    pub fn get_output_value(&self, node: NodeId, port: PortId) -> Option<f64> {
1703        self.routing
1704            .out_slot_index
1705            .get(&PortRef { node, port })
1706            .map(|&slot| self.routing.out_buf[slot])
1707    }
1708
1709    /// Get the signal kind for an output port by node ID and port ID
1710    pub fn get_output_signal_kind(&self, node: NodeId, port: PortId) -> Option<SignalKind> {
1711        self.module_of(node)?
1712            .port_spec()
1713            .outputs
1714            .iter()
1715            .find(|p| p.id == port)
1716            .map(|p| p.kind)
1717    }
1718}
1719
1720/// A control input is one whose base value behaves as a UI-settable "knob": everything
1721/// except raw [`SignalKind::Audio`] carriers (which are meant to be patched, not dialed).
1722#[cfg(feature = "alloc")]
1723fn is_control_input(kind: SignalKind) -> bool {
1724    kind != SignalKind::Audio
1725}
1726
1727/// Synthesize a [`ParamInfo`](crate::introspection::ParamInfo) for a control-input port,
1728/// using the port's signal range for bounds and the supplied effective value.
1729#[cfg(feature = "alloc")]
1730fn port_param_info(port: &crate::port::PortDef, value: f64) -> crate::introspection::ParamInfo {
1731    let (min, max) = port.kind.voltage_range();
1732    crate::introspection::ParamInfo::new(port.name.clone(), port.name.clone())
1733        .with_range(min, max)
1734        .with_default(port.default)
1735        .with_value(value)
1736}
1737
1738/// Introspection / parameter dispatch for a live patch (alloc tier).
1739///
1740/// A node's parameters come from two places, unified here:
1741/// * **Control-input ports** — any non-audio input. Its base (unpatched) value is a knob,
1742///   overridable per node; the override is applied at [`compile`](Self::compile).
1743/// * **Internal state** — parameters that are *not* ports (waveform tables, scales, oversample
1744///   factor, …), reached through the module's [`ModuleIntrospection`](crate::introspection::ModuleIntrospection) via the
1745///   [`introspect`](crate::port::GraphModule::introspect) hook.
1746///
1747/// Port parameters take precedence when an id names both, because in a compiled graph the
1748/// module reads the injected port value, not any mirrored internal field.
1749#[cfg(feature = "alloc")]
1750impl Patch {
1751    /// All UI-exposable parameters for a node.
1752    ///
1753    /// Returns internal-state parameters (from `ModuleIntrospection`, minus any shadowed by a
1754    /// same-named port) followed by one entry per control-input port with its current
1755    /// effective value. Empty if `node` is unknown.
1756    pub fn param_infos(&self, node: NodeId) -> Vec<crate::introspection::ParamInfo> {
1757        let Some(n) = self.nodes.get(node) else {
1758            return Vec::new();
1759        };
1760        let module = self.modules[n.module_slot].as_ref();
1761        let spec = module.port_spec();
1762        let mut infos: Vec<crate::introspection::ParamInfo> = module
1763            .introspect()
1764            .map(|i| i.param_infos())
1765            .unwrap_or_default()
1766            .into_iter()
1767            // Drop internal params shadowed by a real port of the same id (the port wins).
1768            .filter(|p| spec.input_by_name(&p.id).is_none())
1769            .collect();
1770
1771        for input in &spec.inputs {
1772            if !is_control_input(input.kind) {
1773                continue;
1774            }
1775            let value = n
1776                .param_overrides
1777                .get(&input.name)
1778                .copied()
1779                .unwrap_or(input.default);
1780            infos.push(port_param_info(input, value));
1781        }
1782        infos
1783    }
1784
1785    /// Read a single parameter's current value by id (port name or internal param id).
1786    pub fn get_param_by_id(&self, node: NodeId, id: &str) -> Option<f64> {
1787        let n = self.nodes.get(node)?;
1788        let module = self.modules[n.module_slot].as_ref();
1789        // Port parameters are authoritative.
1790        if let Some(port) = module.port_spec().input_by_name(id) {
1791            if is_control_input(port.kind) {
1792                return Some(n.param_overrides.get(id).copied().unwrap_or(port.default));
1793            }
1794        }
1795        module
1796            .introspect()
1797            .and_then(|i| i.get_param_info(id))
1798            .map(|p| p.value)
1799    }
1800
1801    /// Set a parameter by id. Returns `true` if the id was recognized.
1802    ///
1803    /// A control-input port id sets a per-node base-value override (and marks the graph for
1804    /// recompile so the next tick observes it). Otherwise the module's `ModuleIntrospection`
1805    /// is asked to set internal state.
1806    pub fn set_param_by_id(&mut self, node: NodeId, id: &str, value: f64) -> bool {
1807        // Decide the routing without holding a mutable borrow across the invalidate() call.
1808        let is_port = self
1809            .module_of(node)
1810            .and_then(|m| m.port_spec().input_by_name(id))
1811            .map(|p| is_control_input(p.kind))
1812            .unwrap_or(false);
1813
1814        if is_port {
1815            if let Some(n) = self.nodes.get_mut(node) {
1816                n.param_overrides.insert(id.to_string(), value);
1817            }
1818            // The override is baked into InputPlan defaults at compile time.
1819            self.invalidate();
1820            return true;
1821        }
1822
1823        if let Some(intro) = self.module_of_mut(node).and_then(|m| m.introspect_mut()) {
1824            return intro.set_param_by_id(id, value);
1825        }
1826        false
1827    }
1828
1829    /// Restore opaque, non-scalar module state captured by
1830    /// [`GraphModule::serialize_state`] (e.g. a
1831    /// [`ScaleQuantizer`](crate::modules::ScaleQuantizer) custom/Scala tuning table that does
1832    /// not fit the scalar parameter surface). Used by `Patch::from_def` to reconstruct such
1833    /// state on load.
1834    ///
1835    /// Returns the module's own error string if the state is malformed; an unknown `node` is a
1836    /// no-op (`Ok`). Internal state, not routing — no recompile is triggered.
1837    pub fn deserialize_module_state(
1838        &mut self,
1839        node: NodeId,
1840        state: &serde_json::Value,
1841    ) -> Result<(), String> {
1842        match self.module_of_mut(node) {
1843            Some(module) => module.deserialize_state(state),
1844            None => Ok(()),
1845        }
1846    }
1847}
1848
1849/// Manual `Debug` for `Patch` so `println!("{:?}", patch)` works for inspection without
1850/// requiring `GraphModule: Debug`. Prints each node's name and `type_id`, the cable list,
1851/// the output node, validation mode, dirty flag, and warning count.
1852impl core::fmt::Debug for Patch {
1853    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1854        // Wrapper so nodes format as `name (type_id)` without a GraphModule: Debug bound.
1855        struct NodeDebug<'a> {
1856            id: NodeId,
1857            name: &'a str,
1858            type_id: &'a str,
1859        }
1860        impl core::fmt::Debug for NodeDebug<'_> {
1861            fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1862                write!(f, "{:?}: {} ({})", self.id, self.name, self.type_id)
1863            }
1864        }
1865
1866        let nodes: Vec<NodeDebug> = self
1867            .nodes
1868            .iter()
1869            .map(|(id, n)| NodeDebug {
1870                id,
1871                name: n.name.as_str(),
1872                type_id: self.modules[n.module_slot].type_id(),
1873            })
1874            .collect();
1875
1876        f.debug_struct("Patch")
1877            .field("sample_rate", &self.sample_rate)
1878            .field("nodes", &nodes)
1879            .field("cables", &self.cables)
1880            .field("output_node", &self.output_node)
1881            .field("validation_mode", &self.validation_mode)
1882            .field("dirty", &self.dirty)
1883            .field("warnings", &self.warnings.len())
1884            .finish()
1885    }
1886}
1887
1888#[cfg(test)]
1889mod tests {
1890    use super::*;
1891    use crate::port::{PortDef, SignalKind};
1892    use alloc::vec;
1893
1894    // Simple passthrough module for testing
1895    struct Passthrough {
1896        spec: PortSpec,
1897    }
1898
1899    impl Passthrough {
1900        fn new() -> Self {
1901            Self {
1902                spec: PortSpec {
1903                    inputs: vec![PortDef::new(0, "in", SignalKind::Audio)],
1904                    outputs: vec![PortDef::new(10, "out", SignalKind::Audio)],
1905                },
1906            }
1907        }
1908    }
1909
1910    impl GraphModule for Passthrough {
1911        fn port_spec(&self) -> &PortSpec {
1912            &self.spec
1913        }
1914
1915        fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
1916            let input = inputs.get_or(0, 0.0);
1917            outputs.set(10, input);
1918        }
1919
1920        fn reset(&mut self) {}
1921
1922        fn set_sample_rate(&mut self, _: f64) {}
1923    }
1924
1925    #[test]
1926    fn test_add_module() {
1927        let mut patch = Patch::new(44100.0);
1928        let handle = patch.add("test", Passthrough::new());
1929        assert_eq!(patch.node_count(), 1);
1930        assert!(patch.get_name(handle.id()).is_some());
1931    }
1932
1933    #[test]
1934    fn test_connect() {
1935        let mut patch = Patch::new(44100.0);
1936        let a = patch.add("a", Passthrough::new());
1937        let b = patch.add("b", Passthrough::new());
1938
1939        let result = patch.connect(a.out("out"), b.in_("in"));
1940        assert!(result.is_ok());
1941        assert_eq!(patch.cable_count(), 1);
1942    }
1943
1944    #[test]
1945    fn test_topological_sort() {
1946        let mut patch = Patch::new(44100.0);
1947        let a = patch.add("a", Passthrough::new());
1948        let b = patch.add("b", Passthrough::new());
1949        let c = patch.add("c", Passthrough::new());
1950
1951        // A -> B -> C
1952        patch.connect(a.out("out"), b.in_("in")).unwrap();
1953        patch.connect(b.out("out"), c.in_("in")).unwrap();
1954
1955        patch.compile().unwrap();
1956
1957        let order = patch.execution_order();
1958        let a_pos = order.iter().position(|&x| x == a.id()).unwrap();
1959        let b_pos = order.iter().position(|&x| x == b.id()).unwrap();
1960        let c_pos = order.iter().position(|&x| x == c.id()).unwrap();
1961
1962        assert!(a_pos < b_pos, "A should come before B");
1963        assert!(b_pos < c_pos, "B should come before C");
1964    }
1965
1966    #[test]
1967    fn test_cycle_detection() {
1968        let mut patch = Patch::new(44100.0);
1969        let a = patch.add("a", Passthrough::new());
1970        let b = patch.add("b", Passthrough::new());
1971
1972        // Create cycle: A -> B -> A
1973        patch.connect(a.out("out"), b.in_("in")).unwrap();
1974        patch.connect(b.out("out"), a.in_("in")).unwrap();
1975
1976        let result = patch.compile();
1977        assert!(matches!(result, Err(PatchError::CycleDetected { .. })));
1978    }
1979
1980    #[test]
1981    fn test_mult() {
1982        let mut patch = Patch::new(44100.0);
1983        let a = patch.add("a", Passthrough::new());
1984        let b = patch.add("b", Passthrough::new());
1985        let c = patch.add("c", Passthrough::new());
1986
1987        let result = patch.mult(a.out("out"), &[b.in_("in"), c.in_("in")]);
1988        assert!(result.is_ok());
1989        assert_eq!(patch.cable_count(), 2);
1990    }
1991
1992    #[test]
1993    fn test_disconnect() {
1994        let mut patch = Patch::new(44100.0);
1995        let a = patch.add("a", Passthrough::new());
1996        let b = patch.add("b", Passthrough::new());
1997
1998        let cable_id = patch.connect(a.out("out"), b.in_("in")).unwrap();
1999        assert_eq!(patch.cable_count(), 1);
2000
2001        patch.disconnect(cable_id).unwrap();
2002        assert_eq!(patch.cable_count(), 0);
2003    }
2004
2005    #[test]
2006    fn test_remove_module() {
2007        let mut patch = Patch::new(44100.0);
2008        let a = patch.add("a", Passthrough::new());
2009        let b = patch.add("b", Passthrough::new());
2010
2011        patch.connect(a.out("out"), b.in_("in")).unwrap();
2012        assert_eq!(patch.node_count(), 2);
2013        assert_eq!(patch.cable_count(), 1);
2014
2015        patch.remove(a.id()).unwrap();
2016        assert_eq!(patch.node_count(), 1);
2017        assert_eq!(patch.cable_count(), 0); // Cable should be removed too
2018    }
2019
2020    /// Every node must keep pointing at *its own* module across the two operations that
2021    /// move modules around: removing an earlier node (which closes the gap in the module
2022    /// vector) and compiling (which permutes it into execution order).
2023    #[test]
2024    fn test_module_identity_survives_remove_and_compile() {
2025        let mut patch = Patch::new(44100.0);
2026        let vco = patch.add("vco", crate::modules::Vco::new(44100.0));
2027        let doomed = patch.add("doomed", Passthrough::new());
2028        let vca = patch.add("vca", crate::modules::Vca::new());
2029        let out = patch.add("out", crate::modules::StereoOutput::new());
2030
2031        patch.connect(vco.out("saw"), vca.in_("in")).unwrap();
2032        patch.connect(vca.out("out"), out.in_("left")).unwrap();
2033        patch.set_output(out.id());
2034
2035        // Removing the second-added node slides the later modules down a slot.
2036        patch.remove(doomed.id()).unwrap();
2037        // Compiling then permutes what is left into execution order.
2038        patch.compile().unwrap();
2039
2040        let by_id = |id| {
2041            patch
2042                .nodes()
2043                .find(|(nid, _, _)| *nid == id)
2044                .map(|(_, name, module)| (name, module.type_id()))
2045        };
2046        assert_eq!(by_id(vco.id()), Some(("vco", "vco")));
2047        assert_eq!(by_id(vca.id()), Some(("vca", "vca")));
2048        assert_eq!(by_id(out.id()), Some(("out", "stereo_output")));
2049        assert_eq!(patch.node_count(), 3);
2050    }
2051
2052    // ========================================================================
2053    // Phase 2 Tests: Signal Validation & Modulation
2054    // ========================================================================
2055
2056    // Test modules with different signal types
2057    struct GateModule {
2058        spec: PortSpec,
2059    }
2060
2061    impl GateModule {
2062        fn new() -> Self {
2063            Self {
2064                spec: PortSpec {
2065                    inputs: vec![PortDef::new(0, "in", SignalKind::Gate)],
2066                    outputs: vec![PortDef::new(10, "out", SignalKind::Gate)],
2067                },
2068            }
2069        }
2070    }
2071
2072    impl GraphModule for GateModule {
2073        fn port_spec(&self) -> &PortSpec {
2074            &self.spec
2075        }
2076        fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
2077            outputs.set(10, inputs.get_or(0, 0.0));
2078        }
2079        fn reset(&mut self) {}
2080        fn set_sample_rate(&mut self, _: f64) {}
2081    }
2082
2083    #[test]
2084    fn test_validation_mode_none() {
2085        let mut patch = Patch::new(44100.0);
2086        patch.set_validation_mode(ValidationMode::None);
2087
2088        let audio = patch.add("audio", Passthrough::new());
2089        let gate = patch.add("gate", GateModule::new());
2090
2091        // Should succeed without warnings
2092        let result = patch.connect(audio.out("out"), gate.in_("in"));
2093        assert!(result.is_ok());
2094        assert!(patch.warnings().is_empty());
2095    }
2096
2097    #[test]
2098    fn test_validation_mode_warn() {
2099        let mut patch = Patch::new(44100.0);
2100        patch.set_validation_mode(ValidationMode::Warn);
2101
2102        let audio = patch.add("audio", Passthrough::new());
2103        let gate = patch.add("gate", GateModule::new());
2104
2105        // Should succeed but generate warning
2106        let result = patch.connect(audio.out("out"), gate.in_("in"));
2107        assert!(result.is_ok());
2108        assert!(!patch.warnings().is_empty());
2109    }
2110
2111    #[test]
2112    fn test_validation_mode_strict() {
2113        let mut patch = Patch::new(44100.0);
2114        patch.set_validation_mode(ValidationMode::Strict);
2115
2116        let audio = patch.add("audio", Passthrough::new());
2117        let gate = patch.add("gate", GateModule::new());
2118
2119        // Should fail with SignalMismatch error
2120        let result = patch.connect(audio.out("out"), gate.in_("in"));
2121        assert!(matches!(result, Err(PatchError::SignalMismatch { .. })));
2122    }
2123
2124    #[test]
2125    fn test_same_signal_type_no_warning() {
2126        let mut patch = Patch::new(44100.0);
2127        patch.set_validation_mode(ValidationMode::Warn);
2128
2129        let a = patch.add("a", Passthrough::new());
2130        let b = patch.add("b", Passthrough::new());
2131
2132        // Same type should not generate warning
2133        let result = patch.connect(a.out("out"), b.in_("in"));
2134        assert!(result.is_ok());
2135        assert!(patch.warnings().is_empty());
2136    }
2137
2138    #[test]
2139    fn test_connect_modulated() {
2140        let mut patch = Patch::new(44100.0);
2141        let a = patch.add("a", Passthrough::new());
2142        let b = patch.add("b", Passthrough::new());
2143
2144        // Connect with attenuation 0.5 and offset 1.0
2145        let result = patch.connect_modulated(a.out("out"), b.in_("in"), 0.5, 1.0);
2146        assert!(result.is_ok());
2147
2148        let cables = patch.cables();
2149        assert_eq!(cables.len(), 1);
2150        assert_eq!(cables[0].attenuation, Some(0.5));
2151        assert_eq!(cables[0].offset, Some(1.0));
2152    }
2153
2154    #[test]
2155    fn test_modulated_signal_processing() {
2156        let mut patch = Patch::new(44100.0);
2157
2158        // Use a module that outputs a constant value
2159        struct ConstModule {
2160            spec: PortSpec,
2161            value: f64,
2162        }
2163
2164        impl ConstModule {
2165            fn new(value: f64) -> Self {
2166                Self {
2167                    value,
2168                    spec: PortSpec {
2169                        inputs: vec![],
2170                        outputs: vec![PortDef::new(10, "out", SignalKind::Audio)],
2171                    },
2172                }
2173            }
2174        }
2175
2176        impl GraphModule for ConstModule {
2177            fn port_spec(&self) -> &PortSpec {
2178                &self.spec
2179            }
2180            fn tick(&mut self, _: &PortValues, outputs: &mut PortValues) {
2181                outputs.set(10, self.value);
2182            }
2183            fn reset(&mut self) {}
2184            fn set_sample_rate(&mut self, _: f64) {}
2185        }
2186
2187        struct RecordModule {
2188            spec: PortSpec,
2189            last_value: f64,
2190        }
2191
2192        impl RecordModule {
2193            fn new() -> Self {
2194                Self {
2195                    spec: PortSpec {
2196                        inputs: vec![PortDef::new(0, "in", SignalKind::Audio)],
2197                        outputs: vec![],
2198                    },
2199                    last_value: 0.0,
2200                }
2201            }
2202        }
2203
2204        impl GraphModule for RecordModule {
2205            fn port_spec(&self) -> &PortSpec {
2206                &self.spec
2207            }
2208            fn tick(&mut self, inputs: &PortValues, _: &mut PortValues) {
2209                self.last_value = inputs.get_or(0, 0.0);
2210            }
2211            fn reset(&mut self) {}
2212            fn set_sample_rate(&mut self, _: f64) {}
2213        }
2214
2215        let source = patch.add("source", ConstModule::new(4.0));
2216        let sink = patch.add("sink", RecordModule::new());
2217
2218        // Attenuation 0.5, offset 2.0: 4.0 * 0.5 + 2.0 = 4.0
2219        patch
2220            .connect_modulated(source.out("out"), sink.in_("in"), 0.5, 2.0)
2221            .unwrap();
2222        patch.set_output(sink.id());
2223        patch.compile().unwrap();
2224        patch.tick();
2225
2226        // The value should be processed through attenuation and offset
2227        // We can't easily check the internal value, but we verified the connection works
2228    }
2229
2230    #[test]
2231    fn test_signal_compatibility() {
2232        // Test specific compatibility cases
2233        assert!(SignalKind::Audio
2234            .is_compatible_with(&SignalKind::Audio)
2235            .warning
2236            .is_none());
2237        assert!(SignalKind::Audio
2238            .is_compatible_with(&SignalKind::CvBipolar)
2239            .warning
2240            .is_some());
2241        assert!(SignalKind::Gate
2242            .is_compatible_with(&SignalKind::Trigger)
2243            .warning
2244            .is_some());
2245        assert!(SignalKind::Clock
2246            .is_compatible_with(&SignalKind::Trigger)
2247            .warning
2248            .is_none());
2249    }
2250
2251    #[test]
2252    fn test_patch_get_name() {
2253        let mut patch = Patch::new(44100.0);
2254        let a = patch.add("my_module", Passthrough::new());
2255
2256        let name = patch.get_name(a.id());
2257        assert_eq!(name, Some("my_module"));
2258
2259        // Non-existent node
2260        use slotmap::DefaultKey;
2261        let fake_id: NodeId = DefaultKey::default();
2262        assert!(patch.get_name(fake_id).is_none());
2263    }
2264
2265    #[test]
2266    fn test_patch_set_position() {
2267        let mut patch = Patch::new(44100.0);
2268        let a = patch.add("a", Passthrough::new());
2269
2270        patch.set_position(a.id(), (100.0, 200.0));
2271        // Position is stored but not exposed directly in tests
2272    }
2273
2274    #[test]
2275    fn test_patch_clear_warnings() {
2276        let mut patch = Patch::new(44100.0);
2277        patch.set_validation_mode(ValidationMode::Warn);
2278
2279        let audio = patch.add("audio", Passthrough::new());
2280        let gate = patch.add("gate", GateModule::new());
2281
2282        patch.connect(audio.out("out"), gate.in_("in")).unwrap();
2283        assert!(!patch.warnings().is_empty());
2284
2285        patch.clear_warnings();
2286        assert!(patch.warnings().is_empty());
2287    }
2288
2289    #[test]
2290    fn test_patch_validation_mode_getter() {
2291        let mut patch = Patch::new(44100.0);
2292        patch.set_validation_mode(ValidationMode::Strict);
2293        assert_eq!(patch.validation_mode(), ValidationMode::Strict);
2294    }
2295
2296    #[test]
2297    fn test_patch_sample_rate() {
2298        let patch = Patch::new(48000.0);
2299        assert_eq!(patch.sample_rate(), 48000.0);
2300    }
2301
2302    #[test]
2303    fn test_patch_execution_order() {
2304        let mut patch = Patch::new(44100.0);
2305        let a = patch.add("a", Passthrough::new());
2306        let b = patch.add("b", Passthrough::new());
2307        patch.connect(a.out("out"), b.in_("in")).unwrap();
2308        patch.compile().unwrap();
2309
2310        let order = patch.execution_order();
2311        assert_eq!(order.len(), 2);
2312    }
2313
2314    #[test]
2315    fn test_patch_mult() {
2316        let mut patch = Patch::new(44100.0);
2317        let a = patch.add("a", Passthrough::new());
2318        let b = patch.add("b", Passthrough::new());
2319        let c = patch.add("c", Passthrough::new());
2320
2321        // Connect one output to multiple inputs
2322        let result = patch.mult(a.out("out"), &[b.in_("in"), c.in_("in")]);
2323        assert!(result.is_ok());
2324        assert_eq!(patch.cable_count(), 2);
2325    }
2326
2327    #[test]
2328    fn test_patch_reset() {
2329        let mut patch = Patch::new(44100.0);
2330        let a = patch.add("a", Passthrough::new());
2331        patch.set_output(a.id());
2332        patch.compile().unwrap();
2333
2334        for _ in 0..100 {
2335            patch.tick();
2336        }
2337
2338        patch.reset();
2339        // Reset clears internal state
2340    }
2341
2342    #[test]
2343    fn test_patch_set_param_get_param() {
2344        use crate::modules::Vco;
2345        let mut patch = Patch::new(44100.0);
2346        let vco = patch.add("vco", Vco::new(44100.0));
2347
2348        // Try to set/get param (may or may not have params)
2349        patch.set_param(vco.id(), 0, 0.5);
2350        let _ = patch.get_param(vco.id(), 0);
2351    }
2352
2353    #[test]
2354    fn test_node_handle_spec() {
2355        let mut patch = Patch::new(44100.0);
2356        let a = patch.add("a", Passthrough::new());
2357
2358        let spec = a.spec();
2359        assert!(!spec.inputs.is_empty());
2360        assert!(!spec.outputs.is_empty());
2361    }
2362
2363    #[test]
2364    fn test_patch_validation_mode() {
2365        let mut patch = Patch::new(44100.0);
2366
2367        patch.set_validation_mode(ValidationMode::Strict);
2368        assert_eq!(patch.validation_mode(), ValidationMode::Strict);
2369
2370        patch.set_validation_mode(ValidationMode::Warn);
2371        assert_eq!(patch.validation_mode(), ValidationMode::Warn);
2372    }
2373
2374    // ========================================================================
2375    // Wave B-0 audit remediation tests
2376    // ========================================================================
2377
2378    // A module that sums two inputs (ids 0, 1) into one output (id 10).
2379    struct SumModule {
2380        spec: PortSpec,
2381    }
2382    impl SumModule {
2383        fn new() -> Self {
2384            Self {
2385                spec: PortSpec {
2386                    inputs: vec![
2387                        PortDef::new(0, "a", SignalKind::Audio),
2388                        PortDef::new(1, "b", SignalKind::Audio),
2389                    ],
2390                    outputs: vec![PortDef::new(10, "out", SignalKind::Audio)],
2391                },
2392            }
2393        }
2394    }
2395    impl GraphModule for SumModule {
2396        fn port_spec(&self) -> &PortSpec {
2397            &self.spec
2398        }
2399        fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
2400            outputs.set(10, inputs.get_or(0, 0.0) + inputs.get_or(1, 0.0));
2401        }
2402        fn reset(&mut self) {}
2403        fn set_sample_rate(&mut self, _: f64) {}
2404    }
2405
2406    // A one-sample delay that declares itself a feedback cycle-breaker.
2407    struct FeedbackDelay {
2408        spec: PortSpec,
2409        buffer: f64,
2410    }
2411    impl FeedbackDelay {
2412        fn new() -> Self {
2413            Self {
2414                spec: PortSpec {
2415                    inputs: vec![PortDef::new(0, "in", SignalKind::Audio)],
2416                    outputs: vec![PortDef::new(10, "out", SignalKind::Audio)],
2417                },
2418                buffer: 0.0,
2419            }
2420        }
2421    }
2422    impl GraphModule for FeedbackDelay {
2423        fn port_spec(&self) -> &PortSpec {
2424            &self.spec
2425        }
2426        fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
2427            outputs.set(10, self.buffer);
2428            self.buffer = inputs.get_or(0, 0.0);
2429        }
2430        fn reset(&mut self) {
2431            self.buffer = 0.0;
2432        }
2433        fn set_sample_rate(&mut self, _: f64) {}
2434        fn breaks_feedback_cycle(&self) -> bool {
2435            true
2436        }
2437    }
2438
2439    // A constant source with a single non-zero-id output (id 10).
2440    struct ConstSource {
2441        spec: PortSpec,
2442        value: f64,
2443    }
2444    impl ConstSource {
2445        fn new(value: f64) -> Self {
2446            Self {
2447                spec: PortSpec {
2448                    inputs: vec![],
2449                    outputs: vec![PortDef::new(10, "out", SignalKind::Audio)],
2450                },
2451                value,
2452            }
2453        }
2454    }
2455    impl GraphModule for ConstSource {
2456        fn port_spec(&self) -> &PortSpec {
2457            &self.spec
2458        }
2459        fn tick(&mut self, _: &PortValues, outputs: &mut PortValues) {
2460            outputs.set(10, self.value);
2461        }
2462        fn reset(&mut self) {}
2463        fn set_sample_rate(&mut self, _: f64) {}
2464    }
2465
2466    // Q075: CableIds are stable across disconnects of other cables.
2467    #[test]
2468    fn test_cable_ids_are_stable_across_disconnect() {
2469        let mut patch = Patch::new(44100.0);
2470        let a = patch.add("a", Passthrough::new());
2471        let b = patch.add("b", SumModule::new());
2472        let c = patch.add("c", SumModule::new());
2473
2474        let c1 = patch.connect(a.out("out"), b.in_("a")).unwrap();
2475        let c2 = patch.connect(a.out("out"), b.in_("b")).unwrap();
2476        let c3 = patch.connect(a.out("out"), c.in_("a")).unwrap();
2477        assert_eq!(patch.cable_count(), 3);
2478
2479        // Disconnect the FIRST cable. With Vec-index ids this would shift c3 down.
2480        patch.disconnect(c1).unwrap();
2481        assert_eq!(patch.cable_count(), 2);
2482
2483        // Disconnecting the THIRD cable by its still-valid id must remove exactly it,
2484        // leaving only c2 (a.out -> b.b).
2485        patch.disconnect(c3).unwrap();
2486        assert_eq!(patch.cable_count(), 1);
2487        let remaining = &patch.cables()[0];
2488        assert_eq!(remaining.id, c2);
2489        assert_eq!(remaining.to, b.in_("b"));
2490
2491        // A stale id (already removed) errors rather than dropping the wrong cable.
2492        assert!(matches!(
2493            patch.disconnect(c1),
2494            Err(PatchError::InvalidCable)
2495        ));
2496    }
2497
2498    // Q076/Q181: mutating after compile is reflected on the next tick (lazy recompile).
2499    #[test]
2500    fn test_mutation_after_compile_is_reflected_on_tick() {
2501        let mut patch = Patch::new(44100.0);
2502        let src = patch.add("src", ConstSource::new(1.0));
2503        let out = patch.add("out", Passthrough::new());
2504        patch.connect(src.out("out"), out.in_("in")).unwrap();
2505        patch.set_output(out.id());
2506        patch.compile().unwrap();
2507
2508        // First tick: passthrough carries the source through.
2509        let (l0, _) = patch.tick();
2510        assert!((l0 - 1.0).abs() < 1e-9);
2511
2512        // Mutate AFTER compile: add a second source summed in. No explicit recompile.
2513        let src2 = patch.add("src2", ConstSource::new(2.0));
2514        let sum = patch.add("sum", SumModule::new());
2515        // Rewire: src -> sum.a, src2 -> sum.b, sum -> out
2516        patch
2517            .disconnect_ports(src.out("out"), out.in_("in"))
2518            .unwrap();
2519        patch.connect(src.out("out"), sum.in_("a")).unwrap();
2520        patch.connect(src2.out("out"), sum.in_("b")).unwrap();
2521        patch.connect(sum.out("out"), out.in_("in")).unwrap();
2522
2523        // tick() must lazily recompile and reflect the NEW graph (1.0 + 2.0 = 3.0),
2524        // not freeze at the stale 1.0.
2525        let (l1, _) = patch.tick();
2526        assert!((l1 - 3.0).abs() < 1e-9, "expected 3.0, got {}", l1);
2527    }
2528
2529    // Q076/Q181: a cycle-creating mutation surfaces via last_compile_error and ticks silent.
2530    #[test]
2531    fn test_cycle_mutation_surfaces_via_last_compile_error() {
2532        let mut patch = Patch::new(44100.0);
2533        let a = patch.add("a", Passthrough::new());
2534        let b = patch.add("b", Passthrough::new());
2535        patch.connect(a.out("out"), b.in_("in")).unwrap();
2536        patch.set_output(b.id());
2537        patch.compile().unwrap();
2538        assert!(patch.last_compile_error().is_none());
2539
2540        // Introduce a breakerless cycle: b -> a.
2541        patch.connect(b.out("out"), a.in_("in")).unwrap();
2542
2543        // tick() auto-recompiles, fails, outputs silence, and records the error.
2544        let (l, r) = patch.tick();
2545        assert_eq!((l, r), (0.0, 0.0));
2546        match patch.last_compile_error() {
2547            Some(PatchError::CycleDetected { names, .. }) => {
2548                assert_eq!(names.len(), 2);
2549            }
2550            other => panic!("expected CycleDetected, got {:?}", other),
2551        }
2552    }
2553
2554    // Q077: a feedback loop routed through a cycle-breaker compiles and decays.
2555    #[test]
2556    fn test_feedback_loop_with_delay_compiles_and_decays() {
2557        // A one-shot impulse: 1.0 on the first tick, 0.0 thereafter. It stays connected so
2558        // no mid-run mutation clears the feedback buffers.
2559        struct Impulse {
2560            spec: PortSpec,
2561            fired: bool,
2562        }
2563        impl GraphModule for Impulse {
2564            fn port_spec(&self) -> &PortSpec {
2565                &self.spec
2566            }
2567            fn tick(&mut self, _: &PortValues, outputs: &mut PortValues) {
2568                outputs.set(10, if self.fired { 0.0 } else { 1.0 });
2569                self.fired = true;
2570            }
2571            fn reset(&mut self) {
2572                self.fired = false;
2573            }
2574            fn set_sample_rate(&mut self, _: f64) {}
2575        }
2576
2577        let mut patch = Patch::new(44100.0);
2578        let impulse = patch.add(
2579            "impulse",
2580            Impulse {
2581                spec: PortSpec {
2582                    inputs: vec![],
2583                    outputs: vec![PortDef::new(10, "out", SignalKind::Audio)],
2584                },
2585                fired: false,
2586            },
2587        );
2588        let sum = patch.add("sum", SumModule::new());
2589        let delay = patch.add("delay", FeedbackDelay::new());
2590
2591        // impulse -> sum.a ; delay.out -> sum.b (feedback, x0.5) ; sum.out -> delay.in
2592        // (edge into the breaker) ; output = delay.out
2593        patch.connect(impulse.out("out"), sum.in_("a")).unwrap();
2594        patch
2595            .connect_attenuated(delay.out("out"), sum.in_("b"), 0.5)
2596            .unwrap();
2597        patch.connect(sum.out("out"), delay.in_("in")).unwrap();
2598        patch.set_output(delay.id());
2599
2600        // Must compile despite the sum<->delay cycle (delay breaks it).
2601        patch.compile().expect("feedback loop should compile");
2602        assert!(patch.last_compile_error().is_none());
2603
2604        let mut outs = Vec::new();
2605        for _ in 0..14 {
2606            outs.push(patch.tick().0);
2607        }
2608
2609        // The loop must ring: there are several non-zero echoes...
2610        let nonzero: Vec<f64> = outs.iter().copied().filter(|v| v.abs() > 1e-9).collect();
2611        assert!(
2612            nonzero.len() >= 3,
2613            "expected multiple decaying echoes, got {:?}",
2614            outs
2615        );
2616        // ...and successive echo magnitudes decay (0.5 feedback): the first echo is the
2617        // loudest, the tail is quieter.
2618        let peak_early = outs.iter().cloned().fold(0.0_f64, f64::max);
2619        let peak_late = outs[outs.len() - 3..]
2620            .iter()
2621            .cloned()
2622            .fold(0.0_f64, f64::max);
2623        assert!(
2624            peak_late < peak_early,
2625            "echo should decay: early peak {}, late peak {}",
2626            peak_early,
2627            peak_late
2628        );
2629    }
2630
2631    // Q077: a cycle with no breaker still fails to compile.
2632    #[test]
2633    fn test_breakerless_cycle_still_errors() {
2634        let mut patch = Patch::new(44100.0);
2635        let a = patch.add("a", Passthrough::new());
2636        let b = patch.add("b", Passthrough::new());
2637        patch.connect(a.out("out"), b.in_("in")).unwrap();
2638        patch.connect(b.out("out"), a.in_("in")).unwrap();
2639        assert!(matches!(
2640            patch.compile(),
2641            Err(PatchError::CycleDetected { .. })
2642        ));
2643    }
2644
2645    // Q079: normalled input reads the sibling INPUT's current-tick value, not a stale
2646    // output-buffer read. StereoOutput with only `left` patched -> both channels identical.
2647    #[test]
2648    fn test_normalled_input_uses_current_sibling_value() {
2649        use crate::modules::StereoOutput;
2650        let mut patch = Patch::new(44100.0);
2651        // Time-varying source so a one-sample lag would be detectable.
2652        struct Ramp {
2653            spec: PortSpec,
2654            n: f64,
2655        }
2656        impl GraphModule for Ramp {
2657            fn port_spec(&self) -> &PortSpec {
2658                &self.spec
2659            }
2660            fn tick(&mut self, _: &PortValues, outputs: &mut PortValues) {
2661                self.n += 1.0;
2662                outputs.set(10, self.n);
2663            }
2664            fn reset(&mut self) {
2665                self.n = 0.0;
2666            }
2667            fn set_sample_rate(&mut self, _: f64) {}
2668        }
2669        let ramp = patch.add(
2670            "ramp",
2671            Ramp {
2672                spec: PortSpec {
2673                    inputs: vec![],
2674                    outputs: vec![PortDef::new(10, "out", SignalKind::Audio)],
2675                },
2676                n: 0.0,
2677            },
2678        );
2679        let out = patch.add("out", StereoOutput::new());
2680        // Patch only LEFT; RIGHT is normalled to LEFT.
2681        patch.connect(ramp.out("out"), out.in_("left")).unwrap();
2682        patch.set_output(out.id());
2683        patch.compile().unwrap();
2684
2685        for _ in 0..5 {
2686            let (l, r) = patch.tick();
2687            assert!(l > 0.0);
2688            assert_eq!(l, r, "mono fallback must be current-sample, not delayed");
2689        }
2690    }
2691
2692    // A module whose inputs form a forward-ordered + transitive normalled chain:
2693    // input 0 normals to 1, input 1 normals to 2, input 2 is a plain patched input.
2694    // Each tick echoes the three resolved input values to outputs 10/11/12 so a test
2695    // can observe how normalling resolved. `cycle` swaps in a 0<->1 cycle to exercise
2696    // the compile-time cycle guard (both collapse to their own defaults).
2697    struct NormalChain {
2698        spec: PortSpec,
2699    }
2700    impl NormalChain {
2701        fn new(cycle: bool) -> Self {
2702            let (n0, n1) = if cycle { (1, 0) } else { (1, 2) };
2703            Self {
2704                spec: PortSpec {
2705                    inputs: vec![
2706                        PortDef::new(0, "a", SignalKind::Audio)
2707                            .with_default(0.1)
2708                            .normalled_to(n0),
2709                        PortDef::new(1, "b", SignalKind::Audio)
2710                            .with_default(0.2)
2711                            .normalled_to(n1),
2712                        PortDef::new(2, "c", SignalKind::Audio).with_default(0.3),
2713                    ],
2714                    outputs: vec![
2715                        PortDef::new(10, "oa", SignalKind::Audio),
2716                        PortDef::new(11, "ob", SignalKind::Audio),
2717                        PortDef::new(12, "oc", SignalKind::Audio),
2718                    ],
2719                },
2720            }
2721        }
2722    }
2723    impl GraphModule for NormalChain {
2724        fn port_spec(&self) -> &PortSpec {
2725            &self.spec
2726        }
2727        fn tick(&mut self, inputs: &PortValues, outputs: &mut PortValues) {
2728            outputs.set(10, inputs.get_or(0, f64::NAN));
2729            outputs.set(11, inputs.get_or(1, f64::NAN));
2730            outputs.set(12, inputs.get_or(2, f64::NAN));
2731        }
2732        fn reset(&mut self) {}
2733        fn set_sample_rate(&mut self, _: f64) {}
2734    }
2735
2736    // Regression: a forward-ordered + transitive normalled chain must resolve
2737    // order-independently. Input 0 normals to 1, 1 normals to 2, only 2 is
2738    // patched -> all three inputs must read the patched source value, not the
2739    // per-input default. Pre-fix, pass 2's single fixed-order pass left input 0
2740    // (and 1) collapsing to the default because their sibling was not yet set.
2741    #[test]
2742    fn test_normalled_chain_resolves_transitively() {
2743        let mut patch = Patch::new(44100.0);
2744        let src = patch.add("src", ConstSource::new(0.75));
2745        let node = patch.add("chain", NormalChain::new(false));
2746        // Patch ONLY the deepest input (id 2, "c"); 0 and 1 fall back through it.
2747        patch.connect(src.out("out"), node.in_("c")).unwrap();
2748        patch.set_output(node.id());
2749        patch.compile().unwrap();
2750
2751        // tick() reads outputs 10/11 (the output node's first two outputs) which
2752        // echo resolved inputs 0 and 1; inspect input 2 via the raw output buffer.
2753        let (oa, ob) = patch.tick();
2754        assert!(
2755            (oa - 0.75).abs() < 1e-9,
2756            "forward-normalled input 0 must resolve to patched source, got {oa}"
2757        );
2758        assert!(
2759            (ob - 0.75).abs() < 1e-9,
2760            "transitively-normalled input 1 must resolve to patched source, got {ob}"
2761        );
2762    }
2763
2764    // Regression companion: a normalled *cycle* (0<->1, both unpatched) must not
2765    // hang at compile time and each input falls back to its own default.
2766    #[test]
2767    fn test_normalled_cycle_falls_back_to_default() {
2768        let mut patch = Patch::new(44100.0);
2769        let node = patch.add("chain", NormalChain::new(true));
2770        patch.set_output(node.id());
2771        patch.compile().unwrap();
2772        let (oa, ob) = patch.tick();
2773        assert!(
2774            (oa - 0.1).abs() < 1e-9,
2775            "cycled input 0 -> own default, got {oa}"
2776        );
2777        assert!(
2778            (ob - 0.2).abs() < 1e-9,
2779            "cycled input 1 -> own default, got {ob}"
2780        );
2781    }
2782
2783    // Q080: compilation is deterministic — same patch built twice -> same execution_order.
2784    #[test]
2785    fn test_execution_order_is_deterministic() {
2786        fn build_order() -> Vec<usize> {
2787            let mut patch = Patch::new(44100.0);
2788            // Several independent sources feeding one sum -> deterministic tie-breaking.
2789            let s1 = patch.add("s1", ConstSource::new(1.0));
2790            let s2 = patch.add("s2", ConstSource::new(2.0));
2791            let s3 = patch.add("s3", ConstSource::new(3.0));
2792            let sum = patch.add("sum", SumModule::new());
2793            patch.connect(s1.out("out"), sum.in_("a")).unwrap();
2794            patch.connect(s2.out("out"), sum.in_("b")).unwrap();
2795            patch.connect(s3.out("out"), sum.in_("a")).unwrap();
2796            patch.compile().unwrap();
2797            // Map NodeIds to their insertion rank for a build-independent comparison.
2798            let ids = [s1.id(), s2.id(), s3.id(), sum.id()];
2799            patch
2800                .execution_order()
2801                .iter()
2802                .map(|nid| ids.iter().position(|x| x == nid).unwrap())
2803                .collect()
2804        }
2805        assert_eq!(build_order(), build_order());
2806    }
2807
2808    // Q121: read_output reads the output node's first two outputs (mono duplicated),
2809    // regardless of their port ids (here a single output with id 10).
2810    #[test]
2811    fn test_read_output_uses_first_two_outputs_mono_duplicated() {
2812        let mut patch = Patch::new(44100.0);
2813        let src = patch.add("src", ConstSource::new(0.7));
2814        patch.set_output(src.id());
2815        patch.compile().unwrap();
2816        let (l, r) = patch.tick();
2817        assert!((l - 0.7).abs() < 1e-9);
2818        assert_eq!(l, r, "mono node must duplicate to both channels");
2819    }
2820
2821    // Q121/6a: try_set_output validates node existence and presence of outputs.
2822    #[test]
2823    fn test_try_set_output_validates() {
2824        let mut patch = Patch::new(44100.0);
2825        let src = patch.add("src", ConstSource::new(1.0));
2826        assert!(patch.try_set_output(src.id()).is_ok());
2827
2828        // A node with no outputs is rejected.
2829        struct SinkNoOut {
2830            spec: PortSpec,
2831        }
2832        impl GraphModule for SinkNoOut {
2833            fn port_spec(&self) -> &PortSpec {
2834                &self.spec
2835            }
2836            fn tick(&mut self, _: &PortValues, _: &mut PortValues) {}
2837            fn reset(&mut self) {}
2838            fn set_sample_rate(&mut self, _: f64) {}
2839        }
2840        let sink = patch.add(
2841            "sink",
2842            SinkNoOut {
2843                spec: PortSpec {
2844                    inputs: vec![PortDef::new(0, "in", SignalKind::Audio)],
2845                    outputs: vec![],
2846                },
2847            },
2848        );
2849        assert!(matches!(
2850            patch.try_set_output(sink.id()),
2851            Err(PatchError::InvalidPort { .. })
2852        ));
2853    }
2854
2855    // Q122/Q180: NodeHandle fallible port lookups and name discovery.
2856    #[test]
2857    fn test_node_handle_fallible_ports_and_names() {
2858        let mut patch = Patch::new(44100.0);
2859        let a = patch.add("a", Passthrough::new());
2860
2861        assert!(a.output("out").is_ok());
2862        assert!(a.input("in").is_ok());
2863
2864        // Unknown ports return an InvalidPort carrying the available names.
2865        match a.output("nope") {
2866            Err(PatchError::InvalidPort { available, .. }) => {
2867                assert!(available.iter().any(|n| n == "out"));
2868            }
2869            other => panic!("expected InvalidPort, got {:?}", other),
2870        }
2871        assert!(a.input("nope").is_err());
2872
2873        assert_eq!(a.input_names(), vec!["in"]);
2874        assert_eq!(a.output_names(), vec!["out"]);
2875    }
2876
2877    // Q182: Display lists the module's available ports on an invalid connection.
2878    #[test]
2879    fn test_invalid_port_display_lists_available() {
2880        let mut patch = Patch::new(44100.0);
2881        let a = patch.add("a", Passthrough::new());
2882        let b = patch.add("b", Passthrough::new());
2883        // Connect to a non-existent input port id on b.
2884        let bad = PortRef {
2885            node: b.id(),
2886            port: 999,
2887        };
2888        let err = patch.connect(a.out("out"), bad).unwrap_err();
2889        let msg = alloc::format!("{}", err);
2890        assert!(msg.contains("Invalid port"), "got: {}", msg);
2891        assert!(
2892            msg.contains("in"),
2893            "should list available port 'in': {}",
2894            msg
2895        );
2896    }
2897
2898    // Q185: CycleDetected Display prints the module names in the cycle.
2899    #[test]
2900    fn test_cycle_detected_display_names() {
2901        let mut patch = Patch::new(44100.0);
2902        let a = patch.add("osc", Passthrough::new());
2903        let b = patch.add("filt", Passthrough::new());
2904        patch.connect(a.out("out"), b.in_("in")).unwrap();
2905        patch.connect(b.out("out"), a.in_("in")).unwrap();
2906        let err = patch.compile().unwrap_err();
2907        let msg = alloc::format!("{}", err);
2908        assert!(msg.contains("Cycle detected"), "got: {}", msg);
2909        assert!(
2910            msg.contains("osc") && msg.contains("filt"),
2911            "cycle message should name modules: {}",
2912            msg
2913        );
2914    }
2915
2916    // Q183: the default validation mode is Warn.
2917    #[test]
2918    fn test_default_validation_mode_is_warn() {
2919        let patch = Patch::new(44100.0);
2920        assert_eq!(patch.validation_mode(), ValidationMode::Warn);
2921        assert_eq!(ValidationMode::default(), ValidationMode::Warn);
2922    }
2923
2924    // Q187: Patch implements Debug for println-style inspection.
2925    #[test]
2926    fn test_patch_debug_impl() {
2927        let mut patch = Patch::new(44100.0);
2928        let a = patch.add("my_osc", Passthrough::new());
2929        let b = patch.add("my_out", Passthrough::new());
2930        patch.connect(a.out("out"), b.in_("in")).unwrap();
2931        patch.set_output(b.id());
2932        let s = alloc::format!("{:?}", patch);
2933        assert!(s.contains("Patch"));
2934        assert!(s.contains("my_osc"));
2935        assert!(s.contains("my_out"));
2936        assert!(s.contains("validation_mode"));
2937    }
2938
2939    // ========================================================================
2940    // Wave C-0 performance remediation tests (zero-alloc routing)
2941    // ========================================================================
2942
2943    // Q111: tick_block produces exactly the same stereo stream as an equal number of
2944    // per-sample tick() calls (the block path is a pure loop over the same engine).
2945    #[test]
2946    fn test_tick_block_matches_per_sample_tick() {
2947        fn ramp_svf_patch() -> (Patch, NodeHandle) {
2948            let mut patch = Patch::new(44100.0);
2949            let src = patch.add("src", ConstSource::new(0.9));
2950            let pass = patch.add("pass", Passthrough::new());
2951            patch.connect(src.out("out"), pass.in_("in")).unwrap();
2952            patch.set_output(pass.id());
2953            patch.compile().unwrap();
2954            (patch, pass)
2955        }
2956
2957        // Reference: 8 per-sample ticks.
2958        let (mut a, _) = ramp_svf_patch();
2959        let mut reference = Vec::new();
2960        for _ in 0..8 {
2961            reference.push(a.tick());
2962        }
2963
2964        // Block: one tick_block of length 8 on a fresh identical patch.
2965        let (mut b, _) = ramp_svf_patch();
2966        let mut left = [0.0_f64; 8];
2967        let mut right = [0.0_f64; 8];
2968        b.tick_block(&mut left, &mut right);
2969
2970        for (i, &(l, r)) in reference.iter().enumerate() {
2971            assert!((left[i] - l).abs() < 1e-12, "left[{}] mismatch", i);
2972            assert!((right[i] - r).abs() < 1e-12, "right[{}] mismatch", i);
2973        }
2974    }
2975
2976    // Q111: tick_block only writes min(left.len(), right.len()) frames.
2977    #[test]
2978    fn test_tick_block_uses_min_length() {
2979        let mut patch = Patch::new(44100.0);
2980        let src = patch.add("src", ConstSource::new(1.0));
2981        patch.set_output(src.id());
2982        patch.compile().unwrap();
2983
2984        let mut left = [0.0_f64; 4];
2985        let mut right = [0.0_f64; 2]; // shorter -> only 2 frames processed
2986        patch.tick_block(&mut left, &mut right);
2987
2988        assert_eq!(left[0], 1.0);
2989        assert_eq!(left[1], 1.0);
2990        assert_eq!(left[2], 0.0, "frame beyond min length must be untouched");
2991        assert_eq!(left[3], 0.0);
2992        assert_eq!(right[0], 1.0);
2993        assert_eq!(right[1], 1.0);
2994    }
2995
2996    // Q108: a subnormal value produced by a module is flushed to zero as it is scattered
2997    // into the routing buffers, so it cannot circulate through the graph.
2998    #[test]
2999    fn test_denormal_is_flushed_at_scatter() {
3000        let mut patch = Patch::new(44100.0);
3001        // Source emits a subnormal-magnitude value (< 1e-20 flush threshold).
3002        let src = patch.add("src", ConstSource::new(1e-30));
3003        let pass = patch.add("pass", Passthrough::new());
3004        patch.connect(src.out("out"), pass.in_("in")).unwrap();
3005        patch.set_output(pass.id());
3006        patch.compile().unwrap();
3007
3008        let (l, r) = patch.tick();
3009        assert_eq!(l, 0.0, "subnormal must be flushed to zero");
3010        assert_eq!(r, 0.0);
3011        // The source's own output slot is flushed too (observable via get_output_value).
3012        assert_eq!(patch.get_output_value(src.id(), 10), Some(0.0));
3013    }
3014
3015    // Q199: a non-finite value produced by a module is zeroed as it is scattered
3016    // into the routing buffers, so it cannot poison other modules' recursive state.
3017    #[test]
3018    fn test_non_finite_is_sanitized_at_scatter() {
3019        for &bad in &[f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
3020            let mut patch = Patch::new(44100.0);
3021            let src = patch.add("src", ConstSource::new(bad));
3022            let pass = patch.add("pass", Passthrough::new());
3023            patch.connect(src.out("out"), pass.in_("in")).unwrap();
3024            patch.set_output(pass.id());
3025            patch.compile().unwrap();
3026
3027            let (l, r) = patch.tick();
3028            assert_eq!(l, 0.0, "non-finite ({bad}) must be zeroed at scatter");
3029            assert_eq!(r, 0.0);
3030            assert_eq!(patch.get_output_value(src.id(), 10), Some(0.0));
3031        }
3032    }
3033
3034    // Q107: precompiled adjacency preserves multi-cable input summing with per-cable
3035    // attenuation/offset, and get_output_value reflects the dense buffer.
3036    #[test]
3037    fn test_precompiled_adjacency_sums_and_exposes_outputs() {
3038        let mut patch = Patch::new(44100.0);
3039        let s1 = patch.add("s1", ConstSource::new(2.0));
3040        let s2 = patch.add("s2", ConstSource::new(3.0));
3041        let sum = patch.add("sum", SumModule::new());
3042        // Two cables into input "a": 2.0 and (3.0 * 0.5 + 1.0) = 2.5 -> a = 4.5; b = default 0.
3043        patch.connect(s1.out("out"), sum.in_("a")).unwrap();
3044        patch
3045            .connect_modulated(s2.out("out"), sum.in_("a"), 0.5, 1.0)
3046            .unwrap();
3047        patch.set_output(sum.id());
3048        patch.compile().unwrap();
3049
3050        let (l, _) = patch.tick();
3051        assert!((l - 4.5).abs() < 1e-12, "expected 4.5, got {}", l);
3052        assert_eq!(patch.get_output_value(sum.id(), 10), Some(4.5));
3053        // Non-existent port -> None (dense slot index miss).
3054        assert_eq!(patch.get_output_value(sum.id(), 999), None);
3055    }
3056}