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