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}