Skip to main content

auracle_grammar/
mutate.rs

1//! User-driven structural edits: create, delete, replace, and rewire nodes
2//! in a patch tree — the "reconnect anything" surface of the workbench.
3//!
4//! Because the genome is a *typed tree*, rewiring is expressed as a small
5//! vocabulary of operations that are type-safe by construction (an LFO can
6//! never end up in an audio slot; a filter always has exactly one audio
7//! input): replace a node, insert a node into a wire, delete/splice a node,
8//! change a modulation source, swap a mixer's inputs. These are the same
9//! moves evolution's structural proposals make — hand edits and MH walk the
10//! same lattice.
11//!
12//! Nodes are addressed by their trace **key** (`node`, `node/0`, `node/0/1`,
13//! `node/0/m` for mod slots — see [`crate::genome`]).
14
15use serde::{Deserialize, Serialize};
16use thiserror::Error;
17
18use crate::term::{
19    AudioNode, DriveMode, FilterKind, ModNode, ModOp, NoiseColor, PairOp, PatchTree, TableShape,
20    Uid, Waveform,
21};
22
23/// Hard ceilings on hand-built patches (protects the realtime voice and the
24/// feature pipeline; evolution's own prior rarely exceeds these).
25pub const MAX_SIZE: usize = 24;
26/// Maximum tree depth for hand-built patches.
27pub const MAX_DEPTH: usize = 9;
28/// Maximum nesting depth of a hand-built **modulation** term.
29///
30/// Above the prior's `max_mod_depth` of 2, on the same argument as
31/// [`MAX_DEPTH`] against the prior's `max_depth`: a person stacking shapers by
32/// hand knows what they are building, and the ceiling is there to protect the
33/// realtime voice rather than to shape the search. It stops well short of the
34/// audio ceiling because a `Pair` branches, so depth 4 is up to sixteen leaves
35/// on one cable — and each of them is another level of the compiler's
36/// by-value recursion on top of the audio tree's.
37pub const MAX_MOD_DEPTH: usize = 4;
38
39/// The buildable node palette (everything the grammar can express).
40///
41/// Serialized in snake_case, which is also the string the rack description
42/// reports as [`crate::describe::RackModule::kind`] and the frontend keys its
43/// palette off. `RingMod` is renamed by hand because the derived spelling
44/// would be `ring_mod` while the module is `ringmod` everywhere else, and one
45/// module with two spellings is a defect waiting for a caller.
46#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
47#[serde(rename_all = "snake_case")]
48pub enum NodeKind {
49    /// Band-limited oscillator.
50    Vco,
51    /// Seven-voice detuned saw stack.
52    Supersaw,
53    /// Noise source.
54    Noise,
55    /// Equal-power crossfade.
56    Mix,
57    /// SVF / ladder filter.
58    Filter,
59    /// Wavefolder.
60    Fold,
61    /// Delay line.
62    Delay,
63    /// Chorus.
64    Chorus,
65    /// Algorithmic reverb.
66    Reverb,
67    /// Morphing wavetable oscillator.
68    Wavetable,
69    /// Karplus-Strong plucked string.
70    Pluck,
71    /// Waveshaping distortion.
72    Distortion,
73    /// Bit / sample-rate crusher.
74    Bitcrush,
75    /// Swept allpass phaser.
76    Phaser,
77    /// Ring modulator, crossfaded against its carrier.
78    #[serde(rename = "ringmod")]
79    RingMod,
80    /// Formant (vocal-tract) oscillator.
81    Formant,
82    /// Swept comb flanger.
83    Flanger,
84    /// Amplitude tremolo.
85    Tremolo,
86    /// Pitch vibrato.
87    Vibrato,
88    /// Three-band tone control.
89    Eq,
90    /// Granular re-reader.
91    Granular,
92    /// Grain pitch shifter.
93    Shift,
94    /// Sidechain compressor.
95    Comp,
96    /// Sidechain ducker.
97    Duck,
98    /// Keyed noise gate.
99    Gate,
100    /// Carrier/modulator vocoder.
101    Vocoder,
102}
103
104impl NodeKind {
105    /// Is this a source (leaf) kind?
106    pub fn is_source(self) -> bool {
107        matches!(
108            self,
109            NodeKind::Vco
110                | NodeKind::Supersaw
111                | NodeKind::Noise
112                | NodeKind::Wavetable
113                | NodeKind::Pluck
114                | NodeKind::Formant
115        )
116    }
117}
118
119/// A modulation choice for [`StructOp::SetMod`].
120///
121/// The first five are **sources**: they replace whatever is in the slot. The
122/// eleven below them are **shapers**, and setting one *wraps* the slot's
123/// current term rather than discarding it — placing a quantizer on a cable
124/// that already carries an S&H is the gesture, and asking the panel to send a
125/// whole [`ModNode`] through [`StructOp::SetModTree`] to express it would make
126/// the common edit the awkward one.
127#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
128#[serde(rename_all = "snake_case")]
129pub enum ModKind {
130    /// No modulation.
131    None,
132    /// LFO.
133    Lfo,
134    /// Attack/decay envelope.
135    Env,
136    /// Sample-and-hold random source.
137    Rand,
138    /// Envelope follower on the owning module's own input.
139    Follow,
140    /// Clocked euclidean gate pattern.
141    Euclid,
142    /// Wrap the slot in a scale quantizer.
143    Quantize,
144    /// Wrap the slot in a slew limiter.
145    Slew,
146    /// Wrap the slot in a rectifier.
147    Rectify,
148    /// Wrap the slot in a clocked sample-and-hold.
149    Hold,
150    /// Combine the slot with a second modulator, taking the lower.
151    Min,
152    /// …the higher.
153    Max,
154    /// …their gate AND.
155    And,
156    /// …their gate OR.
157    Or,
158    /// …their gate XOR.
159    Xor,
160    /// …switching between them.
161    Switch,
162}
163
164impl ModKind {
165    /// The unary CV processor this kind wraps the slot in, if any.
166    fn as_op(self) -> Option<ModOp> {
167        Some(match self {
168            ModKind::Quantize => ModOp::Quantize,
169            ModKind::Slew => ModOp::Slew,
170            ModKind::Rectify => ModOp::Rectify,
171            ModKind::Hold => ModOp::Hold,
172            _ => return None,
173        })
174    }
175
176    /// The binary CV combiner this kind wraps the slot in, if any.
177    fn as_pair(self) -> Option<PairOp> {
178        Some(match self {
179            ModKind::Min => PairOp::Min,
180            ModKind::Max => PairOp::Max,
181            ModKind::And => PairOp::And,
182            ModKind::Or => PairOp::Or,
183            ModKind::Xor => PairOp::Xor,
184            ModKind::Switch => PairOp::Switch,
185            _ => return None,
186        })
187    }
188}
189
190/// Default knob values for a hand-placed [`ModOp`], as `(p0, p1)`.
191///
192/// Every one is chosen so the module is audibly doing something the instant it
193/// lands, on the same argument as [`default_node`]'s second branches.
194fn default_op_params(kind: ModOp) -> (f64, f64) {
195    match kind {
196        // Root C, and **minor** rather than chromatic: a chromatic quantizer
197        // on a random source is a random source with extra steps, and minor is
198        // the scale that reads as deliberate on the first note.
199        ModOp::Quantize => (0.0, 2.5 / 7.0),
200        // A 0.16 s glide (quiver's `0.001 + cv²·10` under `map::slew_time`),
201        // symmetric — long enough to hear as portamento between S&H steps,
202        // short enough that an LFO still arrives.
203        ModOp::Slew => (0.2, 0.2),
204        // Full-wave: the only mode that changes a bipolar modulator's shape
205        // rather than gating half of it away.
206        ModOp::Rectify => (0.0, 0.0),
207        // ≈115 BPM on `map::clock_rate`, i.e. about two samples a second —
208        // slow enough that the steps are individually audible.
209        ModOp::Hold => (0.5, 0.0),
210    }
211}
212
213/// The second branch a hand-placed [`PairOp`] gets.
214///
215/// A euclidean gate for the three logic ops and the switch, because those four
216/// only mean anything against something that crosses the 2.5 V gate threshold
217/// on a rhythm; a slow triangle LFO for min and max, which are envelope
218/// arithmetic and want a continuous partner.
219fn default_pair_b(kind: PairOp) -> ModNode {
220    if kind.is_gate() || kind == PairOp::Switch {
221        ModNode::Euclid {
222            uid: Uid::NEW,
223            rate: 0.5,
224            steps: 0.5,
225            pulses: 0.4,
226        }
227    } else {
228        ModNode::Lfo {
229            uid: Uid::NEW,
230            wave: Waveform::Triangle,
231            rate: 0.3,
232        }
233    }
234}
235
236/// One structural edit.
237#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
238#[serde(tag = "op", rename_all = "snake_case")]
239pub enum StructOp {
240    /// Replace the node at `key` with a `kind` (subtrees preserved where the
241    /// sorts allow; replacing a source with a processor wraps the source).
242    Replace {
243        /// Node key.
244        key: String,
245        /// New kind.
246        kind: NodeKind,
247    },
248    /// Insert a processor/mix between the node at `key` and its parent
249    /// (i.e., into the wire toward the output).
250    Insert {
251        /// Node key.
252        key: String,
253        /// Inserted kind (must not be a source).
254        kind: NodeKind,
255    },
256    /// Delete the node at `key`, splicing its (primary) input up.
257    Delete {
258        /// Node key.
259        key: String,
260    },
261    /// Set the modulation slot of the module at `key` (everything but the
262    /// two slotless binary nodes has one).
263    ///
264    /// `key` addresses the **audio** module that owns the slot, not the slot
265    /// itself. A source kind replaces the slot's whole term; a shaper wraps
266    /// it — see [`ModKind`]. To edit deeper into a chain, or to install one
267    /// wholesale, use [`StructOp::SetModTree`].
268    SetMod {
269        /// Node key.
270        key: String,
271        /// New modulation kind.
272        kind: ModKind,
273    },
274    /// Swap the two audio inputs of the binary node at `key`.
275    ///
276    /// Named for the only production that accepted it when it was written; it
277    /// now applies to all six two-input kinds, because "swap the two inputs"
278    /// is a musical move on every one of them and the menu has always
279    /// offered it on every one of them.
280    SwapMix {
281        /// Node key.
282        key: String,
283    },
284    /// Replace the subtree at `key` with an explicit fragment (the wire
285    /// gesture "plug this staged chain in here, discard what was there" —
286    /// callers park the old subtree client-side).
287    ReplaceTree {
288        /// Node key.
289        key: String,
290        /// The fragment to install.
291        node: AudioNode,
292    },
293    /// Insert an explicit processor/mix fragment into the wire between
294    /// `key` and its parent; the old subtree becomes the fragment's primary
295    /// input (a Mix keeps its own `b` branch).
296    InsertTree {
297        /// Node key.
298        key: String,
299        /// The fragment to graft in (must not be a source).
300        node: AudioNode,
301    },
302    /// Install an explicit modulation fragment on the module at `key`.
303    SetModTree {
304        /// Node key.
305        key: String,
306        /// The modulation term.
307        m: ModNode,
308    },
309}
310
311/// Why a structural edit was rejected.
312#[derive(Debug, Error)]
313pub enum StructError {
314    /// No node at that key.
315    #[error("no node at {0}")]
316    NoSuchNode(String),
317    /// The operation does not apply to this node kind.
318    #[error("{0}")]
319    Invalid(String),
320    /// The edit would exceed the size/depth ceilings.
321    #[error("patch would exceed limits ({0} nodes max, depth {1})")]
322    TooBig(usize, usize),
323    /// The edit would stack more CV processors on one cable than the realtime
324    /// voice is willing to carry.
325    #[error("modulation chain would exceed depth {0}")]
326    ModTooDeep(usize),
327    /// A continuous site would be seated outside its declared range
328    /// ([`crate::PARAM_DOMAIN`]).
329    ///
330    /// Reported by [`validate_tree`], the predicate. It is deliberately *not*
331    /// how `apply_struct_op` and the whole-tree replace behave — see
332    /// [`PatchTree::clamp_domains`] for why a domain fault is repaired rather
333    /// than refused.
334    #[error("{0} is out of range at {1} (every knob is normalized 0–1)")]
335    OutOfDomain(f64, String),
336}
337
338fn default_node(kind: NodeKind, input: Option<AudioNode>) -> AudioNode {
339    let boxed = |n: Option<AudioNode>| Box::new(n.unwrap_or_else(|| saw_vco(0)));
340    match kind {
341        NodeKind::Vco => saw_vco(0),
342        NodeKind::Supersaw => AudioNode::Supersaw {
343            uid: Uid::NEW,
344            octave: 0,
345            detune: 0.35,
346            mix: 0.5,
347            mod_depth: 0.3,
348            modulation: ModNode::None,
349        },
350        NodeKind::Noise => AudioNode::Noise {
351            uid: Uid::NEW,
352            color: NoiseColor::White,
353        },
354        NodeKind::Mix => AudioNode::Mix {
355            uid: Uid::NEW,
356            balance: 0.5,
357            a: boxed(input),
358            b: Box::new(AudioNode::Vco {
359                uid: Uid::NEW,
360                wave: Waveform::Triangle,
361                octave: 0,
362                detune: 0.5,
363                mod_depth: 0.3,
364                modulation: ModNode::None,
365            }),
366        },
367        NodeKind::Filter => AudioNode::Filter {
368            uid: Uid::NEW,
369            kind: FilterKind::SvfLp,
370            cutoff: 0.6,
371            resonance: 0.3,
372            mod_depth: 0.3,
373            input: boxed(input),
374            modulation: ModNode::None,
375        },
376        NodeKind::Fold => AudioNode::Fold {
377            uid: Uid::NEW,
378            threshold: 0.5,
379            mod_depth: 0.3,
380            input: boxed(input),
381            modulation: ModNode::None,
382        },
383        NodeKind::Delay => AudioNode::Delay {
384            uid: Uid::NEW,
385            time: 0.35,
386            feedback: 0.35,
387            mix: 0.35,
388            mod_depth: 0.3,
389            input: boxed(input),
390            modulation: ModNode::None,
391        },
392        NodeKind::Chorus => AudioNode::Chorus {
393            uid: Uid::NEW,
394            rate: 0.3,
395            depth: 0.4,
396            mix: 0.35,
397            mod_depth: 0.3,
398            input: boxed(input),
399            modulation: ModNode::None,
400        },
401        NodeKind::Reverb => AudioNode::Reverb {
402            uid: Uid::NEW,
403            size: 0.5,
404            damp: 0.5,
405            mix: 0.3,
406            mod_depth: 0.3,
407            input: boxed(input),
408            modulation: ModNode::None,
409        },
410        NodeKind::Wavetable => AudioNode::Wavetable {
411            uid: Uid::NEW,
412            table: TableShape::Saw,
413            octave: 0,
414            morph: 0.35,
415            mod_depth: 0.3,
416            modulation: ModNode::None,
417        },
418        NodeKind::Pluck => AudioNode::Pluck {
419            uid: Uid::NEW,
420            octave: 0,
421            damping: 0.45,
422            brightness: 0.6,
423            mod_depth: 0.3,
424            modulation: ModNode::None,
425        },
426        NodeKind::Distortion => AudioNode::Distortion {
427            uid: Uid::NEW,
428            drive: 0.45,
429            tone: 0.5,
430            mode: DriveMode::Soft,
431            mod_depth: 0.3,
432            input: boxed(input),
433            modulation: ModNode::None,
434        },
435        NodeKind::Bitcrush => AudioNode::Bitcrush {
436            uid: Uid::NEW,
437            bits: 0.55,
438            downsample: 0.3,
439            mod_depth: 0.3,
440            input: boxed(input),
441            modulation: ModNode::None,
442        },
443        NodeKind::Phaser => AudioNode::Phaser {
444            uid: Uid::NEW,
445            rate: 0.3,
446            depth: 0.6,
447            feedback: 0.5,
448            mod_depth: 0.3,
449            input: boxed(input),
450            modulation: ModNode::None,
451        },
452        NodeKind::RingMod => AudioNode::RingMod {
453            uid: Uid::NEW,
454            mix: 0.5,
455            a: boxed(input),
456            // A sine an octave up, not a copy of the carrier: ring-modulating
457            // a signal against itself squares it, which is a quiet, dull
458            // module that looks broken. The default has to *ring*.
459            b: Box::new(AudioNode::Vco {
460                uid: Uid::NEW,
461                wave: Waveform::Sine,
462                octave: 1,
463                detune: 0.5,
464                mod_depth: 0.3,
465                modulation: ModNode::None,
466            }),
467        },
468        NodeKind::Formant => AudioNode::Formant {
469            uid: Uid::NEW,
470            // Off the /a/ end: at vowel 0 the mod slot can only sweep one
471            // way, and a formant oscillator parked on a single vowel is a
472            // static filter bank.
473            vowel: 0.3,
474            shift: 0.5,
475            octave: 0,
476            mod_depth: 0.3,
477            modulation: ModNode::None,
478        },
479        NodeKind::Flanger => AudioNode::Flanger {
480            uid: Uid::NEW,
481            rate: 0.35,
482            depth: 0.6,
483            // Bipolar: 0.62 is a gentle *positive* 0.17, enough to hear the
484            // comb without the module announcing itself as a jet.
485            feedback: 0.62,
486            mod_depth: 0.3,
487            input: boxed(input),
488            modulation: ModNode::None,
489        },
490        NodeKind::Tremolo => AudioNode::Tremolo {
491            uid: Uid::NEW,
492            rate: 0.4,
493            depth: 0.5,
494            shape: 0.0,
495            mod_depth: 0.3,
496            input: boxed(input),
497            modulation: ModNode::None,
498        },
499        NodeKind::Vibrato => AudioNode::Vibrato {
500            uid: Uid::NEW,
501            rate: 0.45,
502            depth: 0.25,
503            // Fully wet. A half-wet vibrato *is* a chorus, and shipping the
504            // default at 0.5 would erase the distinction between the two
505            // modules on the very first click.
506            mix: 1.0,
507            mod_depth: 0.3,
508            input: boxed(input),
509            modulation: ModNode::None,
510        },
511        NodeKind::Eq => AudioNode::Eq {
512            uid: Uid::NEW,
513            // All three bands at centre, i.e. 0 dB: a freshly placed tone
514            // control is audibly a no-op until you move it, which is correct
515            // for a tone control and better than hiding it behind a tilt
516            // nobody asked for.
517            low: 0.5,
518            mid: 0.5,
519            high: 0.5,
520            mod_depth: 0.3,
521            input: boxed(input),
522            modulation: ModNode::None,
523        },
524        NodeKind::Granular => AudioNode::Granular {
525            uid: Uid::NEW,
526            position: 0.5,
527            size: 0.4,
528            density: 0.6,
529            mod_depth: 0.3,
530            input: boxed(input),
531            modulation: ModNode::None,
532        },
533        NodeKind::Shift => AudioNode::Shift {
534            uid: Uid::NEW,
535            // Off unison, or the module is a wire on first placement: 0.62 is
536            // a bright +3 semitones, an interval rather than a detune.
537            semis: 0.62,
538            window: 0.5,
539            // Half wet, so the harmony arrives *against* the original rather
540            // than replacing it — which is what a shifter in a patch is for.
541            mix: 0.5,
542            mod_depth: 0.3,
543            input: boxed(input),
544            modulation: ModNode::None,
545        },
546        // The four below default their `/1` branch to something that makes the
547        // module audibly do its job the instant it lands. A compressor keyed
548        // off a copy of its own input is a gain trim; a ducker keyed off a pad
549        // is a slow tremolo; a vocoder on a sine carrier is silence. The
550        // second branch is the point of these modules, so the default has to
551        // demonstrate it.
552        NodeKind::Comp => AudioNode::Comp {
553            uid: Uid::NEW,
554            // 0.3 is ≈0.2 V of detector level, just under a plucked string's
555            // own envelope peak — measured, because on the geometric knob
556            // (`map::detector_volts`) the difference between 0.3 and 0.4 is
557            // the difference between a compressor that pumps and one that is
558            // a wire. 10.5:1 above it, which is limiting rather than gluing,
559            // because a sidechain compressor that only just moves is one
560            // nobody can hear working.
561            threshold: 0.3,
562            ratio: 0.5,
563            makeup: 0.4,
564            mod_depth: 0.3,
565            input: boxed(input),
566            // A pluck, like the ducker's and the gate's. A *sustained*
567            // sidechain — the obvious choice, and what an earlier draft of
568            // this table had — makes the compressor a static gain trim: the
569            // detector settles inside the first 20 ms and never moves again,
570            // so the module reviews as correct and does nothing. Sidechain
571            // compression is a transient pushing a level down and letting it
572            // come back, and only a pluck has the transient.
573            sidechain: Box::new(pluck_key()),
574            modulation: ModNode::None,
575        },
576        NodeKind::Duck => AudioNode::Duck {
577            uid: Uid::NEW,
578            amount: 0.7,
579            threshold: 0.4,
580            release: 0.35,
581            mod_depth: 0.3,
582            input: boxed(input),
583            key: Box::new(pluck_key()),
584            modulation: ModNode::None,
585        },
586        NodeKind::Gate => AudioNode::Gate {
587            uid: Uid::NEW,
588            // 0.45 is ≈0.4 V, in the middle of the band where a plucked key
589            // opens the gate on its attack and lets it shut again as the
590            // string decays. Below ≈0.42 it never shuts and above ≈0.47 it
591            // never opens; both were measured, and both read on the faceplate
592            // as a fixed −10 dB pad rather than as a gate.
593            threshold: 0.45,
594            range: 0.7,
595            release: 0.3,
596            mod_depth: 0.3,
597            input: boxed(input),
598            sidechain: Box::new(pluck_key()),
599            modulation: ModNode::None,
600        },
601        NodeKind::Vocoder => AudioNode::Vocoder {
602            uid: Uid::NEW,
603            bands: 0.6,
604            attack: 0.25,
605            release: 0.3,
606            mod_depth: 0.3,
607            // A supersaw carrier because a vocoder can only reveal spectrum
608            // the carrier already has — on a sine there is nothing in fifteen
609            // of the sixteen bands to reveal.
610            carrier: Box::new(AudioNode::Supersaw {
611                uid: Uid::NEW,
612                octave: 0,
613                detune: 0.45,
614                mix: 0.6,
615                mod_depth: 0.3,
616                modulation: ModNode::None,
617            }),
618            // A formant oscillator as the modulator, because the vowel is what
619            // makes a vocoder audibly a vocoder rather than a moving filter.
620            modulator: Box::new(AudioNode::Formant {
621                uid: Uid::NEW,
622                vowel: 0.3,
623                shift: 0.5,
624                octave: 0,
625                mod_depth: 0.3,
626                modulation: ModNode::None,
627            }),
628            modulation: ModNode::None,
629        },
630    }
631}
632
633/// The default key/sidechain branch for the ducker and the gate.
634///
635/// A pluck, deliberately: it is the only source in the palette with a sharp
636/// transient *and* a decay, which is exactly the envelope a ducker or a gate
637/// needs in order to visibly do something on the first note. A sustained
638/// source keys them into a static gain change nobody can hear as an effect.
639fn pluck_key() -> AudioNode {
640    AudioNode::Pluck {
641        uid: Uid::NEW,
642        octave: -1,
643        damping: 0.4,
644        brightness: 0.7,
645        mod_depth: 0.3,
646        modulation: ModNode::None,
647    }
648}
649
650/// The grammar's fallback source: a plain saw at the given octave.
651///
652/// Extracted because `default_node` names it three times and it gained two
653/// fields in wave 2A — three places to forget one of them.
654fn saw_vco(octave: i8) -> AudioNode {
655    AudioNode::Vco {
656        uid: Uid::NEW,
657        wave: Waveform::Saw,
658        octave,
659        detune: 0.5,
660        mod_depth: 0.3,
661        modulation: ModNode::None,
662    }
663}
664
665fn primary_input(n: AudioNode) -> Option<AudioNode> {
666    match n {
667        AudioNode::Vco { .. }
668        | AudioNode::Supersaw { .. }
669        | AudioNode::Noise { .. }
670        | AudioNode::Wavetable { .. }
671        | AudioNode::Pluck { .. }
672        | AudioNode::Formant { .. }
673        | AudioNode::Silence { .. } => None,
674        // For a ring modulator the carrier is the primary input, exactly as
675        // `a` is for a mix — the modulator is the branch that gets dropped.
676        AudioNode::Mix { a, .. } | AudioNode::RingMod { a, .. } => Some(*a),
677        // Same rule on the 2B binaries, and here it is not even a choice:
678        // `/1` is a control signal, so the branch that survives a splice is
679        // always the one you were listening to.
680        AudioNode::Comp { input, .. }
681        | AudioNode::Duck { input, .. }
682        | AudioNode::Gate { input, .. } => Some(*input),
683        AudioNode::Vocoder { carrier, .. } => Some(*carrier),
684        AudioNode::Shift { input, .. }
685        | AudioNode::Filter { input, .. }
686        | AudioNode::Fold { input, .. }
687        | AudioNode::Delay { input, .. }
688        | AudioNode::Chorus { input, .. }
689        | AudioNode::Reverb { input, .. }
690        | AudioNode::Distortion { input, .. }
691        | AudioNode::Bitcrush { input, .. }
692        | AudioNode::Phaser { input, .. }
693        | AudioNode::Flanger { input, .. }
694        | AudioNode::Tremolo { input, .. }
695        | AudioNode::Vibrato { input, .. }
696        | AudioNode::Eq { input, .. }
697        | AudioNode::Granular { input, .. } => Some(*input),
698    }
699}
700
701/// Parse a node key (`node`, `node/0`, `node/0/1`) into a child-index path.
702fn parse_key(key: &str) -> Option<Vec<usize>> {
703    let rest = key.strip_prefix("node")?;
704    if rest.is_empty() {
705        return Some(Vec::new());
706    }
707    rest.strip_prefix('/')?
708        .split('/')
709        .map(|s| s.parse::<usize>().ok())
710        .collect()
711}
712
713fn child_mut(n: &mut AudioNode, i: usize) -> Option<&mut AudioNode> {
714    match n {
715        AudioNode::Mix { a, b, .. } | AudioNode::RingMod { a, b, .. } => match i {
716            0 => Some(a),
717            1 => Some(b),
718            _ => None,
719        },
720        // `/0` signal, `/1` control — the same two indices `describe` labels
721        // and `genome` encodes, so a key like `node/1/0` addresses the same
722        // node in all three.
723        AudioNode::Comp {
724            input,
725            sidechain: other,
726            ..
727        }
728        | AudioNode::Gate {
729            input,
730            sidechain: other,
731            ..
732        }
733        | AudioNode::Duck {
734            input, key: other, ..
735        }
736        | AudioNode::Vocoder {
737            carrier: input,
738            modulator: other,
739            ..
740        } => match i {
741            0 => Some(input),
742            1 => Some(other),
743            _ => None,
744        },
745        AudioNode::Shift { input, .. }
746        | AudioNode::Filter { input, .. }
747        | AudioNode::Fold { input, .. }
748        | AudioNode::Delay { input, .. }
749        | AudioNode::Chorus { input, .. }
750        | AudioNode::Reverb { input, .. }
751        | AudioNode::Distortion { input, .. }
752        | AudioNode::Bitcrush { input, .. }
753        | AudioNode::Phaser { input, .. }
754        | AudioNode::Flanger { input, .. }
755        | AudioNode::Tremolo { input, .. }
756        | AudioNode::Vibrato { input, .. }
757        | AudioNode::Eq { input, .. }
758        | AudioNode::Granular { input, .. } => (i == 0).then_some(input),
759        _ => None,
760    }
761}
762
763fn node_at_mut<'a>(root: &'a mut AudioNode, path: &[usize]) -> Option<&'a mut AudioNode> {
764    let mut cur = root;
765    for &i in path {
766        cur = child_mut(cur, i)?;
767    }
768    Some(cur)
769}
770
771fn take(n: &mut AudioNode) -> AudioNode {
772    std::mem::replace(
773        n,
774        AudioNode::Noise {
775            uid: Uid::NEW,
776            color: NoiseColor::White,
777        },
778    )
779}
780
781/// Apply a structural edit, returning the new tree.
782pub fn apply_struct_op(tree: &PatchTree, op: &StructOp) -> Result<PatchTree, StructError> {
783    let mut out = tree.clone();
784    match op {
785        StructOp::Replace { key, kind } => {
786            let path = parse_key(key).ok_or_else(|| StructError::NoSuchNode(key.clone()))?;
787            let slot = node_at_mut(&mut out.root, &path)
788                .ok_or_else(|| StructError::NoSuchNode(key.clone()))?;
789            let old = take(slot);
790            *slot = if kind.is_source() {
791                // Source kinds swap in place; any old subtree is dropped.
792                default_node(*kind, None)
793            } else {
794                // Processor/mix keeps the old primary input; replacing a
795                // source wraps that source.
796                let input = match primary_input(old.clone()) {
797                    Some(i) => Some(i),
798                    None => Some(old),
799                };
800                default_node(*kind, input)
801            };
802        }
803        StructOp::Insert { key, kind } => {
804            if kind.is_source() {
805                return Err(StructError::Invalid(
806                    "sources cannot be inserted into a wire — use replace, or insert a mix".into(),
807                ));
808            }
809            let path = parse_key(key).ok_or_else(|| StructError::NoSuchNode(key.clone()))?;
810            let slot = node_at_mut(&mut out.root, &path)
811                .ok_or_else(|| StructError::NoSuchNode(key.clone()))?;
812            let old = take(slot);
813            *slot = default_node(*kind, Some(old));
814        }
815        StructOp::Delete { key } => {
816            let path = parse_key(key).ok_or_else(|| StructError::NoSuchNode(key.clone()))?;
817            // Deleting one branch of a binary node collapses it to the
818            // sibling. On the 2B family that reads exactly right in the
819            // direction people actually use: pulling the key out of a ducker
820            // leaves the pad, which is what "remove the ducking" means.
821            if let Some((&last, parent_path)) = path.split_last() {
822                let parent = node_at_mut(&mut out.root, parent_path)
823                    .ok_or_else(|| StructError::NoSuchNode(key.clone()))?;
824                if let Some((a, b)) = binary_children_mut(parent) {
825                    let keep = take(if last == 0 { b } else { a });
826                    *parent = keep;
827                    return finish(out);
828                }
829            }
830            let slot = node_at_mut(&mut out.root, &path)
831                .ok_or_else(|| StructError::NoSuchNode(key.clone()))?;
832            let old = take(slot);
833            match primary_input(old) {
834                Some(input) => *slot = input,
835                None => {
836                    return Err(StructError::Invalid(
837                        "a lone source cannot be deleted — replace it instead".into(),
838                    ))
839                }
840            }
841        }
842        StructOp::SetMod { key, kind } => {
843            let path = parse_key(key).ok_or_else(|| StructError::NoSuchNode(key.clone()))?;
844            let slot = node_at_mut(&mut out.root, &path)
845                .ok_or_else(|| StructError::NoSuchNode(key.clone()))?;
846            let m = mod_slot_mut(slot)?;
847            // A shaper wraps what is already there; a source replaces it. An
848            // empty slot has nothing to wrap, so a shaper placed on one gets
849            // the default source underneath — an S&H, which is the modulator
850            // every one of these eleven exists to make musical.
851            let existing = std::mem::take(m);
852            let inner = move || match existing {
853                ModNode::None => ModNode::Rand {
854                    uid: Uid::NEW,
855                    rate: 0.4,
856                    glide: 0.0,
857                },
858                existing => existing,
859            };
860            let replacement = if let Some(op) = kind.as_op() {
861                let (p0, p1) = default_op_params(op);
862                ModNode::Op {
863                    uid: Uid::NEW,
864                    kind: op,
865                    p0,
866                    p1,
867                    input: Box::new(inner()),
868                }
869            } else if let Some(pair) = kind.as_pair() {
870                ModNode::Pair {
871                    uid: Uid::NEW,
872                    kind: pair,
873                    a: Box::new(inner()),
874                    b: Box::new(default_pair_b(pair)),
875                }
876            } else {
877                match kind {
878                    ModKind::Lfo => ModNode::Lfo {
879                        uid: Uid::NEW,
880                        wave: Waveform::Triangle,
881                        rate: 0.4,
882                    },
883                    ModKind::Env => ModNode::Env {
884                        uid: Uid::NEW,
885                        attack: 0.2,
886                        decay: 0.5,
887                    },
888                    ModKind::Rand => ModNode::Rand {
889                        uid: Uid::NEW,
890                        rate: 0.4,
891                        glide: 0.0,
892                    },
893                    ModKind::Follow => ModNode::Follow {
894                        uid: Uid::NEW,
895                        sens: 0.5,
896                        release: 0.4,
897                    },
898                    ModKind::Euclid => ModNode::Euclid {
899                        uid: Uid::NEW,
900                        rate: 0.5,
901                        steps: 0.5,
902                        pulses: 0.4,
903                    },
904                    // `None` and the ten handled above.
905                    _ => ModNode::None,
906                }
907            };
908            *m = replacement;
909        }
910        StructOp::SwapMix { key } => {
911            let path = parse_key(key).ok_or_else(|| StructError::NoSuchNode(key.clone()))?;
912            let slot = node_at_mut(&mut out.root, &path)
913                .ok_or_else(|| StructError::NoSuchNode(key.clone()))?;
914            // Mix is the one binary whose knob is anchored to a *side*: the
915            // crossfade has to mirror or an edit that only reorders the two
916            // branches would also change the balance you hear. Every other
917            // binary's knob names a process (threshold, amount, dry/wet), not
918            // a side, so it stays put — and on those four the swap is the
919            // whole point: exchanging a ducker's `in` and `key` is the
920            // difference between the pad ducking under the kick and the kick
921            // ducking under the pad, and there was no other way to say it.
922            if let AudioNode::Mix { balance, .. } = slot {
923                *balance = 1.0 - *balance;
924            }
925            let Some((a, b)) = binary_children_mut(slot) else {
926                return Err(StructError::Invalid(
927                    "this module has only one input — there is nothing to swap".into(),
928                ));
929            };
930            std::mem::swap(a, b);
931        }
932        StructOp::ReplaceTree { key, node } => {
933            let path = parse_key(key).ok_or_else(|| StructError::NoSuchNode(key.clone()))?;
934            let slot = node_at_mut(&mut out.root, &path)
935                .ok_or_else(|| StructError::NoSuchNode(key.clone()))?;
936            *slot = node.clone();
937        }
938        StructOp::InsertTree { key, node } => {
939            let path = parse_key(key).ok_or_else(|| StructError::NoSuchNode(key.clone()))?;
940            let slot = node_at_mut(&mut out.root, &path)
941                .ok_or_else(|| StructError::NoSuchNode(key.clone()))?;
942            let old = take(slot);
943            *slot = graft(node.clone(), old)?;
944        }
945        StructOp::SetModTree { key, m } => {
946            let path = parse_key(key).ok_or_else(|| StructError::NoSuchNode(key.clone()))?;
947            let slot = node_at_mut(&mut out.root, &path)
948                .ok_or_else(|| StructError::NoSuchNode(key.clone()))?;
949            // Normalized, because this is the one edit that installs a whole
950            // modulation term the panel built: a `Pair` with an empty branch
951            // or an `Op` over nothing is a rack module that cannot make a
952            // sound, and the prior can only rule those out for terms it drew
953            // itself. See [`ModNode::normalized`].
954            *mod_slot_mut(slot)? = m.clone().normalized();
955        }
956    }
957    finish(out)
958}
959
960/// The two audio children of a binary node, `(/0, /1)`, or `None` for
961/// everything else.
962///
963/// Six productions are binary now, so the shape that used to be one `if let`
964/// pattern in `Delete` is worth a name. `/0` is always the branch that carries
965/// the signal you hear.
966fn binary_children_mut(n: &mut AudioNode) -> Option<(&mut AudioNode, &mut AudioNode)> {
967    match n {
968        AudioNode::Mix { a, b, .. } | AudioNode::RingMod { a, b, .. } => Some((a, b)),
969        AudioNode::Comp {
970            input,
971            sidechain: other,
972            ..
973        }
974        | AudioNode::Gate {
975            input,
976            sidechain: other,
977            ..
978        }
979        | AudioNode::Duck {
980            input, key: other, ..
981        }
982        | AudioNode::Vocoder {
983            carrier: input,
984            modulator: other,
985            ..
986        } => Some((input, other)),
987        _ => None,
988    }
989}
990
991/// The modulation slot of a node, or an error for the two slotless binaries.
992///
993/// Ring mod and mix are the exceptions, and *only* they: both of their inputs
994/// are audio and their one knob is the blend, so there is no parameter left
995/// for a modulator to reach. Having two children is not itself
996/// disqualifying — the four 2B dynamics nodes take two subterms and still own
997/// a slot.
998fn mod_slot_mut(n: &mut AudioNode) -> Result<&mut ModNode, StructError> {
999    match n {
1000        // The two oldest sources joined this list in wave 2A: their slot goes
1001        // to *pitch*, which is the one modulation destination no processor in
1002        // the grammar can offer.
1003        AudioNode::Vco { modulation, .. }
1004        | AudioNode::Supersaw { modulation, .. }
1005        | AudioNode::Formant { modulation, .. }
1006        | AudioNode::Wavetable { modulation, .. }
1007        | AudioNode::Pluck { modulation, .. }
1008        | AudioNode::Filter { modulation, .. }
1009        | AudioNode::Fold { modulation, .. }
1010        | AudioNode::Delay { modulation, .. }
1011        | AudioNode::Chorus { modulation, .. }
1012        | AudioNode::Reverb { modulation, .. }
1013        | AudioNode::Distortion { modulation, .. }
1014        | AudioNode::Bitcrush { modulation, .. }
1015        | AudioNode::Phaser { modulation, .. }
1016        | AudioNode::Flanger { modulation, .. }
1017        | AudioNode::Tremolo { modulation, .. }
1018        | AudioNode::Vibrato { modulation, .. }
1019        | AudioNode::Eq { modulation, .. }
1020        | AudioNode::Granular { modulation, .. }
1021        | AudioNode::Shift { modulation, .. }
1022        | AudioNode::Comp { modulation, .. }
1023        | AudioNode::Duck { modulation, .. }
1024        | AudioNode::Gate { modulation, .. }
1025        | AudioNode::Vocoder { modulation, .. } => Ok(modulation),
1026        _ => Err(StructError::Invalid(
1027            "mixers and ring modulators have no modulation slot".into(),
1028        )),
1029    }
1030}
1031
1032/// Graft `old` into `frag`'s primary input slot (the binary nodes keep their
1033/// `/1` — the fragment's own second branch is what the user staged, and on the
1034/// dynamics family it is a control signal that has nothing to do with the wire
1035/// being spliced).
1036fn graft(frag: AudioNode, old: AudioNode) -> Result<AudioNode, StructError> {
1037    match frag {
1038        AudioNode::Mix { balance, b, .. } => Ok(AudioNode::Mix {
1039            uid: Uid::NEW,
1040            balance,
1041            a: Box::new(old),
1042            b,
1043        }),
1044        AudioNode::RingMod { mix, b, .. } => Ok(AudioNode::RingMod {
1045            uid: Uid::NEW,
1046            mix,
1047            a: Box::new(old),
1048            b,
1049        }),
1050        AudioNode::Comp {
1051            threshold,
1052            ratio,
1053            makeup,
1054            mod_depth,
1055            sidechain,
1056            modulation,
1057            ..
1058        } => Ok(AudioNode::Comp {
1059            uid: Uid::NEW,
1060            threshold,
1061            ratio,
1062            makeup,
1063            mod_depth,
1064            sidechain,
1065            modulation,
1066            input: Box::new(old),
1067        }),
1068        AudioNode::Duck {
1069            amount,
1070            threshold,
1071            release,
1072            mod_depth,
1073            key,
1074            modulation,
1075            ..
1076        } => Ok(AudioNode::Duck {
1077            uid: Uid::NEW,
1078            amount,
1079            threshold,
1080            release,
1081            mod_depth,
1082            key,
1083            modulation,
1084            input: Box::new(old),
1085        }),
1086        AudioNode::Gate {
1087            threshold,
1088            range,
1089            release,
1090            mod_depth,
1091            sidechain,
1092            modulation,
1093            ..
1094        } => Ok(AudioNode::Gate {
1095            uid: Uid::NEW,
1096            threshold,
1097            range,
1098            release,
1099            mod_depth,
1100            sidechain,
1101            modulation,
1102            input: Box::new(old),
1103        }),
1104        AudioNode::Vocoder {
1105            bands,
1106            attack,
1107            release,
1108            mod_depth,
1109            modulator,
1110            modulation,
1111            ..
1112        } => Ok(AudioNode::Vocoder {
1113            uid: Uid::NEW,
1114            bands,
1115            attack,
1116            release,
1117            mod_depth,
1118            modulator,
1119            modulation,
1120            carrier: Box::new(old),
1121        }),
1122        AudioNode::Shift {
1123            semis,
1124            window,
1125            mix,
1126            mod_depth,
1127            modulation,
1128            ..
1129        } => Ok(AudioNode::Shift {
1130            uid: Uid::NEW,
1131            semis,
1132            window,
1133            mix,
1134            mod_depth,
1135            modulation,
1136            input: Box::new(old),
1137        }),
1138        AudioNode::Filter {
1139            kind,
1140            cutoff,
1141            resonance,
1142            mod_depth,
1143            modulation,
1144            ..
1145        } => Ok(AudioNode::Filter {
1146            uid: Uid::NEW,
1147            kind,
1148            cutoff,
1149            resonance,
1150            mod_depth,
1151            modulation,
1152            input: Box::new(old),
1153        }),
1154        AudioNode::Fold {
1155            threshold,
1156            mod_depth,
1157            modulation,
1158            ..
1159        } => Ok(AudioNode::Fold {
1160            uid: Uid::NEW,
1161            threshold,
1162            mod_depth,
1163            modulation,
1164            input: Box::new(old),
1165        }),
1166        AudioNode::Delay {
1167            time,
1168            feedback,
1169            mix,
1170            mod_depth,
1171            modulation,
1172            ..
1173        } => Ok(AudioNode::Delay {
1174            uid: Uid::NEW,
1175            time,
1176            feedback,
1177            mix,
1178            mod_depth,
1179            modulation,
1180            input: Box::new(old),
1181        }),
1182        AudioNode::Chorus {
1183            rate,
1184            depth,
1185            mix,
1186            mod_depth,
1187            modulation,
1188            ..
1189        } => Ok(AudioNode::Chorus {
1190            uid: Uid::NEW,
1191            rate,
1192            depth,
1193            mix,
1194            mod_depth,
1195            modulation,
1196            input: Box::new(old),
1197        }),
1198        AudioNode::Reverb {
1199            size,
1200            damp,
1201            mix,
1202            mod_depth,
1203            modulation,
1204            ..
1205        } => Ok(AudioNode::Reverb {
1206            uid: Uid::NEW,
1207            size,
1208            damp,
1209            mix,
1210            mod_depth,
1211            modulation,
1212            input: Box::new(old),
1213        }),
1214        AudioNode::Distortion {
1215            drive,
1216            tone,
1217            mode,
1218            mod_depth,
1219            modulation,
1220            ..
1221        } => Ok(AudioNode::Distortion {
1222            uid: Uid::NEW,
1223            drive,
1224            tone,
1225            mode,
1226            mod_depth,
1227            modulation,
1228            input: Box::new(old),
1229        }),
1230        AudioNode::Bitcrush {
1231            bits,
1232            downsample,
1233            mod_depth,
1234            modulation,
1235            ..
1236        } => Ok(AudioNode::Bitcrush {
1237            uid: Uid::NEW,
1238            bits,
1239            downsample,
1240            mod_depth,
1241            modulation,
1242            input: Box::new(old),
1243        }),
1244        AudioNode::Phaser {
1245            rate,
1246            depth,
1247            feedback,
1248            mod_depth,
1249            modulation,
1250            ..
1251        } => Ok(AudioNode::Phaser {
1252            uid: Uid::NEW,
1253            rate,
1254            depth,
1255            feedback,
1256            mod_depth,
1257            modulation,
1258            input: Box::new(old),
1259        }),
1260        AudioNode::Flanger {
1261            rate,
1262            depth,
1263            feedback,
1264            mod_depth,
1265            modulation,
1266            ..
1267        } => Ok(AudioNode::Flanger {
1268            uid: Uid::NEW,
1269            rate,
1270            depth,
1271            feedback,
1272            mod_depth,
1273            modulation,
1274            input: Box::new(old),
1275        }),
1276        AudioNode::Tremolo {
1277            rate,
1278            depth,
1279            shape,
1280            mod_depth,
1281            modulation,
1282            ..
1283        } => Ok(AudioNode::Tremolo {
1284            uid: Uid::NEW,
1285            rate,
1286            depth,
1287            shape,
1288            mod_depth,
1289            modulation,
1290            input: Box::new(old),
1291        }),
1292        AudioNode::Vibrato {
1293            rate,
1294            depth,
1295            mix,
1296            mod_depth,
1297            modulation,
1298            ..
1299        } => Ok(AudioNode::Vibrato {
1300            uid: Uid::NEW,
1301            rate,
1302            depth,
1303            mix,
1304            mod_depth,
1305            modulation,
1306            input: Box::new(old),
1307        }),
1308        AudioNode::Eq {
1309            low,
1310            mid,
1311            high,
1312            mod_depth,
1313            modulation,
1314            ..
1315        } => Ok(AudioNode::Eq {
1316            uid: Uid::NEW,
1317            low,
1318            mid,
1319            high,
1320            mod_depth,
1321            modulation,
1322            input: Box::new(old),
1323        }),
1324        AudioNode::Granular {
1325            position,
1326            size,
1327            density,
1328            mod_depth,
1329            modulation,
1330            ..
1331        } => Ok(AudioNode::Granular {
1332            uid: Uid::NEW,
1333            position,
1334            size,
1335            density,
1336            mod_depth,
1337            modulation,
1338            input: Box::new(old),
1339        }),
1340        AudioNode::Vco { .. }
1341        | AudioNode::Supersaw { .. }
1342        | AudioNode::Noise { .. }
1343        | AudioNode::Wavetable { .. }
1344        | AudioNode::Pluck { .. }
1345        | AudioNode::Formant { .. }
1346        | AudioNode::Silence { .. } => Err(StructError::Invalid(
1347            "a source has no input to splice into".into(),
1348        )),
1349    }
1350}
1351
1352/// The deepest modulation chain anywhere in a subtree.
1353///
1354/// Walks through `mod_slot_mut`/`child_mut` rather than re-matching every
1355/// variant: those two already know which productions own a slot and which own
1356/// children, and a second copy of that table is a second place to forget a
1357/// module.
1358fn max_mod_depth_of(n: &mut AudioNode) -> usize {
1359    let mut best = mod_slot_mut(n).map(|m| m.depth()).unwrap_or(0);
1360    for i in 0..2 {
1361        if let Some(child) = child_mut(n, i) {
1362            best = best.max(max_mod_depth_of(child));
1363        }
1364    }
1365    best
1366}
1367
1368/// Every ceiling a hand-built patch has to respect, in one callable place.
1369///
1370/// [`apply_struct_op`] has always enforced these on its way out. The whole-tree
1371/// replace route (the wasm `edit_set_tree`, which is what undo/redo and every
1372/// client-side rewrite go through) never did — and that is precisely the route
1373/// a graph editor uses for move/reconnect. A forty-node hand-built patch is not
1374/// merely large: it sits outside the range the standardizer was fitted on, has
1375/// ~zero mass under the prior, and the next refinement mutates it straight back
1376/// inside these ceilings, so the structure the player built by hand evaporates
1377/// the first time they press evolve, silently. Same ceilings, both routes.
1378///
1379/// Takes a shared reference — a caller validating a tree does not necessarily
1380/// own it — and pays one clone of a ≤24-node term for it, because the mod-depth
1381/// walk reuses the `_mut` accessors that already know which productions carry a
1382/// slot rather than standing up a second copy of that table to drift.
1383///
1384/// **Also the parameter-domain predicate.** It used to speak only about size
1385/// and depth, which left the one thing a term can be wrong about that no other
1386/// gate looked at: a *value*. `amp.sustain = 1e30` walked through this function,
1387/// through `finish()`, into φ, into the exported PNG and into the persisted
1388/// observation log, and every surface downstream reported it as a number
1389/// ("SUSTAIN 1200.0 dB") because none of them had been told what a knob's range
1390/// is. Now they have, once, at [`crate::PARAM_DOMAIN`].
1391pub fn validate_tree(tree: &PatchTree) -> Result<(), String> {
1392    if let Some((addr, v)) = tree.domain_violations().into_iter().next() {
1393        return Err(StructError::OutOfDomain(v, addr).to_string());
1394    }
1395    let mut probe = tree.clone();
1396    check_ceilings(&mut probe).map_err(|e| e.to_string())
1397}
1398
1399fn check_ceilings(tree: &mut PatchTree) -> Result<(), StructError> {
1400    if tree.root.size() > MAX_SIZE || tree.root.depth() > MAX_DEPTH {
1401        return Err(StructError::TooBig(MAX_SIZE, MAX_DEPTH));
1402    }
1403    if max_mod_depth_of(&mut tree.root) > MAX_MOD_DEPTH {
1404        return Err(StructError::ModTooDeep(MAX_MOD_DEPTH));
1405    }
1406    Ok(())
1407}
1408
1409fn finish(mut tree: PatchTree) -> Result<PatchTree, StructError> {
1410    // Domains first, and repaired rather than refused. `ReplaceTree`,
1411    // `InsertTree` and `SetModTree` adopt a fragment the panel handed in
1412    // verbatim — including a fragment staged to HELD by a build that predates
1413    // this gate — so this is the funnel every explicit subtree passes through.
1414    // A `#[cfg(debug_assertions)]` shout is on the *engine's own* moves, in
1415    // `auracle_features::struct_features`: nothing this crate generates should
1416    // ever need repairing, and a silent clamp there would hide a real bug.
1417    tree.clamp_domains();
1418    check_ceilings(&mut tree)?;
1419    // Identity survives a structural edit for free, and the reason is worth
1420    // stating: [`apply_struct_op`] works on a *clone* of the incoming tree and
1421    // splices it in place, so every node that lives through the edit carries
1422    // its own `uid` across with it in the same `memmove` that carried its
1423    // knobs. The only nodes wanting an identity here are the ones this op just
1424    // made (`default_node`, the mod-term literals, the `take` placeholder) and
1425    // any subtree the panel handed in through `ReplaceTree`/`InsertTree` —
1426    // which is also where a duplicated uid could arrive. Settling covers both.
1427    tree.ensure_uids();
1428    Ok(tree)
1429}