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}