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}