Skip to main content

auracle_grammar/
lib.rs

1//! # auracle-grammar
2//!
3//! The **patch prior**: a typed probabilistic context-free grammar (PCFG) over
4//! quiver-backed synthesizer patch terms, plus the compiler from sampled terms
5//! to playable quiver [`Patch`](quiver) graphs.
6//!
7//! The genome is a *term* ([`term::PatchTree`]), not a raw patch graph. The
8//! Audio/Mod sort distinction is enforced by the Rust type system — ill-sorted
9//! terms are unrepresentable — and the grammar ([`prior::PatchGrammarPrior`])
10//! is a fugue generative program, so all three levels of evolution live in one
11//! representation:
12//!
13//! - node settings   → leaf parameter sites (`F64`/`Usize` draws per module)
14//! - connectivity    → interior structure (chains, mix, modulation slots)
15//! - node set        → which module productions fire
16//!
17//! [`PatchTree`](term::PatchTree) implements fugue-evo's genome traits with a
18//! canonical trace encoding that **is** the grammar's address scheme, so
19//! subtree mutation/crossover are generic trace moves and tempered SMC / typed
20//! MH come for free.
21//!
22//! ## v1 constraints (the reference: *The genome*, *The vetting gate*)
23//!
24//! - Acyclic terms only — no feedback combinator productions. Modules with
25//!   *internal* feedback (delay, chorus) are allowed.
26//! - Curated palette: Vco, Supersaw, NoiseGenerator, Wavetable,
27//!   KarplusStrong, FormantOsc, Svf, DiodeLadderFilter, ParametricEq,
28//!   Wavefolder, Distortion, Bitcrusher, DelayLine, Chorus, Reverb, Phaser,
29//!   Flanger, Tremolo, Vibrato, Granular, PitchShifter, RingModulator,
30//!   Compressor, Ducker, NoiseGate, Vocoder, Adsr, Vca, Lfo, SampleAndHold,
31//!   SlewLimiter, EnvelopeFollower.
32//! - Every compiled patch gets the mandatory voice stage — amp ADSR → VCA →
33//!   **Limiter** → StereoOutput — and bounded parameter mappings (resonance,
34//!   feedback), so the grammar cannot express the most degenerate settings.
35
36pub mod compile;
37pub mod describe;
38pub mod diff;
39pub mod edit;
40pub mod genome;
41pub mod mutate;
42pub mod presets;
43pub mod prior;
44pub mod term;
45
46pub use compile::{compile, CompiledVoice, ParamHandle, ParamMap};
47pub use describe::{describe, RackDescription};
48pub use diff::{tree_diff, DiffEntry};
49pub use edit::{set_param, EditError, ParamValue};
50pub use genome::{in_domain, PARAM_DOMAIN};
51pub use mutate::{apply_struct_op, validate_tree, ModKind, NodeKind, StructError, StructOp};
52pub use presets::{preset_bank, presets, Category, Preset, CATEGORIES};
53pub use prior::PatchGrammarPrior;
54pub use term::{AudioNode, ModNode, PatchTree, Uid};
55
56#[cfg(test)]
57mod tests {
58    use super::*;
59    use fugue::runtime::handler::run;
60    use fugue::runtime::interpreters::{PriorHandler, ScoreGivenTrace};
61    use fugue::Trace;
62    use fugue_evo::genome::trace_genome::TraceGenome;
63    use fugue_evo::inference::prior::GenomePrior;
64    use rand::rngs::StdRng;
65    use rand::SeedableRng;
66
67    const SR: f64 = 44_100.0;
68
69    fn draw(prior: &PatchGrammarPrior, rng: &mut StdRng) -> (PatchTree, Trace) {
70        run(
71            PriorHandler {
72                rng,
73                trace: Trace::default(),
74            },
75            prior.model(),
76        )
77    }
78
79    /// M1 gate: every prior sample compiles to a valid quiver patch, and any
80    /// wiring warnings stay within the two known-benign classes (constant
81    /// bipolar Offset → unipolar knob, unipolar env → bipolar FM input).
82    #[test]
83    fn every_prior_sample_compiles() {
84        let prior = PatchGrammarPrior::default();
85        let mut rng = StdRng::seed_from_u64(1);
86        for i in 0..200 {
87            let (tree, trace) = draw(&prior, &mut rng);
88            assert!(trace.log_prior.is_finite(), "sample {i}: log_prior finite");
89            assert!(trace.log_prior < 0.0, "sample {i}: pays prior mass");
90            let voice = compile(&tree, SR).unwrap_or_else(|e| {
91                panic!("sample {i} failed to compile: {e}\n{}", tree.to_sexpr())
92            });
93            for w in &voice.warnings {
94                assert!(
95                    w.contains("Bipolar/Unipolar CV mismatch")
96                        || w.contains("Unipolar CV to V/Oct")
97                        || w.contains("may need offset adjustment")
98                        // S&H random wiring: noise (audio) into the CV
99                        // sampler, and the square clock into its trigger.
100                        || w.contains("Audio/CV connection")
101                        || w.contains("Audio to Gate/Trigger")
102                        // ±5 V square clock into the S&H trigger thresholds
103                        // cleanly at the 2.5 V gate level.
104                        || w.contains("Unusual connection: CvBipolar -> Trigger")
105                        // The note gate plucks the string. quiver edge-detects
106                        // the port, so a held gate excites once — which is the
107                        // behaviour this class warns *might* differ, and here
108                        // is exactly the behaviour wanted.
109                        || w.contains("Gate/Trigger connection")
110                        // `Adsr.shape`, `Vca.response` and `Limiter.soft` are
111                        // Gate-kind ports quiver reads as booleans at 2.5 V.
112                        // Pinning them is a baked `set_param_by_id` default
113                        // now (no cable, no warning), but the class stays
114                        // allowed for any remaining CvBipolar-into-Gate wiring.
115                        || w.contains("Unusual connection: CvBipolar -> Gate")
116                        // Wave 2C: modulation is a sort, so CV now meets CV
117                        // through quiver's utility modules, whose ports are
118                        // typed for the job they usually do rather than for
119                        // the one this grammar gives them. All six are volt
120                        // arithmetic on wires that are already in range.
121                        //
122                        // `Rectifier` and `VcSwitch` type their inputs as
123                        // Audio because they are usually waveshapers; here
124                        // they are fed a modulator or a gate, and `|x|` and
125                        // "pick one of two" do not read the signal kind.
126                        || w.contains("Unusual connection: CvUnipolar -> Audio")
127                        || w.contains("Unusual connection: Gate -> Audio")
128                        || w.contains("Unusual connection: Trigger -> Audio")
129                        // A 0–10 V modulator into a logic input, thresholded
130                        // at 2.5 V — which is the whole point of putting a
131                        // logic gate on a modulator.
132                        || w.contains("Unusual connection: CvUnipolar -> Gate")
133                        // A euclidean pattern or a logic output into the mod
134                        // cable's own attenuverter, or into `Min`/`Max`/the
135                        // sample-and-hold: 5 V arriving on a ±5 V wire.
136                        || w.contains("Unusual connection: Gate -> CvBipolar")
137                        || w.contains("Unusual connection: Trigger -> CvBipolar"),
138                    "sample {i}: unexpected warning class: {w}"
139                );
140            }
141        }
142    }
143
144    /// Compiled patches make sound and stay bounded: gate a note, tick a
145    /// second of audio, assert finite output everywhere, a bounded peak, and
146    /// that a healthy fraction of patches are audible.
147    #[test]
148    fn compiled_patches_sound_and_stay_bounded() {
149        let prior = PatchGrammarPrior::default();
150        let mut rng = StdRng::seed_from_u64(2);
151        let n = 24;
152        let mut audible = 0;
153        for i in 0..n {
154            let (tree, _) = draw(&prior, &mut rng);
155            let mut voice = compile(&tree, SR).expect("compiles");
156            voice.gate.set(5.0);
157            voice.pitch.set(0.0); // C4
158            let mut peak = 0.0f64;
159            let mut sum_sq = 0.0f64;
160            let ticks = SR as usize / 2; // half a second
161            for _ in 0..ticks {
162                let (l, r) = voice.patch.tick();
163                assert!(
164                    l.is_finite() && r.is_finite(),
165                    "sample {i}: non-finite output"
166                );
167                peak = peak.max(l.abs()).max(r.abs());
168                sum_sq += l * l;
169            }
170            // The limiter's ceiling is threshold·5 V ≤ 5 V; leave headroom for
171            // its release-time overshoot but fail on runaway.
172            assert!(peak <= 10.0, "sample {i}: peak {peak} exceeds bound");
173            let rms = (sum_sq / ticks as f64).sqrt();
174            if rms > 1e-3 {
175                audible += 1;
176            }
177        }
178        // Slow attacks and low sustains legitimately produce quiet patches;
179        // the vetting gate (M2) will quarantine them. But most must sound.
180        assert!(
181            audible * 2 > n,
182            "only {audible}/{n} patches audible in 1s — grammar is generating duds"
183        );
184    }
185
186    /// The canonical trace encoding is the exact inverse of the generative
187    /// program: choices match site-for-site, and replay-scoring the encoding
188    /// recovers the same PCFG log-prior. This pins `to_trace` to the grammar —
189    /// they cannot drift apart.
190    #[test]
191    fn to_trace_inverts_generative_run() {
192        let prior = PatchGrammarPrior::default();
193        let mut rng = StdRng::seed_from_u64(3);
194        for _ in 0..50 {
195            let (tree, gen_trace) = draw(&prior, &mut rng);
196            let enc = tree.to_trace();
197            assert_eq!(enc.choices.len(), gen_trace.choices.len());
198            for (addr, choice) in &gen_trace.choices {
199                assert_eq!(
200                    enc.choices[addr].value, choice.value,
201                    "encoding mismatch at {addr}"
202                );
203            }
204            let (replayed, scored) = run(
205                ScoreGivenTrace {
206                    base: enc,
207                    trace: Trace::default(),
208                },
209                prior.model(),
210            );
211            assert_eq!(replayed, tree);
212            assert!((scored.log_prior - gen_trace.log_prior).abs() < 1e-9);
213        }
214    }
215
216    /// `from_trace(to_trace(t)) == t` for prior draws and for the plain-RNG
217    /// sampler (the two samplers must agree on representable trees).
218    #[test]
219    fn trace_roundtrip() {
220        let prior = PatchGrammarPrior::default();
221        let mut rng = StdRng::seed_from_u64(4);
222        for _ in 0..50 {
223            let (tree, _) = draw(&prior, &mut rng);
224            let back = PatchTree::from_trace(&tree.to_trace()).expect("roundtrip");
225            assert_eq!(back, tree);
226        }
227        for _ in 0..50 {
228            let tree = prior.sample_with_rng(&mut rng);
229            let back = PatchTree::from_trace(&tree.to_trace()).expect("roundtrip");
230            assert_eq!(back, tree);
231            assert!(compile(&tree, SR).is_ok());
232        }
233    }
234
235    /// The two categorical sites that reach the running voices without a
236    /// recompile have a live handle on **every** module that advertises them.
237    ///
238    /// `oct` is live because it rides the pitch [`compile`]r's one `Offset`,
239    /// and every pitched source goes through `wire_pitch` to get there. A
240    /// future source that hand-wires its own pitch would still describe an
241    /// `oct` chip and would silently be back to a full patch swap per click —
242    /// which is invisible in a diff and audible as a dropout, so it is checked
243    /// here rather than left to be noticed.
244    #[test]
245    fn every_advertised_live_site_has_a_live_handle() {
246        let prior = PatchGrammarPrior::default();
247        let mut rng = StdRng::seed_from_u64(0x1_11E);
248        let mut seen = std::collections::BTreeSet::new();
249        for _ in 0..200 {
250            let tree = prior.sample_with_rng(&mut rng);
251            let rack = describe::describe(&tree);
252            let voice = compile(&tree, SR).expect("compiles");
253            for m in &rack.modules {
254                for knob in &m.knobs {
255                    let site = knob.addr.rsplit('#').next().unwrap_or("");
256                    if site != "table" && site != "oct" {
257                        continue;
258                    }
259                    seen.insert(site.to_string());
260                    assert!(
261                        voice.params.contains_key(&knob.addr),
262                        "{} advertises {site} with no live handle — clicking it \
263                         is a full patch swap",
264                        m.kind
265                    );
266                }
267            }
268        }
269        assert_eq!(
270            seen,
271            ["oct", "table"]
272                .iter()
273                .map(|s| s.to_string())
274                .collect::<std::collections::BTreeSet<_>>(),
275            "the draw never produced both live sites, so this proved nothing"
276        );
277    }
278
279    /// Every knob address in the rack description is a real trace site, every
280    /// continuous/enum knob is editable through it, and the edit is exactly a
281    /// one-site trace change (the panel cannot drift from the genome).
282    #[test]
283    fn rack_description_addresses_are_live() {
284        use fugue_evo::genome::trace_genome::ChoiceValue;
285        let prior = PatchGrammarPrior::default();
286        let mut rng = StdRng::seed_from_u64(11);
287        for _ in 0..50 {
288            let (tree, _) = draw(&prior, &mut rng);
289            let rack = describe::describe(&tree);
290            let trace = tree.to_trace();
291            for m in &rack.modules {
292                for a in &m.structural_addrs {
293                    assert!(
294                        trace.choices.keys().any(|k| &**k == a.as_str()),
295                        "structural addr {a} not in trace"
296                    );
297                }
298                for knob in &m.knobs {
299                    let found = trace
300                        .choices
301                        .iter()
302                        .find(|(k, _)| &***k == knob.addr.as_str())
303                        .unwrap_or_else(|| panic!("knob addr {} not in trace", knob.addr));
304                    let edited = match knob.kind {
305                        describe::KnobKind::Continuous => {
306                            assert!(matches!(found.1.value, ChoiceValue::F64(_)));
307                            set_param(&tree, &knob.addr, ParamValue::Continuous(0.5)).unwrap()
308                        }
309                        describe::KnobKind::Enum { .. } | describe::KnobKind::Octave => {
310                            assert!(matches!(found.1.value, ChoiceValue::Usize(_)));
311                            set_param(&tree, &knob.addr, ParamValue::Index(0)).unwrap()
312                        }
313                    };
314                    // The edit changes at most that one site.
315                    let d = tree_diff(&tree, &edited);
316                    assert!(d.len() <= 1, "edit at {} touched {:?}", knob.addr, d);
317                    assert!(compile(&edited, SR).is_ok());
318                }
319            }
320            // Wires reference existing modules only.
321            for w in &rack.wires {
322                assert!(rack.modules.iter().any(|m| m.key == w.from) || w.from == "node");
323                assert!(rack.modules.iter().any(|m| m.key == w.to));
324            }
325        }
326    }
327
328    /// Structural sites reject knob edits; unknown addresses error cleanly.
329    #[test]
330    fn edits_reject_structure_and_unknowns() {
331        let prior = PatchGrammarPrior::default();
332        let mut rng = StdRng::seed_from_u64(12);
333        let (tree, _) = draw(&prior, &mut rng);
334        assert!(matches!(
335            set_param(&tree, "node#leaf", ParamValue::Index(0)),
336            Err(EditError::Structural(_))
337        ));
338        assert!(matches!(
339            set_param(&tree, "nowhere#cut", ParamValue::Continuous(0.5)),
340            Err(EditError::UnknownAddress(_))
341        ));
342    }
343
344    /// tree_diff is empty on identity and localizes a single edit.
345    #[test]
346    fn diff_localizes_edits() {
347        let prior = PatchGrammarPrior::default();
348        let mut rng = StdRng::seed_from_u64(13);
349        let (tree, _) = draw(&prior, &mut rng);
350        assert!(tree_diff(&tree, &tree).is_empty());
351        let edited = set_param(&tree, "amp#attack", ParamValue::Continuous(0.9)).unwrap();
352        let d = tree_diff(&tree, &edited);
353        assert_eq!(d.len(), 1);
354        assert_eq!(d[0].addr, "amp#attack");
355        assert!(d[0].before.is_some() && d[0].after.is_some());
356    }
357
358    /// Every preset compiles, and structural edits (replace / insert /
359    /// delete / set-mod / swap) always yield compilable, describable,
360    /// trace-roundtrippable trees — hand rewiring cannot leave the grammar.
361    #[test]
362    fn presets_and_struct_ops_stay_in_grammar() {
363        use mutate::{ModKind, NodeKind, StructOp};
364        for (name, tree) in presets::presets() {
365            assert!(compile(&tree, SR).is_ok(), "preset {name} fails to compile");
366            assert!(!tree.signature().is_empty());
367        }
368        let prior = PatchGrammarPrior::default();
369        let mut rng = StdRng::seed_from_u64(21);
370        let kinds = [
371            NodeKind::Vco,
372            NodeKind::Supersaw,
373            NodeKind::Noise,
374            NodeKind::Wavetable,
375            NodeKind::Pluck,
376            NodeKind::Mix,
377            NodeKind::RingMod,
378            NodeKind::Filter,
379            NodeKind::Fold,
380            NodeKind::Delay,
381            NodeKind::Chorus,
382            NodeKind::Distortion,
383            NodeKind::Bitcrush,
384            NodeKind::Phaser,
385            // Wave 2B, and the four binaries are the point: every structural
386            // op has to survive a node with two audio subtrees *and* a
387            // modulation slot, which nothing but mix and ring mod ever had.
388            NodeKind::Shift,
389            NodeKind::Comp,
390            NodeKind::Duck,
391            NodeKind::Gate,
392            NodeKind::Vocoder,
393        ];
394        for i in 0..30 {
395            let (tree, _) = draw(&prior, &mut rng);
396            let keys: Vec<String> = describe::describe(&tree)
397                .modules
398                .iter()
399                .filter(|m| m.key != "amp" && !m.is_mod)
400                .map(|m| m.key.clone())
401                .collect();
402            let mut ops: Vec<StructOp> = Vec::new();
403            for key in &keys {
404                for kind in kinds {
405                    ops.push(StructOp::Replace {
406                        key: key.clone(),
407                        kind,
408                    });
409                    if !kind.is_source() {
410                        ops.push(StructOp::Insert {
411                            key: key.clone(),
412                            kind,
413                        });
414                    }
415                }
416                ops.push(StructOp::Delete { key: key.clone() });
417                for mk in [
418                    ModKind::None,
419                    ModKind::Lfo,
420                    ModKind::Env,
421                    ModKind::Rand,
422                    ModKind::Follow,
423                ] {
424                    ops.push(StructOp::SetMod {
425                        key: key.clone(),
426                        kind: mk,
427                    });
428                }
429                ops.push(StructOp::SwapMix { key: key.clone() });
430            }
431            for op in ops {
432                // Invalid ops are allowed to reject — but never panic.
433                if let Ok(next) = mutate::apply_struct_op(&tree, &op) {
434                    assert!(
435                        compile(&next, SR).is_ok(),
436                        "sample {i}: op {op:?} produced uncompilable tree"
437                    );
438                    assert!(next.root.size() <= mutate::MAX_SIZE);
439                    let back = PatchTree::from_trace(&next.to_trace()).unwrap();
440                    assert_eq!(back, next, "trace roundtrip after {op:?}");
441                    describe::describe(&next); // must not panic
442                }
443            }
444        }
445    }
446
447    /// Every structural path that treats a binary node specially, exercised on
448    /// each of the six deterministically rather than waiting for the prior to
449    /// draw one.
450    ///
451    /// Mix and ring mod were the only two-child productions for two waves, so
452    /// `child_mut`, `graft`, `primary_input`, `Delete`-a-branch, the mod slot
453    /// and the trace address scheme are the least-travelled code in the crate
454    /// — and wave 2B quadrupled the number of shapes going through them, with
455    /// the new ones carrying a modulation slot the old two never had.
456    #[test]
457    fn every_binary_node_survives_the_whole_edit_vocabulary() {
458        use mutate::{ModKind, NodeKind, StructOp};
459        let seed = presets::presets()[0].1.clone();
460        for kind in [
461            NodeKind::Mix,
462            NodeKind::RingMod,
463            NodeKind::Comp,
464            NodeKind::Duck,
465            NodeKind::Gate,
466            NodeKind::Vocoder,
467        ] {
468            let tree = mutate::apply_struct_op(
469                &seed,
470                &StructOp::Replace {
471                    key: "node".into(),
472                    kind,
473                },
474            )
475            .unwrap_or_else(|e| panic!("{kind:?}: replace at the root: {e}"));
476            // A binary node plus two branches: `size` has to count both, and
477            // an arm that forgets `/1` reports the tree a node short.
478            assert!(
479                tree.root.size() >= 3,
480                "{kind:?}: size {} — the second branch is not being counted",
481                tree.root.size()
482            );
483            // Both branches are real nodes at `/0` and `/1`, and the rack
484            // names them there — those keys are what the frontend hangs its
485            // per-module jack labels off.
486            let rack = describe::describe(&tree);
487            for k in ["node/0", "node/1"] {
488                assert!(
489                    rack.modules.iter().any(|m| m.key == k),
490                    "{kind:?}: no module at {k}"
491                );
492                assert!(
493                    rack.wires.iter().any(|w| w.from == k && w.to == "node"),
494                    "{kind:?}: no audio wire from {k}"
495                );
496            }
497            // Deleting either branch collapses to the sibling, both ways.
498            for (gone, kept) in [(0usize, 1usize), (1, 0)] {
499                let before = describe::describe(&tree);
500                let sibling = before
501                    .modules
502                    .iter()
503                    .find(|m| m.key == format!("node/{kept}"))
504                    .expect("sibling")
505                    .kind
506                    .clone();
507                let after = mutate::apply_struct_op(
508                    &tree,
509                    &StructOp::Delete {
510                        key: format!("node/{gone}"),
511                    },
512                )
513                .unwrap_or_else(|e| panic!("{kind:?}: delete /{gone}: {e}"));
514                assert_eq!(
515                    describe::describe(&after).modules[1].kind,
516                    sibling,
517                    "{kind:?}: deleting /{gone} did not leave /{kept} at the root"
518                );
519                assert!(compile(&after, SR).is_ok());
520            }
521            // The modulation slot: the two pure binaries have none, the four
522            // dynamics nodes do, and setting one must not disturb `/1`.
523            let has_slot = !matches!(kind, NodeKind::Mix | NodeKind::RingMod);
524            let set = mutate::apply_struct_op(
525                &tree,
526                &StructOp::SetMod {
527                    key: "node".into(),
528                    kind: ModKind::Lfo,
529                },
530            );
531            assert_eq!(
532                set.is_ok(),
533                has_slot,
534                "{kind:?}: modulation slot present = {}, expected {has_slot}",
535                set.is_ok()
536            );
537            if let Ok(set) = set {
538                let rack = describe::describe(&set);
539                assert!(rack.modules.iter().any(|m| m.key == "node/m" && m.is_mod));
540                assert!(rack.modules.iter().any(|m| m.key == "node/1"));
541                assert!(compile(&set, SR).is_ok());
542                assert_eq!(PatchTree::from_trace(&set.to_trace()).unwrap(), set);
543                // A node is never distance-zero from itself with a different
544                // slot — `node_distance` has to walk both branches *and* the
545                // slot, and a missed arm reads as "identical".
546                use fugue_evo::genome::traits::EvolutionaryGenome;
547                assert!(set.distance(&tree) > 0.0, "{kind:?}: distance is blind");
548            }
549            // Insert-into-the-wire keeps the fragment's own `/1`.
550            let inserted = mutate::apply_struct_op(
551                &tree,
552                &StructOp::InsertTree {
553                    key: "node/0".into(),
554                    node: mutate::apply_struct_op(
555                        &seed,
556                        &StructOp::Replace {
557                            key: "node".into(),
558                            kind,
559                        },
560                    )
561                    .unwrap()
562                    .root,
563                },
564            )
565            .unwrap_or_else(|e| panic!("{kind:?}: insert into a wire: {e}"));
566            assert!(compile(&inserted, SR).is_ok());
567            assert_eq!(
568                PatchTree::from_trace(&inserted.to_trace()).unwrap(),
569                inserted
570            );
571            // "Swap the two inputs" is offered on every binary in the rack
572            // menu, and for five of the six it used to be a guaranteed
573            // rejection — a verb the UI printed and the engine refused. It
574            // now applies to all six, and it has to actually exchange the
575            // branches, not merely return Ok.
576            let before = describe::describe(&tree);
577            let kind_at = |r: &describe::RackDescription, k: &str| {
578                r.modules
579                    .iter()
580                    .find(|m| m.key == k)
581                    .unwrap_or_else(|| panic!("{kind:?}: no module at {k}"))
582                    .kind
583                    .clone()
584            };
585            let swapped = mutate::apply_struct_op(&tree, &StructOp::SwapMix { key: "node".into() })
586                .unwrap_or_else(|e| panic!("{kind:?}: swap the two inputs: {e}"));
587            let after = describe::describe(&swapped);
588            assert_eq!(kind_at(&after, "node/0"), kind_at(&before, "node/1"));
589            assert_eq!(kind_at(&after, "node/1"), kind_at(&before, "node/0"));
590            assert!(compile(&swapped, SR).is_ok());
591            assert_eq!(PatchTree::from_trace(&swapped.to_trace()).unwrap(), swapped);
592        }
593    }
594
595    /// The ceilings have to hold on *both* routes into the bench.
596    ///
597    /// `apply_struct_op` has always checked them on its way out; the whole-tree
598    /// replace behind undo/redo and the editor's client-side rewrites did not,
599    /// and that is the route a graph editor leans on hardest. A tree that
600    /// `apply_struct_op` would refuse must be refused by `validate_tree` too,
601    /// or the ceiling is decorative.
602    #[test]
603    fn validate_tree_refuses_what_apply_struct_op_refuses() {
604        use mutate::{NodeKind, StructOp};
605        let mut tree = presets::presets()[0].1.clone();
606        assert!(
607            validate_tree(&tree).is_ok(),
608            "a preset is inside the ceilings"
609        );
610        // Stack filters at the root until the depth ceiling bites. The op that
611        // finally fails is the one whose *result* is out of bounds, so build
612        // that result by hand and check the validator agrees.
613        let mut over = None;
614        for _ in 0..(mutate::MAX_DEPTH + mutate::MAX_SIZE + 4) {
615            let op = StructOp::Insert {
616                key: "node".into(),
617                kind: NodeKind::Filter,
618            };
619            match mutate::apply_struct_op(&tree, &op) {
620                Ok(next) => tree = next,
621                Err(_) => {
622                    // Same edit, ceiling check skipped: exactly what
623                    // `edit_set_tree` used to hand the engine.
624                    let mut raw = tree.clone();
625                    raw.root = default_filter_over(raw.root);
626                    over = Some(raw);
627                    break;
628                }
629            }
630        }
631        let over = over.expect("the ceilings must bite within a bounded number of inserts");
632        assert!(
633            validate_tree(&over).is_err(),
634            "validate_tree let through a tree apply_struct_op refuses"
635        );
636    }
637
638    /// A filter wrapping `inner`, built without going through the op vocabulary
639    /// — the point of the test above is to construct a tree the vocabulary
640    /// would never return.
641    fn default_filter_over(inner: term::AudioNode) -> term::AudioNode {
642        term::AudioNode::Filter {
643            uid: Uid::NEW,
644            kind: term::FilterKind::SvfLp,
645            cutoff: 0.5,
646            resonance: 0.2,
647            mod_depth: 0.0,
648            input: Box::new(inner),
649            modulation: term::ModNode::None,
650        }
651    }
652
653    /// Deeper patches pay more prior mass — parsimony is the grammar itself.
654    #[test]
655    fn prior_penalizes_depth() {
656        let prior = PatchGrammarPrior::default();
657        let mut rng = StdRng::seed_from_u64(5);
658        let mut sized: Vec<(usize, f64)> = Vec::new();
659        for _ in 0..300 {
660            let (tree, trace) = draw(&prior, &mut rng);
661            sized.push((tree.root.size(), trace.log_prior));
662        }
663        let mean = |v: &[f64]| v.iter().sum::<f64>() / v.len() as f64;
664        let small: Vec<f64> = sized
665            .iter()
666            .filter(|(s, _)| *s <= 2)
667            .map(|(_, lp)| *lp)
668            .collect();
669        let large: Vec<f64> = sized
670            .iter()
671            .filter(|(s, _)| *s >= 5)
672            .map(|(_, lp)| *lp)
673            .collect();
674        assert!(!small.is_empty() && !large.is_empty());
675        assert!(
676            mean(&small) > mean(&large),
677            "small patches {} should out-mass large ones {}",
678            mean(&small),
679            mean(&large)
680        );
681    }
682
683    // ---------- node identity ----------
684
685    /// Uids must be invisible to every system that reasons about *content*.
686    ///
687    /// Three of those, and all three would break loudly: the engine's pool
688    /// dedup and refinement's own "did the walk move" test are both
689    /// `PatchTree` equality, and the render memo is a hash of the tree's JSON.
690    /// If a fresh identity could make two identical patches differ, evolution
691    /// would admit duplicates forever and every refinement step would miss a
692    /// cache it had just filled.
693    #[test]
694    fn uid_is_invisible_to_content() {
695        let mut a = presets::presets()[0].1.clone();
696        let mut b = a.clone();
697        a.ensure_uids();
698        b.ensure_uids();
699        assert_ne!(
700            a.root.uid().0,
701            b.root.uid().0,
702            "two settlings must mint different identities, or the test is vacuous"
703        );
704        assert_eq!(a, b, "patches that differ only in uid are the same patch");
705
706        // The render memo's content address is `canonical_tree_json`, which
707        // clears identities first; the half of that contract this crate can
708        // state is that clearing lands both trees on the same term. The JSON
709        // itself is pinned in `auracle_features::cache`, where the key lives.
710        let (mut ca, mut cb) = (a.clone(), b.clone());
711        ca.clear_uids();
712        cb.clear_uids();
713        assert!(ca.root.uid().is_new() && cb.root.uid().is_new());
714        assert_eq!(ca, cb);
715    }
716
717    /// A structural edit keeps the identity of every module that lived
718    /// through it, and mints one for the module it added.
719    ///
720    /// This is the difference between "insert a filter" and "throw the patch
721    /// away and build a new one that looks similar", and every lock, hand
722    /// position and selection in the panel rides on it.
723    #[test]
724    fn struct_ops_carry_identity_through() {
725        use mutate::{NodeKind, StructOp};
726        let mut tree = presets::presets()[0].1.clone();
727        tree.ensure_uids();
728        let before = describe::describe(&tree);
729        let uid_of = |d: &describe::RackDescription, key: &str| {
730            d.modules.iter().find(|m| m.key == key).map(|m| m.uid)
731        };
732        let root_uid = uid_of(&before, "node").expect("a root module");
733
734        // Insert above the root: everything shifts down one key, and nothing
735        // changes identity but the new plate.
736        let after = describe::describe(
737            &mutate::apply_struct_op(
738                &tree,
739                &StructOp::Insert {
740                    key: "node".into(),
741                    kind: NodeKind::Filter,
742                },
743            )
744            .expect("insert at the root is legal"),
745        );
746        assert_eq!(
747            uid_of(&after, "node/0"),
748            Some(root_uid),
749            "the module that was at `node` is now at `node/0` and is the same module"
750        );
751        assert!(
752            uid_of(&after, "node") != Some(root_uid) && uid_of(&after, "node") != Some(0),
753            "the inserted filter gets an identity of its own"
754        );
755
756        // And the identities in one tree are unique, including after a splice.
757        let mut seen = std::collections::HashSet::new();
758        for m in &after.modules {
759            if m.key == "amp" {
760                continue;
761            }
762            assert!(seen.insert(m.uid), "duplicate uid on {}", m.key);
763        }
764    }
765
766    /// **R6.** A refined child must inherit its seed's identities wherever the
767    /// structure survived.
768    ///
769    /// Refinement proposes over the *trace* and rebuilds the genome from it on
770    /// every accepted step, and a trace has no room for a uid — so the decoded
771    /// tree comes back anonymous. This is that exact round trip, without the
772    /// MCMC: encode, decode, and check that identity is gone and that
773    /// `inherit_uids` puts it back. Without it every ⚡ evolve would look to
774    /// the panel like a brand-new patch and every lock and hand position in it
775    /// would evaporate on the app's central action.
776    #[test]
777    fn identity_survives_the_trace_round_trip() {
778        let mut seed = presets::presets()[3].1.clone();
779        seed.ensure_uids();
780        let mut child = PatchTree::from_trace(&seed.to_trace()).expect("a trace decodes");
781        assert!(
782            child.root.uid().is_new(),
783            "the decoder cannot carry identities — that is why inheritance exists"
784        );
785        child.inherit_uids(&seed);
786        let (a, b) = (describe::describe(&seed), describe::describe(&child));
787        assert_eq!(a.modules.len(), b.modules.len());
788        for (x, y) in a.modules.iter().zip(&b.modules) {
789            assert_eq!(x.key, y.key);
790            assert_eq!(x.uid, y.uid, "identity lost at {}", x.key);
791        }
792    }
793
794    /// **R6, the other half.** Turning a knob must not rename the patch.
795    ///
796    /// `set_param` edits the *trace* and decodes it back, which is the same
797    /// anonymising round trip refinement takes — and it is on the hottest path
798    /// in the app. It went unnoticed because nothing in the engine reads a uid:
799    /// the loss only shows in the panel, where after one knob turn every lock
800    /// id collapses onto `0#site`, the motion system sees the whole rack
801    /// arrive at once, and every hand-placed position is orphaned. Measured in
802    /// the browser, not deduced from the code, which is why the assertion is
803    /// on `describe` — what the panel actually reads.
804    #[test]
805    fn identity_survives_a_knob_turn() {
806        let mut tree = presets::presets()[2].1.clone();
807        tree.ensure_uids();
808        let before = describe::describe(&tree);
809        // A continuous site somewhere below the root, so this is not just a
810        // statement about the amp.
811        let addr = before
812            .modules
813            .iter()
814            .filter(|m| m.key != "amp")
815            .find_map(|m| {
816                m.knobs
817                    .iter()
818                    .find(|k| k.kind == describe::KnobKind::Continuous)
819                    .map(|k| k.addr.clone())
820            })
821            .expect("a preset with a knob on it");
822        let edited = set_param(&tree, &addr, ParamValue::Continuous(0.375)).expect("a plain knob");
823        let after = describe::describe(&edited);
824        assert_eq!(before.modules.len(), after.modules.len());
825        for (x, y) in before.modules.iter().zip(&after.modules) {
826            assert_eq!(x.key, y.key);
827            assert_eq!(x.uid, y.uid, "a knob turn renamed {}", x.key);
828        }
829        // …and the edit itself still happened.
830        let value_at = |t: &PatchTree| {
831            t.to_trace()
832                .choices
833                .iter()
834                .find(|(k, _)| &***k == addr.as_str())
835                .map(|(_, c)| c.value.clone())
836        };
837        assert_ne!(value_at(&edited), value_at(&tree));
838    }
839
840    /// Settling reaches **every** module the rack draws, in every patch the
841    /// prior can produce.
842    ///
843    /// The walk has to know which productions carry children and which carry a
844    /// modulation slot, and a wildcard arm in either table is a module that
845    /// silently never gets an identity — which is how a `Shift`'s modulator
846    /// went uid-less on the first pass here. Prior draws are the right net:
847    /// they reach productions no preset uses.
848    #[test]
849    fn every_drawn_module_gets_an_identity() {
850        let prior = PatchGrammarPrior::default();
851        let mut rng = StdRng::seed_from_u64(0x1D_5E7);
852        for _ in 0..200 {
853            let (mut tree, _) = draw(&prior, &mut rng);
854            tree.ensure_uids();
855            let rack = describe::describe(&tree);
856            let mut seen = std::collections::HashSet::new();
857            for m in rack.modules.iter().filter(|m| m.key != "amp") {
858                assert_ne!(m.uid, 0, "{} ({}) has no identity", m.key, m.kind);
859                assert!(seen.insert(m.uid), "{} shares an identity", m.key);
860            }
861        }
862    }
863
864    /// A restored save carries identities the mint has never issued, and the
865    /// mint must not issue them again.
866    ///
867    /// The counter is per-process and a page reload starts it at 1, while the
868    /// save it restores is full of ids from the session that wrote it. Without
869    /// this, inserting one module into a restored patch would hand out an id
870    /// that patch already uses and two nodes would answer to one lock — the
871    /// exact confusion identities exist to end, arriving only for the returning
872    /// user, only after a reload.
873    #[test]
874    fn settling_pushes_the_mint_past_what_it_has_seen() {
875        // Stand in for a save written by an older session: a tree whose
876        // identities are far above anything this process has minted.
877        let mut restored = presets::presets()[0].1.clone();
878        restored.ensure_uids();
879        let high = term::Uid(9_000_000);
880        restored.root.set_uid(high);
881        restored.ensure_uids();
882        assert_eq!(
883            restored.root.uid().0,
884            high.0,
885            "a set identity is not reissued"
886        );
887
888        let mut fresh = presets::presets()[0].1.clone();
889        fresh.ensure_uids();
890        assert!(
891            fresh.root.uid().0 > high.0,
892            "the mint reissued an identity a restored patch is already using"
893        );
894    }
895
896    /// A duplicated subtree brings its original's identities with it in the
897    /// copy, and two nodes claiming one identity is worse than none: a lock on
898    /// either would light both. Settling breaks the tie.
899    #[test]
900    fn settling_breaks_duplicate_identities() {
901        let mut inner = presets::presets()[0].1.clone();
902        inner.ensure_uids();
903        let mut tree = inner.clone();
904        tree.root = term::AudioNode::Mix {
905            uid: Uid::NEW,
906            balance: 0.5,
907            a: Box::new(inner.root.clone()),
908            b: Box::new(inner.root.clone()),
909        };
910        tree.ensure_uids();
911        let d = describe::describe(&tree);
912        let mut seen = std::collections::HashSet::new();
913        for m in d.modules.iter().filter(|m| m.key != "amp") {
914            assert_ne!(m.uid, 0, "{} was left without an identity", m.key);
915            assert!(seen.insert(m.uid), "{} shares an identity", m.key);
916        }
917    }
918}