Skip to main content

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}