rlx_core/preset/expr.rs
1//! A tiny pure expression language over the audio-analysis variables, compiled
2//! once at preset load and evaluated per parameter per frame.
3//!
4//! Grammar (recursive descent, standard precedence):
5//!
6//! ```text
7//! expr := sum (('>' | '<' | '>=' | '<=' | '==' | '!=') sum)*
8//! sum := term (('+' | '-') term)*
9//! term := unary (('*' | '/') unary)*
10//! unary := ('-' | '+')? primary
11//! primary:= number | ident | ident '(' expr (',' expr)* ')' | '(' expr ')'
12//! ```
13//!
14//! Comparisons sit at the lowest precedence and yield `1.0`/`0.0`, so they
15//! compose with arithmetic (`0.4 + (bass > 0.2) * 0.3`) and with `select`.
16//! There are no boolean operators: with clean `0/1` results, `min` is and,
17//! `max` is or, and `1 - c` is not.
18//!
19//! One thing an expression reads is **not** a function of this frame's analysis:
20//! a `[latch]` variable (ADR-0137). Its value is armed-and-fired state held in
21//! the render layer and written into the reserved slots of [`Variables`] once
22//! per preset per frame, before the params that read it. That leaves everything
23//! here intact — evaluation is still a pure, re-entrant function of the bundle
24//! it is handed, which is what lets one compiled expression run once per vertex
25//! or once per element — while making the *bundle* depend on the frames before
26//! it. A caller that runs no bank, and that is every probe and every
27//! single-frame capture, reads a latch at its rest value of `0`.
28//!
29//! Variables: `bass mid treb onset beat bar time tempo novelty index`, the
30//! per-vertex position `x y rad ang`, the
31//! absolute-level escapes `bass_raw mid_raw treb_raw onset_raw`, and the musical
32//! clock `beat_index time_since_beat beat_in_bar bar_index bar_phase`. The first
33//! four are normalized against their own recent peak (ADR-0049), so a threshold
34//! on them means "loud for this track" rather than naming a magnitude. `bar` is
35//! **beat** phase under a historical name; `bar_phase` is the real thing
36//! (ADR-0050).
37//! Constants: `pi tau`.
38//! Functions: `sin cos abs floor sqrt log min max pow mod clamp lerp smoothstep
39//! select bin hash noise`. Compilation is fallible (a malformed expression is
40//! rejected with a surfaced error, never a panic); evaluation of a compiled
41//! expression is total, panic-free, and allocation-free — it walks a prebuilt
42//! AST returning `f32`, so it is safe to call every frame (hot-path §5).
43//!
44//! `bin(x)` is the one function that reads something other than its arguments:
45//! it samples the analysis frame's log-spaced spectrum, which [`Variables`]
46//! carries **by borrow** (ADR-0036). The language stays scalar-only — there is
47//! no array type and no indexing syntax; the band array is reachable only
48//! through this call, at a normalized position, interpolated.
49//!
50//! `hash(x)` and `noise(x)` are the grammar's only randomness (ADR-0051), and
51//! they are random the way a shader is: pure functions of `(argument, salt)`,
52//! where the salt is a per-preset constant [`Variables`] carries. Nothing here
53//! reads a clock or draws from an RNG — two evaluations of the same argument
54//! under the same salt are bit-identical, which is exactly what NFR §6 asks of
55//! visual randomness. Who supplies the salt is the preset's business (see
56//! [`schema::Preset`](super::schema::Preset)); this module only mixes it in.
57
58// Hot-path panic-denial pragma: `eval` runs per parameter per frame. This file
59// is a named target in the hygiene guard's scan (tests/hygiene.rs), so the
60// pragma is enforced here even though the rest of preset/ is load-time only.
61#![deny(
62 clippy::unwrap_used,
63 clippy::expect_used,
64 clippy::indexing_slicing,
65 clippy::panic,
66 clippy::unreachable
67)]
68
69use std::fmt;
70
71/// The analysis variables an expression may reference, in slot order.
72///
73/// The first nine are the analysis frame's headline values, `bass` through
74/// `novelty`. The four `*_raw` names after them are the absolute magnitudes the
75/// first four carried before ADR-0049 normalized them — reachable for looks that
76/// genuinely want absolute level rather than "loud for this track". Then
77/// `beat_index` and `time_since_beat`, ADR-0050's unconditional Layer 1 musical
78/// clock, and `beat_in_bar`/`bar_index`/`bar_phase`, its Layer 2 bar position —
79/// gated on a confidence the grammar deliberately cannot see, so these three are
80/// always *something* sensible and never wrong about the music. Then
81/// `x`/`y`/`rad`/`ang`, the **vertex's own position** during a per-vertex
82/// evaluation (Plan 0100 Phase 1) — the same kind of thing as `index` one axis
83/// up, and `0` anywhere else. Then the reserved `[latch]` block (ADR-0137),
84/// [`LATCH_CAP`] slots an author never writes by these names: a preset's own
85/// latch names resolve **onto** them at load, and the placeholders are held out
86/// of the identifier lookup so `_latch0` is not a variable anybody can bind.
87/// `index` stays **last** and is different in kind: it is not audio but the
88/// *element's own position* during a per-element evaluation (Plan 0034 Phase 4),
89/// and it reads `0` anywhere else.
90pub const VAR_NAMES: [&str; 27] = [
91 "bass",
92 "mid",
93 "treb",
94 "onset",
95 "beat",
96 "bar",
97 "time",
98 "tempo",
99 "novelty",
100 "bass_raw",
101 "mid_raw",
102 "treb_raw",
103 "onset_raw",
104 "beat_index",
105 "time_since_beat",
106 "beat_in_bar",
107 "bar_index",
108 "bar_phase",
109 "x",
110 "y",
111 "rad",
112 "ang",
113 "_latch0",
114 "_latch1",
115 "_latch2",
116 "_latch3",
117 "index",
118];
119/// Number of expression variables.
120pub const VAR_COUNT: usize = VAR_NAMES.len();
121
122/// Whether the variable in `slot` is one an author can reach **by its name in
123/// [`VAR_NAMES`]**.
124///
125/// The reserved `[latch]` placeholders are storage, not grammar: they sit in
126/// `VAR_NAMES` so the positional assertions can see them, and an author reaches
127/// a latch only through the name they declared for it (ADR-0137). The parser's
128/// identifier lookup and the exported roster read this one predicate, so a
129/// consumer is never offered a name the parser would refuse.
130fn is_bindable_slot(slot: usize) -> bool {
131 !(LATCH_SLOT_BASE..LATCH_SLOT_BASE + LATCH_CAP).contains(&slot)
132}
133
134/// Every variable name an expression may **write**, in [`VAR_NAMES`] order.
135///
136/// [`VAR_NAMES`] itself is the storage layout and includes the four reserved
137/// latch placeholders; this is the grammar. A consumer building an editor wants
138/// this one.
139pub fn variable_names() -> impl Iterator<Item = &'static str> {
140 VAR_NAMES
141 .iter()
142 .enumerate()
143 .filter(|(slot, _)| is_bindable_slot(*slot))
144 .map(|(_, name)| *name)
145}
146
147/// Every bare identifier that resolves to a literal, in declaration order.
148///
149/// Resolved before the variable lookup, so a consumer highlighting these is
150/// naming something no future variable can shadow.
151pub fn constant_names() -> impl Iterator<Item = &'static str> {
152 CONSTANTS.iter().map(|(name, _)| *name)
153}
154
155/// Every built-in function name, in declaration order.
156pub fn function_names() -> impl Iterator<Item = &'static str> {
157 FUNCS.iter().map(|(name, _)| *name)
158}
159
160/// Slot of the implicit per-element `index` variable — kept last, so it stays
161/// derivable from the count however many analysis variables precede it.
162const INDEX_SLOT: usize = VAR_COUNT - 1;
163
164/// Slot of `bass_raw`, the first of the four raw levels, which occupy
165/// `RAW_SLOT_BASE..RAW_SLOT_BASE + 4` in [`VAR_NAMES`] order.
166///
167/// A named base rather than four literals threaded through
168/// [`with_raw`](Variables::with_raw), and `raw_slots_are_where_the_names_say`
169/// asserts the four names really do live here — so reordering `VAR_NAMES` fails
170/// a test instead of silently binding `treb_raw` to `onset_raw`. That is the
171/// same "two sources that agree today and nothing ties them" failure Plan 0041's
172/// review found in the old duplicated construction sites.
173const RAW_SLOT_BASE: usize = 9;
174
175/// Slot of `beat_index`, followed by `time_since_beat` — ADR-0050's Layer 1 pair,
176/// written by [`with_beat_clock`](Variables::with_beat_clock) and checked by the
177/// same name assertion the raw block gets.
178const CLOCK_SLOT_BASE: usize = 13;
179
180/// Slot of `beat_in_bar`, followed by `bar_index` and `bar_phase` — ADR-0050's
181/// gated Layer 2 trio, written by [`with_bar`](Variables::with_bar).
182///
183/// The *confidence* behind these is deliberately absent from `VAR_NAMES`: an
184/// author gets bar-aware behavior with a counter fallback underneath it, not a
185/// gate to hand-tune. It rides on the analysis frame for diagnostics instead.
186const BAR_SLOT_BASE: usize = 15;
187
188/// Slot of `x`, followed by `y`, `rad` and `ang` — the per-vertex position
189/// written by [`with_vertex`](Variables::with_vertex) (Plan 0100 Phase 1).
190///
191/// These are `0` in every evaluation that is not per-vertex, exactly as `index`
192/// is `0` outside a per-element one. That is the whole of "the grammar is not
193/// widened for other systems": the *names* exist crate-wide because slots are
194/// positional, and the only caller that ever binds them is the warp mesh's
195/// `[per_vertex]` table.
196const VERTEX_SLOT_BASE: usize = 18;
197
198/// How many `[latch]` entries one preset may declare (ADR-0137).
199///
200/// **A chosen constant, and it is chosen rather than measured.** There is no
201/// experiment behind four: it is the number of independent armed-and-fired
202/// events a preset can hold in one reader's head at once, and every slot costs
203/// *every* preset — declared or not — a float in [`Variables`] and a bool plus a
204/// float in the render layer's bank, because the block is fixed and positional.
205/// A preset asking for more gets a load error naming this number, not a slower
206/// path; raising it is a recompile and nothing else.
207pub const LATCH_CAP: usize = 4;
208
209/// Slot of the first reserved latch variable; the block runs
210/// `LATCH_SLOT_BASE..LATCH_SLOT_BASE + LATCH_CAP` and sits immediately before
211/// `index`.
212///
213/// **Before `index` on purpose.** `INDEX_SLOT` is derived as `VAR_COUNT - 1`, so
214/// a block appended after it would silently re-point the one slot in this file
215/// whose position is computed rather than written down. Every other base
216/// (`RAW_`, `CLOCK_`, `BAR_`, `VERTEX_`) is a literal that sits *before* this
217/// one and is therefore unmoved by it — and `latch_slots_are_where_the_names_say`
218/// holds all of them to their names, so a reordered `VAR_NAMES` fails a test
219/// rather than binding `bar_phase` to `_latch1`.
220const LATCH_SLOT_BASE: usize = 22;
221
222// The five slot blocks must not overlap. Every bound here is a compile-time
223// constant, so this is checked at compile time: an overlapping base is a build
224// failure, not a test failure. `raw_slots_are_where_the_names_say` covers the
225// half a constant cannot — that the *names* at these offsets are the expected
226// ones.
227const _: () = assert!(
228 RAW_SLOT_BASE + 4 <= CLOCK_SLOT_BASE,
229 "the raw block must end before the clock block begins"
230);
231const _: () = assert!(
232 CLOCK_SLOT_BASE + 2 <= BAR_SLOT_BASE,
233 "the clock block must end before the bar block begins"
234);
235const _: () = assert!(
236 BAR_SLOT_BASE + 3 <= VERTEX_SLOT_BASE,
237 "the bar block must end before the vertex block begins"
238);
239const _: () = assert!(
240 VERTEX_SLOT_BASE + 4 <= LATCH_SLOT_BASE,
241 "the vertex block must end before the latch block begins"
242);
243const _: () = assert!(
244 LATCH_SLOT_BASE + LATCH_CAP <= INDEX_SLOT,
245 "the latch block must end before `index`"
246);
247
248/// A bound set of variable values for one evaluation. Field order matches
249/// [`VAR_NAMES`]; `beat` is the caller's bool coerced to 0.0/1.0.
250///
251/// The spectrum is held **by borrow**, not by value (ADR-0036): the analysis
252/// frame's 64 bands are 256 bytes, and this bundle is built once per frame but
253/// read once per *binding*. A by-value payload would put that memcpy on the
254/// per-binding path; a slice reference keeps the whole struct at nine floats
255/// plus a fat pointer, so it stays cheaply `Copy`.
256#[derive(Debug, Clone, Copy, Default)]
257pub struct Variables<'a> {
258 values: [f32; VAR_COUNT],
259 /// The log-spaced band array `bin(x)` samples, borrowed from the analysis
260 /// frame. Empty when no caller supplied one, which makes `bin` read `0`.
261 spectrum: &'a [f32],
262 /// The per-preset salt `hash()`/`noise()` mix into their argument
263 /// (ADR-0051). A **load-time constant**, never per-frame entropy: the caller
264 /// sets it once from the preset's `[generator] seed`, so two presets writing
265 /// the same expression scatter differently while one preset reproduces frame
266 /// to frame and run to run. `0` — the default — is a perfectly good salt,
267 /// and the one every preset that declares no seed gets.
268 salt: u32,
269}
270
271impl<'a> Variables<'a> {
272 /// Bind all nine variables (order matches [`VAR_NAMES`]). `tempo` is the
273 /// tracked BPM (`0` until the tracker warms, then ~60-200 — not a `0..1`
274 /// band); `novelty` is the experimental spectral track-change transient.
275 ///
276 /// The spectrum starts empty; attach one with
277 /// [`with_spectrum`](Self::with_spectrum).
278 #[allow(clippy::too_many_arguments)]
279 pub fn new(
280 bass: f32,
281 mid: f32,
282 treb: f32,
283 onset: f32,
284 beat: f32,
285 bar: f32,
286 time: f32,
287 tempo: f32,
288 novelty: f32,
289 ) -> Self {
290 let mut values = [0.0f32; VAR_COUNT];
291 // The nine headline slots. Everything after them — the four raw levels
292 // and `index` — starts at 0, so an expression naming one outside the
293 // caller that supplies it reads zero rather than something undefined.
294 values[..9].copy_from_slice(&[bass, mid, treb, onset, beat, bar, time, tempo, novelty]);
295 Self {
296 values,
297 spectrum: &[],
298 // Unsalted until a caller says otherwise — see `with_salt`.
299 salt: 0,
300 }
301 }
302
303 /// Bind the four absolute levels `bass_raw`/`mid_raw`/`treb_raw`/`onset_raw`
304 /// (ADR-0049), leaving everything else as it was.
305 ///
306 /// A builder rather than four more positional arguments on
307 /// [`new`](Self::new): that constructor is already at the argument-count lint
308 /// and growing it to thirteen is how a caller silently transposes two levels.
309 pub fn with_raw(self, bass_raw: f32, mid_raw: f32, treb_raw: f32, onset_raw: f32) -> Self {
310 let mut next = self;
311 if let Some(slots) = next.values.get_mut(RAW_SLOT_BASE..RAW_SLOT_BASE + 4) {
312 slots.copy_from_slice(&[bass_raw, mid_raw, treb_raw, onset_raw]);
313 }
314 next
315 }
316
317 /// Bind `beat_index` and `time_since_beat` (ADR-0050 Layer 1), leaving
318 /// everything else as it was.
319 ///
320 /// `beat_index` arrives as the frame's `u32` and converts here: exact up to
321 /// 2^24 beats, which at 200 BPM is about 1400 hours of continuous playback.
322 pub fn with_beat_clock(self, beat_index: u32, time_since_beat: f32) -> Self {
323 let mut next = self;
324 if let Some(slots) = next.values.get_mut(CLOCK_SLOT_BASE..CLOCK_SLOT_BASE + 2) {
325 slots.copy_from_slice(&[beat_index as f32, time_since_beat]);
326 }
327 next
328 }
329
330 /// Bind `beat_in_bar`, `bar_index` and `bar_phase` (ADR-0050 Layer 2),
331 /// leaving everything else as it was.
332 ///
333 /// These arrive already resolved: the caller has decided whether they came
334 /// from the downbeat estimate or from the counter fallback, so nothing here
335 /// or downstream needs to know which. That is the point of the gate living in
336 /// the analyzer.
337 pub fn with_bar(self, beat_in_bar: u32, bar_index: u32, bar_phase: f32) -> Self {
338 let mut next = self;
339 if let Some(slots) = next.values.get_mut(BAR_SLOT_BASE..BAR_SLOT_BASE + 3) {
340 slots.copy_from_slice(&[beat_in_bar as f32, bar_index as f32, bar_phase]);
341 }
342 next
343 }
344
345 /// Bind every analysis variable from `frame`, with the clock at `time`.
346 ///
347 /// **This is the only place the frame-to-slot mapping is written.** Both the
348 /// render loop and `shot`'s reachability probe come through here, so a tenth
349 /// variable or a reordered slot is a one-file change rather than two copies
350 /// that happen to agree. They did agree — and nothing could have told you
351 /// which one the code actually used, which is the failure this closes: a
352 /// probe binding different values than the engine would report flags about
353 /// an expression the renderer never evaluates (Plan 0041 review).
354 ///
355 /// `time` stays an argument because it is the one variable that is not on
356 /// the frame — the renderer passes its own clock, the probe the hop position
357 /// it synthesized.
358 ///
359 /// The band array rides **by borrow** (ADR-0036), so this costs exactly what
360 /// [`new`](Self::new) plus [`with_spectrum`](Self::with_spectrum) cost: no
361 /// copy of the spectrum, nothing allocated, safe on the per-frame path.
362 pub fn from_frame(frame: &'a crate::dsp::AnalysisFrame, time: f32) -> Self {
363 Self::new(
364 frame.bass,
365 frame.mid,
366 frame.treb,
367 frame.onset,
368 f32::from(frame.beat),
369 frame.bar,
370 time,
371 frame.bpm,
372 frame.novelty,
373 )
374 .with_raw(
375 frame.bass_raw,
376 frame.mid_raw,
377 frame.treb_raw,
378 frame.onset_raw,
379 )
380 .with_beat_clock(frame.beat_index, frame.time_since_beat)
381 .with_bar(frame.beat_in_bar, frame.bar_index, frame.bar_phase)
382 .with_spectrum(&frame.spectrum)
383 }
384
385 /// Bind the reserved `[latch]` block from `values` (ADR-0137), leaving
386 /// everything else as it was.
387 ///
388 /// The render layer's latch bank calls this once per preset per frame,
389 /// before the params that read a latch. Entries past [`LATCH_CAP`] are
390 /// ignored, and a slot no latch declares keeps its `0.0` rest value — which
391 /// is what any caller that does not run a bank (a probe, a test, a
392 /// single-frame capture) sees for every latch.
393 pub fn with_latches(self, values: &[f32]) -> Self {
394 let mut next = self;
395 let n = values.len().min(LATCH_CAP);
396 if let (Some(slots), Some(src)) = (
397 next.values.get_mut(LATCH_SLOT_BASE..LATCH_SLOT_BASE + n),
398 values.get(..n),
399 ) {
400 slots.copy_from_slice(src);
401 }
402 next
403 }
404
405 /// Rebind the per-element `index` to `t` (the element's normalized `0..1`
406 /// position), returning a fresh binding — the caller evaluates once per
407 /// element against these (Plan 0034 Phase 4).
408 ///
409 /// By value and `Copy`, so a per-element loop rebinds one float without
410 /// touching the borrowed spectrum or allocating.
411 pub fn with_index(self, t: f32) -> Self {
412 let mut next = self;
413 if let Some(slot) = next.values.get_mut(INDEX_SLOT) {
414 *slot = t;
415 }
416 next
417 }
418
419 /// Rebind the per-vertex position `x`, `y`, `rad`, `ang`, returning a fresh
420 /// binding — the caller evaluates a `[per_vertex]` binding once per mesh
421 /// vertex against these (Plan 0100 Phase 1).
422 ///
423 /// `x`/`y` are the vertex's uv in `0..1`; `rad` is its distance from the
424 /// mesh centre and `ang` its angle there, both taken in the
425 /// **aspect-corrected** space of the render target (ADR-0037) so a
426 /// `rad`-driven figure is round on any display and does not follow the mesh
427 /// grid's own proportions. The caller does that correction — this only
428 /// carries the four values.
429 ///
430 /// By value and `Copy`, like [`with_index`](Self::with_index): a per-vertex
431 /// loop rebinds four floats without touching the borrowed spectrum or
432 /// allocating.
433 pub fn with_vertex(self, x: f32, y: f32, rad: f32, ang: f32) -> Self {
434 let mut next = self;
435 if let Some(slots) = next.values.get_mut(VERTEX_SLOT_BASE..VERTEX_SLOT_BASE + 4) {
436 slots.copy_from_slice(&[x, y, rad, ang]);
437 }
438 next
439 }
440
441 /// Attach the frame's log-spaced band array, which `bin(x)` samples. A
442 /// borrow rather than a copy — see the type docs. Kept a separate builder so
443 /// the nine-scalar constructor stays the shape every existing caller (and
444 /// every test) already uses.
445 pub fn with_spectrum(self, spectrum: &'a [f32]) -> Self {
446 Self { spectrum, ..self }
447 }
448
449 /// Bind the per-preset salt `hash()`/`noise()` mix in (ADR-0051).
450 ///
451 /// Its own builder rather than a constructor argument because the salt is a
452 /// fact about the **preset**, not about the analysis frame: the render loop
453 /// builds one [`Variables`] per frame from the frame alone, then re-salts it
454 /// per preset. That is what keeps both sides of a dissolve on their own seed
455 /// while they read the same audio.
456 ///
457 /// By value and `Copy`, like [`with_index`](Self::with_index) — re-salting
458 /// rebinds one `u32` without touching the borrowed spectrum or allocating.
459 pub fn with_salt(self, salt: u32) -> Self {
460 Self { salt, ..self }
461 }
462
463 /// Value in `slot` (0.0 for an out-of-range slot — never panics; compiled
464 /// expressions only ever produce valid slots).
465 fn get(&self, slot: usize) -> f32 {
466 self.values.get(slot).copied().unwrap_or(0.0)
467 }
468
469 /// The spectrum at normalized position `x`, linearly interpolated between
470 /// the two adjacent bands — so a preset addresses a frequency *region*
471 /// without ever naming the engine's band count (`SPECTRUM_BINS`).
472 ///
473 /// **Total by construction**, because this runs per binding per frame:
474 /// `x <= 0` reads the first band, `x >= 1` the last, `NaN` clamps to the
475 /// first, and an absent spectrum reads `0`. No indexing, no panic path.
476 fn bin(&self, x: f32) -> f32 {
477 let last = match self.spectrum.len().checked_sub(1) {
478 Some(last) => last,
479 // No spectrum bound at all — a `bin()` in an expression evaluated
480 // outside the render loop reads a flat zero rather than erroring.
481 None => return 0.0,
482 };
483 // Same total `max().min()` as `clamp`/`smoothstep` below and for the
484 // same reason: `f32::max` returns the non-NaN operand, so a NaN input
485 // folds to 0.0 instead of propagating into a scene parameter.
486 #[allow(clippy::manual_clamp)]
487 let pos = x.max(0.0).min(1.0) * last as f32;
488 let floor = pos.floor();
489 let index = floor as usize;
490 let a = self.spectrum.get(index).copied().unwrap_or(0.0);
491 // At the top end there is no next band; `unwrap_or(a)` makes the
492 // interpolation degenerate to `a` rather than reaching past the array.
493 let b = self.spectrum.get(index + 1).copied().unwrap_or(a);
494 a + (b - a) * (pos - floor)
495 }
496}
497
498/// Built-in functions, tagged with their arity so the parser can check it.
499#[derive(Debug, Clone, Copy, PartialEq, Eq)]
500enum Func {
501 Sin,
502 Cos,
503 Abs,
504 Floor,
505 Sqrt,
506 /// `log(x)` — **natural** logarithm (Plan 0038 Phase 4). There is no
507 /// `log10`; divide by `ln(10)` = `2.302585` for a decade-based one, which is
508 /// what the dB idiom in `docs/presets.md` does.
509 Log,
510 Min,
511 Max,
512 Pow,
513 Mod,
514 Clamp,
515 Lerp,
516 Smoothstep,
517 Select,
518 /// `bin(x)` — the log-spaced spectrum at normalized position `x`
519 /// (ADR-0036). The only function whose result depends on [`Variables`]
520 /// rather than on its arguments alone.
521 Bin,
522 /// `hash(x)` — a deterministic uniform scatter of `x` into `[0, 1)`, salted
523 /// per preset (ADR-0051). Discontinuous by design: adjacent arguments give
524 /// unrelated results, which is what makes `hash(floor(time * 2))` a lottery
525 /// rather than a ramp.
526 Hash,
527 /// `noise(x)` — smooth value noise of `x` in `[0, 1]`, salted per preset
528 /// (ADR-0051). The continuous counterpart to [`Hash`](Func::Hash): one call
529 /// replaces a sum of incommensurate sines.
530 Noise,
531}
532
533/// **The single roster of built-in functions**, spelling first.
534///
535/// [`Func::from_name`] resolves through it, [`Func::name`] inverts it, and
536/// [`function_names`] publishes it — so a function added here is spellable,
537/// printable and declared to a studio in one edit, and there is no second list
538/// for any of the three to fall out of step with (ADR-0170's discipline, applied
539/// to the grammar rather than to the parameters).
540const FUNCS: [(&str, Func); 17] = [
541 ("sin", Func::Sin),
542 ("cos", Func::Cos),
543 ("abs", Func::Abs),
544 ("floor", Func::Floor),
545 ("sqrt", Func::Sqrt),
546 ("log", Func::Log),
547 ("min", Func::Min),
548 ("max", Func::Max),
549 ("pow", Func::Pow),
550 ("mod", Func::Mod),
551 ("clamp", Func::Clamp),
552 ("lerp", Func::Lerp),
553 ("smoothstep", Func::Smoothstep),
554 ("select", Func::Select),
555 ("bin", Func::Bin),
556 ("hash", Func::Hash),
557 ("noise", Func::Noise),
558];
559
560impl Func {
561 fn from_name(name: &str) -> Option<Self> {
562 FUNCS
563 .iter()
564 .find(|(spelling, _)| *spelling == name)
565 .map(|(_, func)| *func)
566 }
567
568 /// The source name — the inverse of [`Func::from_name`], for
569 /// [`Node::write_source`].
570 ///
571 /// `"?"` is unreachable while [`FUNCS`] holds every variant, which
572 /// `every_function_variant_is_in_the_roster` asserts; this file denies
573 /// panics, so it degrades rather than proving the point with a crash.
574 fn name(self) -> &'static str {
575 FUNCS
576 .iter()
577 .find(|(_, func)| *func == self)
578 .map_or("?", |(spelling, _)| *spelling)
579 }
580
581 fn arity(self) -> usize {
582 match self {
583 Func::Sin
584 | Func::Cos
585 | Func::Abs
586 | Func::Floor
587 | Func::Sqrt
588 | Func::Log
589 | Func::Bin
590 | Func::Hash
591 | Func::Noise => 1,
592 Func::Min | Func::Max | Func::Pow | Func::Mod => 2,
593 Func::Clamp | Func::Lerp | Func::Smoothstep | Func::Select => 3,
594 }
595 }
596}
597
598/// Integer avalanche — the mixer both seeded functions are built on. Every input
599/// bit affects every output bit, so two arguments one ULP apart scatter to
600/// unrelated results, which is the whole point of `hash`. Wrapping arithmetic
601/// throughout: there is no overflow to panic on, debug build included.
602const fn mix32(mut v: u32) -> u32 {
603 v ^= v >> 16;
604 v = v.wrapping_mul(0x7feb_352d);
605 v ^= v >> 15;
606 v = v.wrapping_mul(0x846c_a68b);
607 v ^= v >> 16;
608 v
609}
610
611/// A mixed `u32` as a uniform `f32` in `[0, 1)`.
612///
613/// The top **24** bits, not all 32, because `f32` carries a 24-bit mantissa: the
614/// division is then exact and every representable result is equally likely.
615/// Scaling the full 32 bits would round, and round *up* at the top end — which is
616/// how a generator documented as `[0, 1)` starts handing back exactly `1.0`.
617fn unit(v: u32) -> f32 {
618 (v >> 8) as f32 / 16_777_216.0
619}
620
621/// The scatter both seeded functions share: fold the salt in, then avalanche.
622/// The salt is mixed **before** the xor so that a small seed (`1`, `2`, `7` —
623/// what an author actually types) still changes every output bit.
624fn scatter(bits: u32, salt: u32) -> f32 {
625 unit(mix32(bits ^ mix32(salt)))
626}
627
628/// `hash(x)` — a deterministic uniform scatter of `x` into `[0, 1)` (ADR-0051).
629///
630/// Total for every input, infinities and `NaN` included: a float's bit pattern is
631/// always a valid `u32`, so there is no domain to guard and no branch to take.
632fn hash01(x: f32, salt: u32) -> f32 {
633 // `0.0` and `-0.0` are the same number carrying different bits, and an author
634 // writing `hash(a - b)` should not be able to see the sign of a zero.
635 let bits = if x == 0.0 { 0 } else { x.to_bits() };
636 scatter(bits, salt)
637}
638
639/// `noise(x)` — smooth value noise of `x` in `[0, 1]` (ADR-0051): a hashed value
640/// at each integer, eased across the cell `x` falls in.
641///
642/// One octave, deliberately (ADR-0051): an author wanting fBm sums calls at
643/// different rates, which costs them one line and costs the engine nothing.
644fn value_noise(x: f32, salt: u32) -> f32 {
645 // Total by construction, the same posture as `bin` and `clamp`: a non-finite
646 // argument names no cell, so it reads the midpoint instead of propagating a
647 // NaN into a scene parameter. Every input keeps the documented `[0, 1]`.
648 if !x.is_finite() {
649 return 0.5;
650 }
651 let cell = x.floor();
652 let frac = x - cell;
653 // `as` saturates rather than wrapping, so an argument past `i32` range pins
654 // to one lattice point — a flat stretch, not a wrap and not a panic.
655 let i = cell as i32;
656 let a = scatter(i as u32, salt);
657 let b = scatter(i.wrapping_add(1) as u32, salt);
658 // The same eased ramp `smoothstep` uses — zero derivative at both ends, so
659 // one cell joins the next without a crease.
660 let t = frac * frac * (3.0 - 2.0 * frac);
661 a + (b - a) * t
662}
663
664/// Bare identifiers that resolve to a literal. Resolved before the variable
665/// lookup so they cannot be shadowed; an unknown bare name still errors.
666/// Whether `name` is already resolved by the grammar — a built-in variable
667/// (the reserved `[latch]` placeholders included), a named constant, or a
668/// function.
669///
670/// The loader's guard against a `[latch]` name nothing could reach. Latch names
671/// resolve **last** in `Parser::parse_primary`, so a latch called `bass` would
672/// silently be the band and one called `sin` would fail as a call — either way
673/// the author debugs a preset that is doing exactly what it was told. Rejecting
674/// the collision at load is what makes that resolution order unobservable.
675pub fn is_reserved_ident(name: &str) -> bool {
676 VAR_NAMES.contains(&name) || constant(name).is_some() || Func::from_name(name).is_some()
677}
678
679/// Whether `name` lexes as a single identifier — `[A-Za-z_][A-Za-z0-9_]*`, the
680/// rule `tokenize` applies.
681///
682/// A `[latch]` name failing this could never be written inside an expression, so
683/// the loader rejects it rather than admitting a latch no binding can read.
684pub fn is_identifier(name: &str) -> bool {
685 let mut chars = name.chars();
686 matches!(chars.next(), Some(c) if c.is_ascii_alphabetic() || c == '_')
687 && chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
688}
689
690/// **The single roster of named constants.** [`constant`] resolves through it
691/// and [`constant_names`] publishes it, so a constant added here is spellable
692/// and declared in one edit.
693const CONSTANTS: [(&str, f32); 2] = [("pi", std::f32::consts::PI), ("tau", std::f32::consts::TAU)];
694
695fn constant(name: &str) -> Option<f32> {
696 CONSTANTS
697 .iter()
698 .find(|(spelling, _)| *spelling == name)
699 .map(|(_, value)| *value)
700}
701
702#[derive(Debug, Clone, Copy)]
703enum BinOp {
704 Add,
705 Sub,
706 Mul,
707 Div,
708 Gt,
709 Lt,
710 Ge,
711 Le,
712 Eq,
713 Ne,
714}
715
716impl BinOp {
717 /// The source token, for [`Node::write_source`].
718 fn symbol(self) -> &'static str {
719 match self {
720 BinOp::Add => "+",
721 BinOp::Sub => "-",
722 BinOp::Mul => "*",
723 BinOp::Div => "/",
724 BinOp::Gt => ">",
725 BinOp::Lt => "<",
726 BinOp::Ge => ">=",
727 BinOp::Le => "<=",
728 BinOp::Eq => "==",
729 BinOp::Ne => "!=",
730 }
731 }
732
733 /// Which grammar tier this operator belongs to.
734 fn precedence(self) -> u8 {
735 match self {
736 BinOp::Gt | BinOp::Lt | BinOp::Ge | BinOp::Le | BinOp::Eq | BinOp::Ne => PREC_CMP,
737 BinOp::Add | BinOp::Sub => PREC_SUM,
738 BinOp::Mul | BinOp::Div => PREC_TERM,
739 }
740 }
741
742 /// Whether this operator yields a gate (a clean `0.0`/`1.0`) rather than a
743 /// magnitude. Stated as its own predicate rather than as
744 /// `precedence() == PREC_CMP`, because [`Node::probe`] observes exactly this
745 /// set (ADR-0043) and a future tier reshuffle must not silently redefine it.
746 fn is_comparison(self) -> bool {
747 match self {
748 BinOp::Gt | BinOp::Lt | BinOp::Ge | BinOp::Le | BinOp::Eq | BinOp::Ne => true,
749 BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Div => false,
750 }
751 }
752}
753
754/// Compiled AST node. `Box`/`Box<[_]>` allocate once at compile; evaluation
755/// only reads them.
756#[derive(Debug)]
757enum Node {
758 Const(f32),
759 Var(usize),
760 Neg(Box<Node>),
761 Bin(BinOp, Box<Node>, Box<Node>),
762 Call(Func, Box<[Node]>),
763}
764
765impl Node {
766 /// Nodes in this subtree, counting itself. Used only by the probe walk, to
767 /// keep a node's index the same no matter which branch a `select()` took on
768 /// this evaluation — an untaken subtree still occupies its indices.
769 fn node_count(&self) -> usize {
770 1 + match self {
771 Node::Const(_) | Node::Var(_) => 0,
772 Node::Neg(inner) => inner.node_count(),
773 Node::Bin(_, l, r) => l.node_count() + r.node_count(),
774 Node::Call(_, args) => args.iter().map(Node::node_count).sum(),
775 }
776 }
777
778 /// Whether this subtree reads variable `slot`. Walked **once at compile**,
779 /// never per frame.
780 fn references(&self, slot: usize) -> bool {
781 match self {
782 Node::Const(_) => false,
783 Node::Var(s) => *s == slot,
784 Node::Neg(inner) => inner.references(slot),
785 Node::Bin(_, l, r) => l.references(slot) || r.references(slot),
786 Node::Call(_, args) => args.iter().any(|arg| arg.references(slot)),
787 }
788 }
789
790 fn eval(&self, vars: &Variables<'_>) -> f32 {
791 match self {
792 Node::Const(c) => *c,
793 Node::Var(slot) => vars.get(*slot),
794 Node::Neg(inner) => -inner.eval(vars),
795 Node::Bin(op, l, r) => {
796 let a = l.eval(vars);
797 let b = r.eval(vars);
798 match op {
799 BinOp::Add => a + b,
800 BinOp::Sub => a - b,
801 BinOp::Mul => a * b,
802 // f32 division by zero yields inf/NaN, not a panic — fine
803 // for a display value; expressions never divide silently.
804 BinOp::Div => a / b,
805 // Comparisons yield a clean 0.0/1.0 so they compose with
806 // arithmetic. A NaN operand compares false everywhere, so
807 // the result is 0.0 (except `!=`, where NaN != NaN is true
808 // by IEEE rule) — total either way.
809 BinOp::Gt => f32::from(a > b),
810 BinOp::Lt => f32::from(a < b),
811 BinOp::Ge => f32::from(a >= b),
812 BinOp::Le => f32::from(a <= b),
813 BinOp::Eq => f32::from(a == b),
814 BinOp::Ne => f32::from(a != b),
815 }
816 }
817 // Arity is guaranteed by the parser; slice patterns keep this
818 // indexing- and panic-free, with a safe default for completeness.
819 Node::Call(func, args) => match (func, args.as_ref()) {
820 (Func::Sin, [x]) => x.eval(vars).sin(),
821 (Func::Cos, [x]) => x.eval(vars).cos(),
822 (Func::Abs, [x]) => x.eval(vars).abs(),
823 (Func::Floor, [x]) => x.eval(vars).floor(),
824 // Out-of-domain input yields NaN, not a panic.
825 (Func::Sqrt, [x]) => x.eval(vars).sqrt(),
826 // Same posture as `sqrt` rather than a new rule: mathematically
827 // honest at the edges, so `log(0)` is -inf and `log(-1)` is NaN.
828 // `max` and `select` are the guard idiom (see `docs/presets.md`).
829 (Func::Log, [x]) => x.eval(vars).ln(),
830 (Func::Min, [a, b]) => a.eval(vars).min(b.eval(vars)),
831 (Func::Max, [a, b]) => a.eval(vars).max(b.eval(vars)),
832 (Func::Pow, [b, e]) => b.eval(vars).powf(e.eval(vars)),
833 // Floored (divisor-signed) modulo, so it wraps cleanly for
834 // cyclic hue/time: mod(-0.2, 1.0) is 0.8, not -0.2. A zero
835 // divisor yields NaN rather than panicking.
836 (Func::Mod, [a, b]) => {
837 let a = a.eval(vars);
838 let b = b.eval(vars);
839 a - b * (a / b).floor()
840 }
841 // Manual clamp: std f32::clamp panics if lo > hi; max().min()
842 // is total.
843 (Func::Clamp, [x, lo, hi]) => x.eval(vars).max(lo.eval(vars)).min(hi.eval(vars)),
844 (Func::Lerp, [a, b, t]) => {
845 let a = a.eval(vars);
846 let b = b.eval(vars);
847 a + (b - a) * t.eval(vars)
848 }
849 (Func::Smoothstep, [e0, e1, x]) => {
850 let e0 = e0.eval(vars);
851 let e1 = e1.eval(vars);
852 // Same total max().min() clamp as above, deliberately not
853 // f32::clamp: a degenerate e0 == e1 divides by zero, and
854 // max().min() folds the resulting +-inf/NaN into [0, 1]
855 // (f32::max returns the non-NaN operand) where `clamp`
856 // would propagate the NaN into the scene parameter.
857 #[allow(clippy::manual_clamp)]
858 let t = ((x.eval(vars) - e0) / (e1 - e0)).max(0.0).min(1.0);
859 t * t * (3.0 - 2.0 * t)
860 }
861 // Only the taken branch is evaluated, so the untaken one cannot
862 // poison the result: `select(x >= 0, sqrt(x), 0)` is safe in a
863 // way a `lerp` blend of both branches would not be.
864 (Func::Select, [cond, x, y]) => {
865 if cond.eval(vars) != 0.0 {
866 x.eval(vars)
867 } else {
868 y.eval(vars)
869 }
870 }
871 // The one call that reads the variable bundle's non-scalar
872 // payload. Total for every input (see `Variables::bin`).
873 (Func::Bin, [x]) => vars.bin(x.eval(vars)),
874 // The two seeded functions (ADR-0051). Like `bin` they read the
875 // bundle rather than their arguments alone — but what they read
876 // is a load-time constant, so the expression stays pure: same
877 // argument, same salt, bit-identical result, every frame.
878 (Func::Hash, [x]) => hash01(x.eval(vars), vars.salt),
879 (Func::Noise, [x]) => value_noise(x.eval(vars), vars.salt),
880 _ => 0.0,
881 },
882 }
883 }
884
885 /// Walk the subtree rooted here — whose own node index is `index` — and
886 /// record what each comparison, each `select()` condition and each `clamp()`
887 /// bound did under `vars`. Descends only into the branch a `select()`
888 /// actually took, which is the whole point: an unreached subtree stays
889 /// [`NodeObservation::Untouched`].
890 ///
891 /// **This records; it does not compute.** The value of a probed evaluation
892 /// comes from [`Node::eval`] itself (see [`Expr::eval_probed`]), so there is
893 /// no second copy of the arithmetic to drift out of step with the first —
894 /// the divergence ADR-0042 names as this approach's main cost is removed by
895 /// construction rather than merely tested for. The price is that comparisons,
896 /// conditions and clamp arguments are evaluated twice per probed call — and a
897 /// comparison nested under another one compounds it — which is free:
898 /// expressions are pure and nothing but the harness calls this.
899 fn probe(&self, vars: &Variables<'_>, obs: &mut Observations, index: usize) {
900 match self {
901 Node::Const(_) | Node::Var(_) => {}
902 Node::Neg(inner) => inner.probe(vars, obs, index + 1),
903 Node::Bin(op, l, r) => {
904 // A comparison is a gate whether or not it sits in a `select()`
905 // (ADR-0043): `reseed = "onset > 0.55"` is the idiomatic boolean
906 // form and contains no `select()` at all. Arithmetic operators
907 // carry no branch, so they only recurse.
908 if op.is_comparison() {
909 obs.record_compare(index, self.eval(vars) != 0.0);
910 }
911 // Both operands are evaluated either way, so both are live.
912 l.probe(vars, obs, index + 1);
913 r.probe(vars, obs, index + 1 + l.node_count());
914 }
915 Node::Call(func, args) => match (func, args.as_ref()) {
916 (Func::Select, [cond, x, y]) => {
917 let taken = cond.eval(vars) != 0.0;
918 obs.record_select(index, taken);
919 let cond_at = index + 1;
920 let x_at = cond_at + cond.node_count();
921 cond.probe(vars, obs, cond_at);
922 // Only the live branch, matching what `eval` executes.
923 if taken {
924 x.probe(vars, obs, x_at);
925 } else {
926 y.probe(vars, obs, x_at + x.node_count());
927 }
928 }
929 (Func::Clamp, [x, lo, hi]) => {
930 obs.record_clamp(index, x.eval(vars), hi.eval(vars));
931 let x_at = index + 1;
932 let lo_at = x_at + x.node_count();
933 x.probe(vars, obs, x_at);
934 lo.probe(vars, obs, lo_at);
935 hi.probe(vars, obs, lo_at + lo.node_count());
936 }
937 // Every other call evaluates all of its arguments, so all of
938 // them are on the live path.
939 (_, rest) => {
940 let mut at = index + 1;
941 for arg in rest {
942 arg.probe(vars, obs, at);
943 at += arg.node_count();
944 }
945 }
946 },
947 }
948 }
949
950 /// Re-render this subtree as source text, parenthesized only where its own
951 /// precedence is below `parent_prec`. This is what *names* a flagged gate:
952 /// a node index tells a reader nothing, and the preset's original text is
953 /// not kept past compile.
954 ///
955 /// Round-trips through [`compile`] (asserted in the tests) but is not
956 /// character-identical to what the author wrote — whitespace and redundant
957 /// parentheses are gone, and `2` prints for `2.0`.
958 fn write_source(&self, out: &mut String, parent_prec: u8) {
959 match self {
960 Node::Const(c) => out.push_str(&c.to_string()),
961 Node::Var(slot) => out.push_str(VAR_NAMES.get(*slot).copied().unwrap_or("?")),
962 Node::Neg(inner) => {
963 // Unary binds tighter than everything but a call, so it only
964 // needs wrapping inside another unary-or-higher context.
965 let wrap = parent_prec > PREC_UNARY;
966 if wrap {
967 out.push('(');
968 }
969 out.push('-');
970 inner.write_source(out, PREC_UNARY);
971 if wrap {
972 out.push(')');
973 }
974 }
975 Node::Bin(op, l, r) => {
976 let prec = op.precedence();
977 let wrap = prec < parent_prec;
978 if wrap {
979 out.push('(');
980 }
981 l.write_source(out, prec);
982 out.push(' ');
983 out.push_str(op.symbol());
984 out.push(' ');
985 // The right operand of a left-associative tier needs one more
986 // level, so `a - (b - c)` keeps its parentheses.
987 r.write_source(out, prec + 1);
988 if wrap {
989 out.push(')');
990 }
991 }
992 Node::Call(func, args) => {
993 out.push_str(func.name());
994 out.push('(');
995 for (i, arg) in args.iter().enumerate() {
996 if i > 0 {
997 out.push_str(", ");
998 }
999 arg.write_source(out, PREC_CMP);
1000 }
1001 out.push(')');
1002 }
1003 }
1004 }
1005
1006 /// This subtree as source text, at statement level.
1007 fn source(&self) -> String {
1008 let mut out = String::new();
1009 self.write_source(&mut out, PREC_CMP);
1010 out
1011 }
1012}
1013
1014/// Precedence tiers for [`Node::write_source`], matching the grammar in the
1015/// module docs: comparisons are loosest, a call or literal binds tightest.
1016const PREC_CMP: u8 = 0;
1017const PREC_SUM: u8 = 2;
1018const PREC_TERM: u8 = 4;
1019const PREC_UNARY: u8 = 6;
1020
1021/// A compiled expression: parse once, [`eval`](Expr::eval) every frame.
1022#[derive(Debug)]
1023pub struct Expr {
1024 root: Node,
1025 /// Whether the expression names `index` anywhere — decided **once at
1026 /// compile**, because it is a property of the source text and cannot change
1027 /// while the preset is loaded. This is what lets the frame loop ask "is this
1028 /// binding per-element?" for the price of reading a `bool`.
1029 uses_index: bool,
1030 /// Whether the expression names any of `x`, `y`, `rad`, `ang` — decided at
1031 /// compile for [`uses_index`](Self::uses_index)'s reason.
1032 ///
1033 /// Unlike `uses_index` this does **not** select a code path: a binding is
1034 /// per-vertex because it sits in a `[per_vertex]` table, not because of what
1035 /// it names (Plan 0100 Phase 1). It is read by the loader, which warns when
1036 /// an ordinary `[params]` binding reaches for a vertex variable that will
1037 /// read a flat zero there.
1038 uses_vertex: bool,
1039}
1040
1041impl Expr {
1042 /// Evaluate against a variable binding. Total and allocation-free.
1043 pub fn eval(&self, vars: &Variables<'_>) -> f32 {
1044 self.root.eval(vars)
1045 }
1046
1047 /// Evaluate exactly as [`eval`](Expr::eval) does, additionally accumulating
1048 /// per-node reachability into `obs` (Plan 0041 / ADR-0042).
1049 ///
1050 /// **Harness only.** Nothing on the render path calls this: it allocates
1051 /// (the observation arena grows on first touch) and it walks the tree twice.
1052 /// `eval` is untouched and remains the only thing a frame executes.
1053 ///
1054 /// Call it repeatedly with the *same* `obs` across a run of varying
1055 /// [`Variables`] — one `Observations` per expression. What accumulates is
1056 /// which way each comparison and each `select()` condition went, and — for
1057 /// each `clamp()` — both how close its inner value came to the upper bound
1058 /// and how many hops it spent *at* that bound (ADR-0062);
1059 /// [`flag_gates`](Expr::flag_gates) reads the verdict back out.
1060 pub fn eval_probed(&self, vars: &Variables<'_>, obs: &mut Observations) -> f32 {
1061 self.root.probe(vars, obs, 0);
1062 // The value is `eval`'s, not a re-derivation of it.
1063 self.root.eval(vars)
1064 }
1065
1066 /// The gates `obs` never saw exercised, named by their source text.
1067 ///
1068 /// Read every one as a **suspect, not a conviction**: it says the run these
1069 /// observations came from never drove the gate both ways, which is a
1070 /// property of the stimulus as much as of the preset. A gate on `tempo` is
1071 /// correctly one-sided under a single-BPM generator.
1072 ///
1073 /// Nodes never reached at all are silent — a `select()` buried inside a dead
1074 /// branch is not a second finding, it is the same one. Fix the outer gate
1075 /// and the inner one starts reporting.
1076 pub fn flag_gates(&self, obs: &Observations) -> Vec<GateFlag> {
1077 let mut flags = Vec::new();
1078 // The root is nobody's condition, so a bare `onset > 0.55` reports.
1079 collect_flags(&self.root, obs, 0, false, &mut flags);
1080 flags
1081 }
1082
1083 /// This expression rendered back to source text (normalized whitespace and
1084 /// parentheses, not the author's original characters).
1085 pub fn source(&self) -> String {
1086 self.root.source()
1087 }
1088
1089 /// Whether this expression references the per-element `index`, i.e. whether
1090 /// it wants to be evaluated **once per element** rather than once per frame
1091 /// (Plan 0034 Phase 4). Free to call — the answer was computed at compile.
1092 pub fn uses_index(&self) -> bool {
1093 self.uses_index
1094 }
1095
1096 /// The value this expression **always** takes, when it takes only one.
1097 ///
1098 /// `Some` exactly when the compiled root is a constant — a literal, or
1099 /// anything the compiler folded to one (`2 * 0.008` folds; `bass * 0` does
1100 /// not, and deliberately: the fold is syntactic and a binding that names a
1101 /// variable is not resting anywhere).
1102 ///
1103 /// Read by the loader, which can only warn about a value it knows a binding
1104 /// *rests* at. An expression that sweeps through a bad range is not
1105 /// something a load-time check can see, and pretending otherwise would put
1106 /// a false warning on every preset that animates the parameter.
1107 pub fn as_const(&self) -> Option<f32> {
1108 match self.root {
1109 Node::Const(c) => Some(c),
1110 _ => None,
1111 }
1112 }
1113
1114 /// Whether this expression references any per-vertex position variable
1115 /// (`x`, `y`, `rad`, `ang`) — Plan 0100 Phase 1. Free to call; the answer was
1116 /// computed at compile.
1117 ///
1118 /// The loader uses it for a warning, not for routing: only a `[per_vertex]`
1119 /// table's bindings are evaluated per vertex, and outside one these names
1120 /// read `0`.
1121 pub fn uses_vertex(&self) -> bool {
1122 self.uses_vertex
1123 }
1124
1125 /// Whether this expression reads the latch at `slot` in its preset's
1126 /// `[latch]` order (ADR-0137).
1127 ///
1128 /// Not precomputed like the two above, because nothing routes on it: the
1129 /// loader asks it once per latch per binding, to warn about a latch no
1130 /// binding names. Never called per frame.
1131 pub fn uses_latch(&self, slot: usize) -> bool {
1132 slot < LATCH_CAP && self.root.references(LATCH_SLOT_BASE + slot)
1133 }
1134}
1135
1136/// Walk `node` (whose index is `index`) and report the gates `obs` never saw
1137/// exercised. Recurses into every child, including a flagged gate's own
1138/// branches — a dead gate can still contain a live one.
1139///
1140/// `is_select_condition` is true only when `node` is the **direct** first
1141/// argument of an enclosing `select()`. A one-sided comparison there is
1142/// suppressed, because that `select()` already reports it and in better words:
1143/// a gate flag names the *consequence* ("its `then` branch never ran"), which a
1144/// comparison flag cannot (ADR-0043). Stating the rule as tree position rather
1145/// than as a property of the operator is what keeps it from drifting out of step
1146/// with the grammar — a construct that later grows a condition child reports
1147/// noisily through the comparison rule until it is taught to report its own.
1148fn collect_flags(
1149 node: &Node,
1150 obs: &Observations,
1151 index: usize,
1152 is_select_condition: bool,
1153 out: &mut Vec<GateFlag>,
1154) {
1155 match (node, obs.node(index)) {
1156 (
1157 Node::Call(Func::Select, args),
1158 NodeObservation::Select {
1159 saw_true,
1160 saw_false,
1161 },
1162 ) if saw_true != saw_false => {
1163 // The condition is the text worth printing, not the whole call: it
1164 // is the part an author has to re-gain.
1165 out.push(GateFlag {
1166 kind: GateKind::Select { always: saw_true },
1167 source: args.first().map(Node::source).unwrap_or_default(),
1168 });
1169 }
1170 (
1171 Node::Bin(..),
1172 NodeObservation::Compare {
1173 saw_true,
1174 saw_false,
1175 },
1176 ) if saw_true != saw_false && !is_select_condition => {
1177 // The comparison names itself: unlike a `select()`, there is no
1178 // enclosing call whose branches are the interesting part.
1179 out.push(GateFlag {
1180 kind: GateKind::Compare { always: saw_true },
1181 source: node.source(),
1182 });
1183 }
1184 (
1185 Node::Call(Func::Clamp, _),
1186 NodeObservation::Clamp {
1187 peak_fraction_of_bound,
1188 hops_at_bound,
1189 hops,
1190 },
1191 ) => {
1192 // The two findings are mutually exclusive by construction: a peak
1193 // below the bound means no hop reached it, so occupancy is `0`.
1194 // Written as an `if`/`else if` anyway, so neither can ever be
1195 // reported twice about one node.
1196 let occupancy = occupancy_of(hops_at_bound, hops);
1197 if peak_fraction_of_bound < 1.0 {
1198 out.push(GateFlag {
1199 kind: GateKind::Clamp {
1200 peak_fraction_of_bound,
1201 },
1202 source: node.source(),
1203 });
1204 } else if occupancy >= SATURATED_OCCUPANCY {
1205 out.push(GateFlag {
1206 kind: GateKind::Saturated { occupancy },
1207 source: node.source(),
1208 });
1209 }
1210 }
1211 _ => {}
1212 }
1213
1214 let mut at = index + 1;
1215 match node {
1216 Node::Const(_) | Node::Var(_) => {}
1217 Node::Neg(inner) => collect_flags(inner, obs, at, false, out),
1218 Node::Bin(_, l, r) => {
1219 collect_flags(l, obs, at, false, out);
1220 collect_flags(r, obs, at + l.node_count(), false, out);
1221 }
1222 Node::Call(func, args) => {
1223 for (i, arg) in args.iter().enumerate() {
1224 // Only argument 0 of a `select()` is a condition. A comparison
1225 // one level deeper — `select(min(tempo > 124, bass > 0.38), …)` —
1226 // is *not* suppressed, which is the whole point: the excusable
1227 // half must not launder the inexcusable one.
1228 let condition = matches!(func, Func::Select) && i == 0;
1229 collect_flags(arg, obs, at, condition, out);
1230 at += arg.node_count();
1231 }
1232 }
1233 }
1234}
1235
1236/// Per-AST-node reachability accumulated across a run of probed evaluations
1237/// (ADR-0042). One of these belongs to one [`Expr`].
1238///
1239/// Lives only in the harness path: [`Expr::eval`] neither reads nor writes it,
1240/// and no type a frame touches gained a field for it.
1241#[derive(Debug, Default, Clone)]
1242pub struct Observations {
1243 /// Indexed by the node's pre-order position in its expression's tree. The
1244 /// index of a node does **not** depend on which branch a `select()` took —
1245 /// an untaken subtree still occupies its slots — so observations from
1246 /// different evaluations land in the same places.
1247 nodes: Vec<NodeObservation>,
1248}
1249
1250impl Observations {
1251 /// An empty set of observations. Grows to fit as nodes are touched.
1252 pub fn new() -> Self {
1253 Self::default()
1254 }
1255
1256 /// What was observed at `index`; [`NodeObservation::Untouched`] for a node
1257 /// this run never reached.
1258 pub fn node(&self, index: usize) -> NodeObservation {
1259 self.nodes.get(index).copied().unwrap_or_default()
1260 }
1261
1262 /// Every recorded slot, in node order.
1263 pub fn nodes(&self) -> &[NodeObservation] {
1264 &self.nodes
1265 }
1266
1267 /// The slot for `index`, growing the arena to fit. `None` is unreachable
1268 /// after the resize — it is how this file stays free of a panic path.
1269 fn slot(&mut self, index: usize) -> Option<&mut NodeObservation> {
1270 if self.nodes.len() <= index {
1271 self.nodes.resize(index + 1, NodeObservation::Untouched);
1272 }
1273 self.nodes.get_mut(index)
1274 }
1275
1276 fn record_select(&mut self, index: usize, taken: bool) {
1277 let Some(slot) = self.slot(index) else {
1278 return;
1279 };
1280 let (was_true, was_false) = match *slot {
1281 NodeObservation::Select {
1282 saw_true,
1283 saw_false,
1284 } => (saw_true, saw_false),
1285 _ => (false, false),
1286 };
1287 *slot = NodeObservation::Select {
1288 saw_true: was_true || taken,
1289 saw_false: was_false || !taken,
1290 };
1291 }
1292
1293 /// Record which way a comparison operator went. Same two-valued shape as
1294 /// [`record_select`](Self::record_select) — deliberately, so the reporting
1295 /// logic reads the same verdict out of both (ADR-0043).
1296 fn record_compare(&mut self, index: usize, taken: bool) {
1297 let Some(slot) = self.slot(index) else {
1298 return;
1299 };
1300 let (was_true, was_false) = match *slot {
1301 NodeObservation::Compare {
1302 saw_true,
1303 saw_false,
1304 } => (saw_true, saw_false),
1305 _ => (false, false),
1306 };
1307 *slot = NodeObservation::Compare {
1308 saw_true: was_true || taken,
1309 saw_false: was_false || !taken,
1310 };
1311 }
1312
1313 /// Record how close `value` came to the clamp's upper bound `hi`, and
1314 /// whether it reached it (ADR-0062).
1315 ///
1316 /// The two statistics are opposite ends of the same measurement and they
1317 /// **err in opposite directions**, because they accuse of opposite things.
1318 /// A non-positive or non-finite bound is recorded as *reached* for the peak
1319 /// — "fraction of the bound" means nothing there, and the peak's finding is
1320 /// "the ceiling never bit", which would be a false accusation. The same
1321 /// bound counts as *not at bound* for occupancy, whose finding is "the
1322 /// ceiling never released". Each declines to convict on a bound it cannot
1323 /// read.
1324 fn record_clamp(&mut self, index: usize, value: f32, hi: f32) {
1325 let usable = hi.is_finite() && hi > 0.0;
1326 let fraction = if usable { value / hi } else { 1.0 };
1327 // NaN compares false, so a NaN inner value never counts as pinned.
1328 let at_bound = usable && value >= hi;
1329 let Some(slot) = self.slot(index) else {
1330 return;
1331 };
1332 let (previous, was_at_bound, was_hops) = match *slot {
1333 NodeObservation::Clamp {
1334 peak_fraction_of_bound,
1335 hops_at_bound,
1336 hops,
1337 } => (peak_fraction_of_bound, hops_at_bound, hops),
1338 _ => (f32::NEG_INFINITY, 0, 0),
1339 };
1340 *slot = NodeObservation::Clamp {
1341 // `max` returns the non-NaN operand, so a NaN inner value cannot
1342 // poison the peak.
1343 peak_fraction_of_bound: previous.max(fraction),
1344 hops_at_bound: was_at_bound.saturating_add(u32::from(at_bound)),
1345 hops: was_hops.saturating_add(1),
1346 };
1347 }
1348}
1349
1350/// Occupancy from the two counters: the fraction of evaluated hops a `clamp()`
1351/// spent at its upper bound. A clamp no hop ever evaluated reports `0.0` rather
1352/// than dividing by zero — an unreached node makes no claim, exactly as
1353/// [`NodeObservation::Untouched`] does everywhere else in this file.
1354fn occupancy_of(hops_at_bound: u32, hops: u32) -> f32 {
1355 if hops == 0 {
1356 0.0
1357 } else {
1358 hops_at_bound as f32 / hops as f32
1359 }
1360}
1361
1362/// What one AST node did across a run.
1363#[derive(Debug, Default, Clone, Copy, PartialEq)]
1364pub enum NodeObservation {
1365 /// Never evaluated — either not a comparison/`select()`/`clamp()`, or inside
1366 /// a branch the run never took.
1367 #[default]
1368 Untouched,
1369 /// A `select()` condition: did it ever go each way?
1370 Select {
1371 /// The condition evaluated non-zero at least once.
1372 saw_true: bool,
1373 /// The condition evaluated zero at least once.
1374 saw_false: bool,
1375 },
1376 /// A comparison operator (`> < >= <= == !=`), wherever it sits in the tree.
1377 /// Same two-valued shape as [`Select`](Self::Select) — deliberately, so the
1378 /// reporting logic is shared (ADR-0043). A comparison that is the direct
1379 /// condition of a `select()` is still observed here; it is *reporting* that
1380 /// suppresses it, because the `select()` names it in better words.
1381 Compare {
1382 /// The comparison evaluated true at least once.
1383 saw_true: bool,
1384 /// The comparison evaluated false at least once.
1385 saw_false: bool,
1386 },
1387 /// A `clamp()`: how close the inner value came to the upper bound, and how
1388 /// long it sat there. The two are opposite ends of one measurement
1389 /// (ADR-0062). A peak below `1.0` across a whole run means the bound never
1390 /// bit at this stimulus — the ceiling is decorative and the parameter's real
1391 /// range is narrower than the preset reads. An occupancy near `1.0` means
1392 /// the opposite and worse thing: the bound bit and never let go, so the
1393 /// binding is an arithmetic expression that has become a constant.
1394 Clamp {
1395 /// Peak of `value / upper_bound` over the run.
1396 peak_fraction_of_bound: f32,
1397 /// Hops where the inner value reached the upper bound.
1398 hops_at_bound: u32,
1399 /// Hops this clamp was evaluated on at all — the denominator of
1400 /// [`occupancy`](NodeObservation::occupancy).
1401 hops: u32,
1402 },
1403}
1404
1405impl NodeObservation {
1406 /// The fraction of evaluated hops a `clamp()` spent at its upper bound;
1407 /// `0.0` for anything that is not a clamp, and for a clamp no hop reached.
1408 pub fn occupancy(self) -> f32 {
1409 match self {
1410 NodeObservation::Clamp {
1411 hops_at_bound,
1412 hops,
1413 ..
1414 } => occupancy_of(hops_at_bound, hops),
1415 _ => 0.0,
1416 }
1417 }
1418}
1419
1420/// A gate that a run never exercised, with the source text that names it.
1421#[derive(Debug, Clone, PartialEq)]
1422pub struct GateFlag {
1423 /// Which kind of gate, and what it did.
1424 pub kind: GateKind,
1425 /// The gate's source: a `select()`'s **condition**, a comparison's own text,
1426 /// or a `clamp()`'s whole call. Re-rendered from the AST (see
1427 /// [`Expr::source`]), so whitespace and redundant parentheses will not match
1428 /// the preset file character for character.
1429 pub source: String,
1430}
1431
1432/// Occupancy at or above which a `clamp()` is reported as
1433/// [`Saturated`](GateKind::Saturated) — the fraction of hops its inner value may
1434/// spend pinned at the upper bound before the binding stops being a function of
1435/// the audio and becomes a constant (ADR-0062).
1436///
1437/// **A measured constant, not a principled one.** Plan 0056 Phase 3 measured
1438/// both sides of it — the library that passes, and the library that should not —
1439/// over 339 clamped bindings each, on the 12 s `dynamic:110` probe:
1440///
1441/// ```text
1442/// occupancy today pre-retune (80c5dff^)
1443/// [0.00, 0.10) 29 6
1444/// [0.10, 0.25) 171 11
1445/// [0.25, 0.50) 138 22
1446/// [0.50, 0.75) 1 51
1447/// [0.75, 0.90) 0 104
1448/// [0.90, 1.01) 0 145
1449/// ```
1450///
1451/// The retuned library's highest is `0.609` (`Aurora.warp`) and its next is
1452/// `0.444`, so `0.9` clears the measured maximum by `0.29` and the body of the
1453/// distribution by twice that. The saturated library it exists to catch puts
1454/// **145 bindings across 23 of 35 presets** above it — the gate would have failed
1455/// the build the day ADR-0049 landed.
1456///
1457/// Two things this value is not. It is not the most *sensitive* threshold that
1458/// still separates the two libraries: `0.75` would catch 249 of the 339
1459/// pre-retune bindings rather than 145, but it would sit only `0.14` above a
1460/// shipped, reviewed preset, and a HARD gate that fires on good content buys
1461/// exemptions — which are the thing that dulls the instrument. And it does not
1462/// see the *marginal* form of the defect: one binding pinned for 50-90 % of a
1463/// track, in a preset with no severe case beside it, passes. What makes that
1464/// acceptable is that the defect arrives in clusters — every affected preset in
1465/// the pre-retune set carried a severe case too.
1466///
1467/// It has a shelf life. Re-measure it whenever the library changes materially,
1468/// and expect to move it rather than to bless a preset through it.
1469pub const SATURATED_OCCUPANCY: f32 = 0.9;
1470
1471/// The four structural findings [`Expr::flag_gates`] reports.
1472#[derive(Debug, Clone, Copy, PartialEq)]
1473pub enum GateKind {
1474 /// A `select()` whose condition only ever went one way — so one branch is
1475 /// dead and the preset renders as if the `select()` were the constant it
1476 /// always chose.
1477 Select {
1478 /// The side it always took: `true` means the condition never went false.
1479 always: bool,
1480 },
1481 /// A comparison that only ever took one value, and that no `select()` flag
1482 /// already names (ADR-0043). Either the whole binding is the comparison
1483 /// (`reseed = "onset > 0.55"` — a boolean param stuck at one value), or it
1484 /// is a term inside a composite condition, where it is the half a
1485 /// `select()` flag would have hidden behind the other.
1486 Compare {
1487 /// The value it always took: `true` means the comparison never went
1488 /// false.
1489 always: bool,
1490 },
1491 /// A `clamp()` whose inner value never approached its upper bound.
1492 Clamp {
1493 /// Peak of `value / upper_bound` over the run.
1494 peak_fraction_of_bound: f32,
1495 },
1496 /// A `clamp()` whose inner value sat **at** its upper bound for at least
1497 /// [`SATURATED_OCCUPANCY`] of the run (ADR-0062). The mirror of
1498 /// [`Clamp`](Self::Clamp) and the more serious of the two: a decorative
1499 /// ceiling only narrows a parameter's real range, while a ceiling that never
1500 /// releases has turned the binding into a constant that no reachability
1501 /// walk can see, because a gain contains no fork to observe.
1502 ///
1503 /// The number states its own fix. `0.97` on `clamp(mid * 16, 0, 0.3)` means
1504 /// the ceiling is reached at `mid = 0.019`, so the gain is 16x too hot.
1505 Saturated {
1506 /// Fraction of evaluated hops spent at the upper bound.
1507 occupancy: f32,
1508 },
1509}
1510
1511/// Why an expression failed to compile. Evaluation never errors.
1512#[derive(Debug, Clone, PartialEq)]
1513pub enum ExprError {
1514 /// A character the tokenizer does not recognize.
1515 UnexpectedChar(char),
1516 /// A numeric literal that does not parse as `f32`.
1517 BadNumber(String),
1518 /// An identifier that is neither a known variable nor function.
1519 UnknownIdent(String),
1520 /// A function called with the wrong number of arguments.
1521 WrongArity {
1522 /// Function name.
1523 func: String,
1524 /// Arity the function requires.
1525 expected: usize,
1526 /// Arity supplied.
1527 got: usize,
1528 },
1529 /// A token appeared where the grammar did not allow it.
1530 UnexpectedToken(String),
1531 /// The expression ended earlier than the grammar allows.
1532 UnexpectedEnd,
1533 /// Extra tokens remained after a complete expression.
1534 TrailingTokens,
1535}
1536
1537impl fmt::Display for ExprError {
1538 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1539 match self {
1540 ExprError::UnexpectedChar(c) => write!(f, "unexpected character '{c}'"),
1541 ExprError::BadNumber(s) => write!(f, "invalid number '{s}'"),
1542 ExprError::UnknownIdent(s) => write!(f, "unknown variable or function '{s}'"),
1543 ExprError::WrongArity {
1544 func,
1545 expected,
1546 got,
1547 } => write!(f, "{func}() takes {expected} argument(s), got {got}"),
1548 ExprError::UnexpectedToken(s) => write!(f, "unexpected token '{s}'"),
1549 ExprError::UnexpectedEnd => write!(f, "unexpected end of expression"),
1550 ExprError::TrailingTokens => write!(f, "unexpected trailing tokens"),
1551 }
1552 }
1553}
1554
1555impl std::error::Error for ExprError {}
1556
1557/// Compile a source expression into an evaluatable [`Expr`], with no latch
1558/// names in scope.
1559///
1560/// The entry point for every expression that is not a preset binding — probes,
1561/// tests, and the two `[latch]` expressions themselves, which deliberately
1562/// cannot read a latch (see [`compile_with_latches`]).
1563pub fn compile(src: &str) -> Result<Expr, ExprError> {
1564 compile_with_latches(src, &[])
1565}
1566
1567/// [`compile`], with a preset's `[latch]` names in scope (ADR-0137).
1568///
1569/// `latches` is the preset's latch names **in slot order**: entry `i` resolves
1570/// to `LATCH_SLOT_BASE + i`, which is the whole of the name-to-slot resolution
1571/// and it happens once, here, at load — exactly as `Binding::tau` is read out of
1572/// `[smoothing]` once. Nothing per-frame looks a latch name up. Entries past
1573/// [`LATCH_CAP`] are unreachable; the loader rejects them before this is called,
1574/// and the `take` below means a caller that did not would get an unknown
1575/// identifier rather than a slot outside the block.
1576///
1577/// A latch name is resolved **after** the constants and the built-in variables,
1578/// so nothing an author can declare shadows `bass` or `pi`. The loader also
1579/// rejects a colliding name outright, which is what makes this ordering
1580/// unobservable rather than a silently preferred one.
1581pub fn compile_with_latches(src: &str, latches: &[String]) -> Result<Expr, ExprError> {
1582 let tokens = tokenize(src)?;
1583 let mut parser = Parser {
1584 tokens,
1585 pos: 0,
1586 latches,
1587 };
1588 let root = parser.parse_expr()?;
1589 if parser.pos != parser.tokens.len() {
1590 return Err(ExprError::TrailingTokens);
1591 }
1592 let uses_index = root.references(INDEX_SLOT);
1593 let uses_vertex = (VERTEX_SLOT_BASE..VERTEX_SLOT_BASE + 4).any(|slot| root.references(slot));
1594 Ok(Expr {
1595 root,
1596 uses_index,
1597 uses_vertex,
1598 })
1599}
1600
1601#[derive(Debug, Clone, PartialEq)]
1602enum Token {
1603 Num(f32),
1604 Ident(String),
1605 Plus,
1606 Minus,
1607 Star,
1608 Slash,
1609 LParen,
1610 RParen,
1611 Comma,
1612 Gt,
1613 Lt,
1614 Ge,
1615 Le,
1616 EqEq,
1617 NotEq,
1618}
1619
1620impl Token {
1621 fn describe(&self) -> String {
1622 match self {
1623 Token::Num(n) => n.to_string(),
1624 Token::Ident(s) => s.clone(),
1625 Token::Plus => "+".into(),
1626 Token::Minus => "-".into(),
1627 Token::Star => "*".into(),
1628 Token::Slash => "/".into(),
1629 Token::LParen => "(".into(),
1630 Token::RParen => ")".into(),
1631 Token::Comma => ",".into(),
1632 Token::Gt => ">".into(),
1633 Token::Lt => "<".into(),
1634 Token::Ge => ">=".into(),
1635 Token::Le => "<=".into(),
1636 Token::EqEq => "==".into(),
1637 Token::NotEq => "!=".into(),
1638 }
1639 }
1640}
1641
1642/// Consume a following `=` (the second half of `>=`/`<=`/`==`/`!=`), reporting
1643/// whether one was there. At end of input `peek` yields `None`, so a trailing
1644/// bare `>` tokenizes as `Gt` instead of reading past the end.
1645fn eat_eq(chars: &mut std::iter::Peekable<std::str::Chars<'_>>) -> bool {
1646 if matches!(chars.peek(), Some('=')) {
1647 chars.next();
1648 true
1649 } else {
1650 false
1651 }
1652}
1653
1654fn tokenize(src: &str) -> Result<Vec<Token>, ExprError> {
1655 let mut tokens = Vec::new();
1656 let mut chars = src.chars().peekable();
1657 while let Some(&c) = chars.peek() {
1658 match c {
1659 c if c.is_whitespace() => {
1660 chars.next();
1661 }
1662 '+' => {
1663 chars.next();
1664 tokens.push(Token::Plus);
1665 }
1666 '-' => {
1667 chars.next();
1668 tokens.push(Token::Minus);
1669 }
1670 '*' => {
1671 chars.next();
1672 tokens.push(Token::Star);
1673 }
1674 '/' => {
1675 chars.next();
1676 tokens.push(Token::Slash);
1677 }
1678 '(' => {
1679 chars.next();
1680 tokens.push(Token::LParen);
1681 }
1682 ')' => {
1683 chars.next();
1684 tokens.push(Token::RParen);
1685 }
1686 ',' => {
1687 chars.next();
1688 tokens.push(Token::Comma);
1689 }
1690 // Two-char comparison forms need one char of lookahead. A trailing
1691 // bare `>`/`<` at end of input still tokenizes (peek yields None).
1692 '>' => {
1693 chars.next();
1694 let tok = if eat_eq(&mut chars) {
1695 Token::Ge
1696 } else {
1697 Token::Gt
1698 };
1699 tokens.push(tok);
1700 }
1701 '<' => {
1702 chars.next();
1703 let tok = if eat_eq(&mut chars) {
1704 Token::Le
1705 } else {
1706 Token::Lt
1707 };
1708 tokens.push(tok);
1709 }
1710 // `=` and `!` are only valid as the two-char forms; a bare one is an
1711 // explicit error rather than a silently-dropped character.
1712 '=' | '!' => {
1713 chars.next();
1714 if !eat_eq(&mut chars) {
1715 return Err(ExprError::UnexpectedChar(c));
1716 }
1717 tokens.push(if c == '=' { Token::EqEq } else { Token::NotEq });
1718 }
1719 c if c.is_ascii_digit() || c == '.' => {
1720 let mut num = String::new();
1721 while let Some(&d) = chars.peek() {
1722 if d.is_ascii_digit() || d == '.' {
1723 num.push(d);
1724 chars.next();
1725 } else {
1726 break;
1727 }
1728 }
1729 let value: f32 = num.parse().map_err(|_| ExprError::BadNumber(num.clone()))?;
1730 tokens.push(Token::Num(value));
1731 }
1732 c if c.is_ascii_alphabetic() || c == '_' => {
1733 let mut ident = String::new();
1734 while let Some(&d) = chars.peek() {
1735 if d.is_ascii_alphanumeric() || d == '_' {
1736 ident.push(d);
1737 chars.next();
1738 } else {
1739 break;
1740 }
1741 }
1742 tokens.push(Token::Ident(ident));
1743 }
1744 other => return Err(ExprError::UnexpectedChar(other)),
1745 }
1746 }
1747 Ok(tokens)
1748}
1749
1750struct Parser<'a> {
1751 tokens: Vec<Token>,
1752 pos: usize,
1753 /// This preset's `[latch]` names in slot order — empty for every expression
1754 /// compiled outside a preset's `[params]`.
1755 latches: &'a [String],
1756}
1757
1758impl Parser<'_> {
1759 fn peek(&self) -> Option<&Token> {
1760 self.tokens.get(self.pos)
1761 }
1762
1763 fn advance(&mut self) -> Option<&Token> {
1764 let tok = self.tokens.get(self.pos);
1765 if tok.is_some() {
1766 self.pos += 1;
1767 }
1768 tok
1769 }
1770
1771 /// The lowest-precedence tier: comparisons over sums. Left-associative, so
1772 /// a chained `a > b > c` parses as `(a > b) > c` — legal but rarely
1773 /// intended (the docs discourage it).
1774 fn parse_expr(&mut self) -> Result<Node, ExprError> {
1775 let mut left = self.parse_sum()?;
1776 while let Some(op) = match self.peek() {
1777 Some(Token::Gt) => Some(BinOp::Gt),
1778 Some(Token::Lt) => Some(BinOp::Lt),
1779 Some(Token::Ge) => Some(BinOp::Ge),
1780 Some(Token::Le) => Some(BinOp::Le),
1781 Some(Token::EqEq) => Some(BinOp::Eq),
1782 Some(Token::NotEq) => Some(BinOp::Ne),
1783 _ => None,
1784 } {
1785 self.pos += 1;
1786 let right = self.parse_sum()?;
1787 left = Node::Bin(op, Box::new(left), Box::new(right));
1788 }
1789 Ok(left)
1790 }
1791
1792 fn parse_sum(&mut self) -> Result<Node, ExprError> {
1793 let mut left = self.parse_term()?;
1794 while let Some(op) = match self.peek() {
1795 Some(Token::Plus) => Some(BinOp::Add),
1796 Some(Token::Minus) => Some(BinOp::Sub),
1797 _ => None,
1798 } {
1799 self.pos += 1;
1800 let right = self.parse_term()?;
1801 left = Node::Bin(op, Box::new(left), Box::new(right));
1802 }
1803 Ok(left)
1804 }
1805
1806 fn parse_term(&mut self) -> Result<Node, ExprError> {
1807 let mut left = self.parse_unary()?;
1808 while let Some(op) = match self.peek() {
1809 Some(Token::Star) => Some(BinOp::Mul),
1810 Some(Token::Slash) => Some(BinOp::Div),
1811 _ => None,
1812 } {
1813 self.pos += 1;
1814 let right = self.parse_unary()?;
1815 left = Node::Bin(op, Box::new(left), Box::new(right));
1816 }
1817 Ok(left)
1818 }
1819
1820 fn parse_unary(&mut self) -> Result<Node, ExprError> {
1821 match self.peek() {
1822 Some(Token::Minus) => {
1823 self.pos += 1;
1824 Ok(Node::Neg(Box::new(self.parse_unary()?)))
1825 }
1826 Some(Token::Plus) => {
1827 self.pos += 1;
1828 self.parse_unary()
1829 }
1830 _ => self.parse_primary(),
1831 }
1832 }
1833
1834 fn parse_primary(&mut self) -> Result<Node, ExprError> {
1835 match self.advance() {
1836 Some(Token::Num(n)) => Ok(Node::Const(*n)),
1837 Some(Token::LParen) => {
1838 let inner = self.parse_expr()?;
1839 self.expect(&Token::RParen)?;
1840 Ok(inner)
1841 }
1842 Some(Token::Ident(name)) => {
1843 let name = name.clone();
1844 if matches!(self.peek(), Some(Token::LParen)) {
1845 self.parse_call(name)
1846 } else if let Some(c) = constant(&name) {
1847 // Checked before the variable lookup, so a constant can
1848 // never be shadowed by a future variable of the same name.
1849 Ok(Node::Const(c))
1850 } else if let Some(slot) = VAR_NAMES
1851 .iter()
1852 .position(|&v| v == name)
1853 // The reserved latch placeholders are storage, not grammar:
1854 // they are in `VAR_NAMES` so the positional assertion can
1855 // see them, and held out here so an author reaches a latch
1856 // only through the name they declared for it (ADR-0137
1857 // Alternative B is the readability this protects). The same
1858 // predicate narrows the exported roster, so a consumer is
1859 // never offered a name this lookup refuses.
1860 .filter(|slot| is_bindable_slot(*slot))
1861 {
1862 Ok(Node::Var(slot))
1863 } else if let Some(slot) = self
1864 .latches
1865 .iter()
1866 .take(LATCH_CAP)
1867 .position(|declared| *declared == name)
1868 {
1869 Ok(Node::Var(LATCH_SLOT_BASE + slot))
1870 } else {
1871 Err(ExprError::UnknownIdent(name))
1872 }
1873 }
1874 Some(other) => Err(ExprError::UnexpectedToken(other.describe())),
1875 None => Err(ExprError::UnexpectedEnd),
1876 }
1877 }
1878
1879 fn parse_call(&mut self, name: String) -> Result<Node, ExprError> {
1880 let func = Func::from_name(&name).ok_or(ExprError::UnknownIdent(name.clone()))?;
1881 self.expect(&Token::LParen)?;
1882 let mut args = Vec::new();
1883 if !matches!(self.peek(), Some(Token::RParen)) {
1884 loop {
1885 args.push(self.parse_expr()?);
1886 match self.peek() {
1887 Some(Token::Comma) => {
1888 self.pos += 1;
1889 }
1890 _ => break,
1891 }
1892 }
1893 }
1894 self.expect(&Token::RParen)?;
1895 if args.len() != func.arity() {
1896 return Err(ExprError::WrongArity {
1897 func: name,
1898 expected: func.arity(),
1899 got: args.len(),
1900 });
1901 }
1902 Ok(Node::Call(func, args.into_boxed_slice()))
1903 }
1904
1905 fn expect(&mut self, want: &Token) -> Result<(), ExprError> {
1906 match self.advance() {
1907 Some(tok) if tok == want => Ok(()),
1908 Some(other) => Err(ExprError::UnexpectedToken(other.describe())),
1909 None => Err(ExprError::UnexpectedEnd),
1910 }
1911 }
1912}
1913
1914#[cfg(test)]
1915mod tests {
1916 use super::*;
1917
1918 /// The slot-base constants are the one place this module trades a name for a
1919 /// number, so they get the assertion. Inline rather than in
1920 /// `core/tests/preset.rs` because both constants are private — and they
1921 /// should stay private, which makes this the only place the claim is
1922 /// checkable.
1923 ///
1924 /// Without it, inserting a variable before `bass_raw` would leave
1925 /// [`Variables::with_raw`] writing four floats into `novelty` and the three
1926 /// slots after it, quietly, with every existing test still green: the raw
1927 /// values would simply read as each other. The reserved `[latch]` block
1928 /// (ADR-0137) is held to the same claim for the same reason — it is the
1929 /// newest block and the one most likely to be moved.
1930 #[test]
1931 fn latch_slots_are_where_the_names_say() {
1932 assert_eq!(
1933 VAR_NAMES.get(RAW_SLOT_BASE..RAW_SLOT_BASE + 4),
1934 Some(["bass_raw", "mid_raw", "treb_raw", "onset_raw"].as_slice()),
1935 "with_raw writes four floats starting at RAW_SLOT_BASE; those are the names it must land on"
1936 );
1937 assert_eq!(
1938 VAR_NAMES.get(CLOCK_SLOT_BASE..CLOCK_SLOT_BASE + 2),
1939 Some(["beat_index", "time_since_beat"].as_slice()),
1940 "with_beat_clock writes two floats starting at CLOCK_SLOT_BASE"
1941 );
1942 assert_eq!(
1943 VAR_NAMES.get(BAR_SLOT_BASE..BAR_SLOT_BASE + 3),
1944 Some(["beat_in_bar", "bar_index", "bar_phase"].as_slice()),
1945 "with_bar writes three floats starting at BAR_SLOT_BASE"
1946 );
1947 // The gate's own confidence must NOT be bindable (ADR-0050).
1948 for hidden in ["downbeat_confidence", "confidence", "downbeat_locked"] {
1949 assert!(
1950 !VAR_NAMES.contains(&hidden),
1951 "`{hidden}` must stay out of the grammar: authors get behavior, not homework"
1952 );
1953 }
1954 assert_eq!(
1955 VAR_NAMES.get(VERTEX_SLOT_BASE..VERTEX_SLOT_BASE + 4),
1956 Some(["x", "y", "rad", "ang"].as_slice()),
1957 "with_vertex writes four floats starting at VERTEX_SLOT_BASE"
1958 );
1959 assert_eq!(
1960 VAR_NAMES.get(LATCH_SLOT_BASE..LATCH_SLOT_BASE + LATCH_CAP),
1961 Some(["_latch0", "_latch1", "_latch2", "_latch3"].as_slice()),
1962 "the latch bank writes LATCH_CAP floats starting at LATCH_SLOT_BASE"
1963 );
1964 assert_eq!(
1965 VAR_NAMES.get(INDEX_SLOT),
1966 Some(&"index"),
1967 "`index` must stay last: INDEX_SLOT is derived from the variable count"
1968 );
1969 // The blocks must not overlap either — see the `const` assertions beside
1970 // the constants themselves, which reject an overlap at compile time
1971 // rather than waiting for this test to run.
1972 }
1973
1974 /// `with_raw` fills exactly its own four slots — it must not disturb the
1975 /// headline levels it sits beside, which is the failure a copy_from_slice
1976 /// with a wrong base would produce.
1977 #[test]
1978 fn with_raw_touches_only_the_raw_slots() {
1979 let base = Variables::new(0.1, 0.2, 0.3, 0.4, 1.0, 0.5, 6.0, 120.0, 0.7);
1980 let with = base.with_raw(0.01, 0.02, 0.03, 0.04);
1981 assert_eq!(
1982 base.values.get(..RAW_SLOT_BASE),
1983 with.values.get(..RAW_SLOT_BASE),
1984 "the nine headline slots must be untouched"
1985 );
1986 assert_eq!(
1987 with.values.get(RAW_SLOT_BASE..RAW_SLOT_BASE + 4),
1988 Some([0.01f32, 0.02, 0.03, 0.04].as_slice())
1989 );
1990 assert_eq!(
1991 with.values.get(INDEX_SLOT),
1992 Some(&0.0),
1993 "`index` sits after the raw block and must not be clipped by it"
1994 );
1995 }
1996
1997 // -----------------------------------------------------------------------
1998 // Clamp occupancy (Plan 0056 Phase 1 / ADR-0062)
1999 // -----------------------------------------------------------------------
2000
2001 /// `bass` at each of `levels`, everything else zero.
2002 fn bass_at(level: f32) -> Variables<'static> {
2003 Variables::new(level, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
2004 }
2005
2006 /// Probe `src` over `levels` as `bass` and read back the root clamp's
2007 /// observation. Every expression here is a bare `clamp(...)`, so the root is
2008 /// node 0.
2009 fn probe_bass(src: &str, levels: &[f32]) -> NodeObservation {
2010 let e = compile(src).expect("compiles");
2011 let mut obs = Observations::new();
2012 for &level in levels {
2013 e.eval_probed(&bass_at(level), &mut obs);
2014 }
2015 obs.node(0)
2016 }
2017
2018 #[test]
2019 fn a_clamp_above_its_ceiling_on_every_hop_is_fully_occupied() {
2020 // The Plan 0048 Phase 7 defect in miniature: a gain written for raw
2021 // levels, met with normalized ones. The ceiling is reached at
2022 // `bass = 0.01875` and every level here is far above it.
2023 let obs = probe_bass("clamp(bass * 16, 0, 0.3)", &[0.2, 0.4, 0.6, 0.8, 1.0]);
2024 assert_eq!(obs.occupancy(), 1.0, "pinned on every hop: {obs:?}");
2025 // The peak is a different statistic and must still read the peak.
2026 match obs {
2027 NodeObservation::Clamp {
2028 peak_fraction_of_bound,
2029 hops,
2030 ..
2031 } => {
2032 assert_eq!(hops, 5, "one hop recorded per probed evaluation");
2033 let expected = 1.0 * 16.0 / 0.3;
2034 assert!(
2035 (peak_fraction_of_bound - expected).abs() < 1e-3,
2036 "peak should be {expected}, got {peak_fraction_of_bound}"
2037 );
2038 }
2039 other => panic!("expected a clamp observation, got {other:?}"),
2040 }
2041 }
2042
2043 #[test]
2044 fn a_clamp_that_never_reaches_its_ceiling_is_unoccupied() {
2045 // The mirror finding, and the one that already shipped: the bound is
2046 // decorative. Occupancy must read `0.0` and the peak must be unchanged
2047 // from what it read before occupancy existed.
2048 let obs = probe_bass("clamp(bass * 0.001, 0, 0.5)", &[0.2, 0.4, 0.6, 0.8, 1.0]);
2049 assert_eq!(obs.occupancy(), 0.0, "never at the bound: {obs:?}");
2050 match obs {
2051 NodeObservation::Clamp {
2052 peak_fraction_of_bound,
2053 hops_at_bound,
2054 hops,
2055 } => {
2056 assert_eq!(hops_at_bound, 0);
2057 assert_eq!(hops, 5);
2058 let expected = 1.0 * 0.001 / 0.5;
2059 assert!(
2060 (peak_fraction_of_bound - expected).abs() < 1e-6,
2061 "peak should be {expected}, got {peak_fraction_of_bound}"
2062 );
2063 }
2064 other => panic!("expected a clamp observation, got {other:?}"),
2065 }
2066 }
2067
2068 #[test]
2069 fn a_clamp_that_crosses_part_way_reports_the_crossing_fraction() {
2070 // The ceiling is reached at `bass = 0.65`, so exactly three of these ten
2071 // levels (0.7, 0.8, 0.9) pin it — the statistic is the crossing
2072 // fraction, not a boolean.
2073 let levels: Vec<f32> = (0..10).map(|i| i as f32 / 10.0).collect();
2074 let obs = probe_bass("clamp(bass, 0, 0.65)", &levels);
2075 assert!(
2076 (obs.occupancy() - 0.3).abs() < 1e-6,
2077 "three of ten levels sit at or above 0.65: {obs:?}"
2078 );
2079 }
2080
2081 #[test]
2082 fn a_clamp_evaluated_zero_times_reports_no_occupancy() {
2083 // Two ways to reach zero hops, and neither may divide by zero: a node
2084 // the run never touched at all, and a clamp sitting in a `select()`
2085 // branch the run never took.
2086 assert_eq!(NodeObservation::Untouched.occupancy(), 0.0);
2087
2088 let e = compile("select(bass > 0.5, clamp(bass * 99, 0, 0.1), 0)").expect("compiles");
2089 let mut obs = Observations::new();
2090 for level in [0.0, 0.1, 0.2] {
2091 e.eval_probed(&bass_at(level), &mut obs);
2092 }
2093 // Node 0 is the `select`, 1..=3 its condition, 4 the clamp.
2094 let clamp = obs
2095 .nodes()
2096 .iter()
2097 .find(|n| matches!(n, NodeObservation::Clamp { .. }));
2098 assert!(
2099 clamp.is_none(),
2100 "the `then` branch never ran, so its clamp recorded nothing: {clamp:?}"
2101 );
2102 assert!(
2103 e.flag_gates(&obs)
2104 .iter()
2105 .all(|f| !matches!(f.kind, GateKind::Saturated { .. })),
2106 "an unreached clamp makes no saturation claim"
2107 );
2108 }
2109
2110 #[test]
2111 fn an_unreadable_upper_bound_accuses_neither_way() {
2112 // A non-positive or non-finite bound means "fraction of the bound" is
2113 // undefined. The peak treats it as reached (so it does not claim the
2114 // ceiling was decorative); occupancy must treat it as *not* at bound
2115 // (so it does not claim the ceiling never released). Both stay silent.
2116 for src in ["clamp(bass, 0, 0)", "clamp(bass, 0, 0 - 1)"] {
2117 let e = compile(src).expect("compiles");
2118 let mut obs = Observations::new();
2119 for level in [0.0, 0.5, 1.0] {
2120 e.eval_probed(&bass_at(level), &mut obs);
2121 }
2122 assert_eq!(obs.node(0).occupancy(), 0.0, "`{src}` must not accuse");
2123 assert!(
2124 e.flag_gates(&obs).is_empty(),
2125 "`{src}` produced a finding on a bound it cannot read"
2126 );
2127 }
2128 }
2129
2130 #[test]
2131 fn saturation_is_flagged_only_past_the_threshold() {
2132 // The flag, not the statistic: a binding pinned on nearly every hop
2133 // reports, and one pinned on half of them does not.
2134 let pinned: Vec<f32> = (0..100).map(|i| 0.5 + i as f32 / 200.0).collect();
2135 let e = compile("clamp(bass * 16, 0, 0.3)").expect("compiles");
2136 let mut obs = Observations::new();
2137 for &level in &pinned {
2138 e.eval_probed(&bass_at(level), &mut obs);
2139 }
2140 match e.flag_gates(&obs).first().map(|f| f.kind) {
2141 Some(GateKind::Saturated { occupancy }) => assert!(
2142 occupancy >= SATURATED_OCCUPANCY,
2143 "flagged at {occupancy}, below the threshold"
2144 ),
2145 other => panic!("expected a saturation flag, got {other:?}"),
2146 }
2147
2148 // Half the hops at the bound: a binding that still varies, and must not
2149 // be convicted of being a constant.
2150 let half: Vec<f32> = (0..100)
2151 .map(|i| if i % 2 == 0 { 0.0 } else { 1.0 })
2152 .collect();
2153 let mut obs = Observations::new();
2154 for &level in &half {
2155 e.eval_probed(&bass_at(level), &mut obs);
2156 }
2157 assert!(
2158 (obs.node(0).occupancy() - 0.5).abs() < 1e-6,
2159 "half the hops pinned"
2160 );
2161 assert!(
2162 e.flag_gates(&obs).is_empty(),
2163 "half-occupancy is a live binding, not a saturated one"
2164 );
2165 }
2166
2167 /// **The published variable roster is exactly what the parser accepts**, in
2168 /// both directions, asked of the parser rather than of a second list.
2169 ///
2170 /// The trap this guards is the reserved `[latch]` block: those four names
2171 /// are in [`VAR_NAMES`] as storage and are held out of the identifier
2172 /// lookup, so a roster published straight from `VAR_NAMES` would offer an
2173 /// editor four spellings that do not compile.
2174 #[test]
2175 fn the_published_variable_roster_is_what_the_parser_accepts() {
2176 let published: Vec<&str> = variable_names().collect();
2177 for name in VAR_NAMES {
2178 let compiles = compile(name).is_ok();
2179 assert_eq!(
2180 published.contains(&name),
2181 compiles,
2182 "`{name}` is {} the published roster and {} as an expression",
2183 if published.contains(&name) {
2184 "in"
2185 } else {
2186 "absent from"
2187 },
2188 if compiles {
2189 "compiles"
2190 } else {
2191 "does not compile"
2192 }
2193 );
2194 }
2195 assert_eq!(
2196 published.len(),
2197 VAR_COUNT - LATCH_CAP,
2198 "the roster is VAR_NAMES without the reserved latch block"
2199 );
2200 }
2201
2202 /// **The function roster is the only source**: `from_name` resolves through
2203 /// it, `name` inverts it, and the published roster is it.
2204 ///
2205 /// A variant added to [`Func`] without an entry in [`FUNCS`] is constructed
2206 /// by nothing, so `dead_code` fails the build before this test runs — which
2207 /// is why nothing here hand-lists the variants.
2208 #[test]
2209 fn every_function_variant_is_in_the_roster() {
2210 for (spelling, func) in FUNCS {
2211 assert_eq!(
2212 Func::from_name(spelling),
2213 Some(func),
2214 "`{spelling}` is published and the parser does not resolve it"
2215 );
2216 assert_eq!(
2217 func.name(),
2218 spelling,
2219 "`{spelling}` does not print as the name it parses from, so a \
2220 round-tripped expression would change spelling"
2221 );
2222 }
2223 let published: Vec<&str> = function_names().collect();
2224 assert_eq!(published.len(), FUNCS.len());
2225 // Plausible names that are NOT functions here, so "the roster is what
2226 // the engine knows" is a claim with a failing case rather than a set
2227 // that happens to contain everything asked of it.
2228 for absent in ["tan", "log10", "atan2", "fract", "random", "step"] {
2229 assert_eq!(Func::from_name(absent), None, "`{absent}` resolved");
2230 assert!(
2231 !published.contains(&absent),
2232 "`{absent}` is published and the parser refuses it"
2233 );
2234 }
2235 }
2236
2237 /// The constant roster is likewise one table, and resolves before the
2238 /// variable lookup so nothing can shadow it.
2239 #[test]
2240 fn every_constant_resolves_by_its_published_name() {
2241 for name in constant_names() {
2242 let value = constant(name).unwrap_or_else(|| panic!("`{name}` resolves"));
2243 assert!(value.is_finite(), "`{name}` is {value}");
2244 assert!(
2245 compile(name).is_ok(),
2246 "`{name}` is published and does not compile"
2247 );
2248 assert!(
2249 !VAR_NAMES.contains(&name),
2250 "`{name}` is both a constant and a variable, and the constant \
2251 wins — so the variable is unreachable"
2252 );
2253 }
2254 for absent in ["e", "phi", "inf"] {
2255 assert_eq!(constant(absent), None, "`{absent}` resolved");
2256 }
2257 }
2258}