1use sva_engine::overload::{notation, signature};
4use sva_engine::{BUILTINS, Cast, Codomain, Held, MAX_WIDTH, REGISTRY, Ty, Var, recognized_named};
5use sva_formula::filter::{ALL_SHAPES, Shape};
6use sva_formula::{FAMILIES, TABLE_VERSION};
7
8pub struct Callable {
9 pub name: &'static str,
10 pub required: usize,
11 pub max_positional: usize,
12 pub named: &'static [&'static str],
13 pub required_named: &'static [&'static str],
16 pub takes_gain: Option<bool>,
17}
18
19pub const UNIT_SUFFIXES: [(&str, &str); 11] = [
21 ("khz", "kilohertz (x1000)"),
22 ("ms", "milliseconds (seconds x0.001)"),
23 ("sp", "samples"),
24 ("hz", "hertz"),
25 ("db", "decibels (log)"),
26 ("ct", "cents (log)"),
27 ("st", "semitones (log)"),
28 ("b", "bars, resolved against bpm/meter"),
29 ("s", "seconds"),
30 ("m", "minutes (seconds x60)"),
31 ("h", "hours (seconds x3600)"),
32];
33
34pub const NOTE_GRAMMAR: &str = "a bare identifier: a letter A-G, an optional accidental spelled \
35 s (sharp) or b (flat) -- never #, then a whole-number octave, e.g. A4, Cs4, Db4; A4 = 440 Hz, \
36 twelve-tone equal temperament, up to G9 where MIDI's 128 notes end";
37
38pub const RESERVED: [(&str, &str); 5] = [
40 ("t", "time in seconds"),
41 ("f", "frequency in hertz"),
42 ("i", "the imaginary unit"),
43 ("pi", "the constant pi"),
44 ("self", "a bounded self-reference, call-only: self(t - 1sp)"),
45];
46
47pub const SPECIAL_FORMS: [(&str, &str); 5] = [
49 (
50 "sum",
51 "sum(index, lo, hi, expr) -- index is a name the series binds, not a value; hi may be \
52 inf, which makes it a series",
53 ),
54 (
55 "crop",
56 "crop(x, start, end) -- windows x to [start, end) seconds, zero outside",
57 ),
58 (
59 "join",
60 "join(a, b, ...) -- builds one wide value from 2..=8 mono args",
61 ),
62 (
63 "ch",
64 "ch(x, index) -- extracts one component of a wide value by a literal index",
65 ),
66 (
67 "noise",
68 "noise(seed, period=, color=) -- one line per 1/period hertz, each of unit \
69 amplitude, so the series' own RMS is the square root of half its line count; scale \
70 it to the level the piece wants",
71 ),
72];
73
74pub const NOT_SUPPORTED: [&str; 4] = [
77 "comparison operators: < > <= >= == !=",
78 "boolean operators: && || !",
79 "conditionals: if/else, a ternary",
80 "exponentiation `^`: write pow(base, exponent)",
81];
82
83pub struct Crossing {
85 pub name: &'static str,
86 pub rows: Vec<(&'static str, &'static str)>,
87}
88
89pub struct Builtins {
90 pub callables: Vec<Callable>,
91 pub casts: Vec<Crossing>,
92 pub table_version: u64,
93 pub families: &'static [(&'static str, &'static str)],
94 pub refusals: &'static [(&'static str, &'static str)],
95 pub unit_suffixes: &'static [(&'static str, &'static str)],
96 pub note_names: &'static str,
97 pub reserved: &'static [(&'static str, &'static str)],
98 pub special_forms: &'static [(&'static str, &'static str)],
99 pub not_supported: &'static [&'static str],
100}
101
102fn plain_callable(name: &'static str) -> Callable {
103 let sig = signature(name)
104 .unwrap_or_else(|| unreachable!("{name} is in BUILTINS but SIGNATURES does not cover it"));
105 Callable {
106 name,
107 required: sig.required(),
108 max_positional: if sig.variadic {
109 MAX_WIDTH
110 } else {
111 sig.params.len()
112 },
113 named: sig.named(),
114 required_named: &[],
115 takes_gain: None,
116 }
117}
118
119fn filter_callable(shape: Shape) -> Callable {
122 let gain = shape.takes_gain();
123 let max_positional = if shape == Shape::OnePole {
124 2
125 } else if gain {
126 4
127 } else {
128 3
129 };
130 Callable {
131 name: shape.name(),
132 required: 2,
133 max_positional,
134 named: recognized_named(shape.name()).unwrap_or(&[]),
135 required_named: &[],
136 takes_gain: Some(gain),
137 }
138}
139
140const HELD: [(Held, bool); 6] = [
142 (Held::Form(Var::T), false),
143 (Held::Form(Var::T), true),
144 (Held::Form(Var::F), false),
145 (Held::Form(Var::F), true),
146 (Held::Sampled, false),
147 (Held::Frames, false),
148];
149
150fn crossings() -> Vec<Crossing> {
152 Cast::NAMES
153 .into_iter()
154 .map(|name| {
155 let cast = Cast::from_name(name, &[("window", 1024.0), ("hop", 256.0)])
156 .unwrap_or_else(|| unreachable!("{name} is one of Cast::NAMES"));
157 Crossing {
158 name,
159 rows: HELD
160 .into_iter()
161 .filter_map(|(held, dual)| {
162 let ty = Ty {
163 dual,
164 ..Ty::discrete(held, Codomain::Real)
165 };
166 cast.resolve(&[ty])
167 .ok()
168 .map(|out| (notation(ty), notation(out)))
169 })
170 .collect(),
171 }
172 })
173 .collect()
174}
175
176pub fn builtins() -> Builtins {
177 let mut callables: Vec<Callable> = BUILTINS.into_iter().map(plain_callable).collect();
178 callables.extend(ALL_SHAPES.into_iter().map(filter_callable));
179 Builtins {
180 callables,
181 casts: crossings(),
182 table_version: TABLE_VERSION,
183 families: &FAMILIES,
184 refusals: ®ISTRY,
185 unit_suffixes: &UNIT_SUFFIXES,
186 note_names: NOTE_GRAMMAR,
187 reserved: &RESERVED,
188 special_forms: &SPECIAL_FORMS,
189 not_supported: &NOT_SUPPORTED,
190 }
191}
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196
197 #[test]
198 fn the_note_grammar_this_dump_states_is_what_note_actually_resolves() {
199 for note in ["A4", "Cs4", "Db4"] {
200 assert!(
201 sva_formula::note::frequency(note).is_some(),
202 "{note} should be a note"
203 );
204 }
205 for not_a_note in ["H4", "Cs", "C99999999999"] {
206 assert!(
207 sva_formula::note::frequency(not_a_note).is_none(),
208 "{not_a_note} is not the grammar this dump states"
209 );
210 }
211 }
212
213 #[test]
214 fn every_operator_this_dump_says_does_not_exist_actually_refuses_to_parse() {
215 for bad in [
216 "1 < 2",
217 "1 > 2",
218 "1 <= 2",
219 "1 >= 2",
220 "1 == 2",
221 "1 != 2",
222 "1 && 1",
223 "1 || 1",
224 "!1",
225 "1 ? 2 : 3",
226 "2 ^ 3",
227 ] {
228 assert!(
229 sva_ast::parse_expr(bad).is_err(),
230 "`{bad}` parsed, so this dump's not_supported claim is stale"
231 );
232 }
233 }
234
235 #[test]
236 fn every_builtin_name_gets_exactly_one_callable_entry() {
237 let b = builtins();
238 assert_eq!(b.callables.len(), BUILTINS.len() + ALL_SHAPES.len());
239 for name in BUILTINS {
240 assert_eq!(
241 b.callables.iter().filter(|c| c.name == name).count(),
242 1,
243 "{name} should appear exactly once"
244 );
245 }
246 for shape in ALL_SHAPES {
247 let found = b
248 .callables
249 .iter()
250 .find(|c| c.name == shape.name())
251 .unwrap_or_else(|| panic!("{} is missing", shape.name()));
252 assert_eq!(found.takes_gain, Some(shape.takes_gain()));
253 }
254 }
255
256 #[test]
257 fn one_pole_alone_has_no_room_for_q_or_gain() {
258 let b = builtins();
259 let lp = b.callables.iter().find(|c| c.name == "lp").unwrap();
260 assert_eq!(lp.max_positional, 2);
261 let lowpass = b.callables.iter().find(|c| c.name == "lowpass").unwrap();
262 assert_eq!(lowpass.max_positional, 3);
263 let peaking = b.callables.iter().find(|c| c.name == "peaking").unwrap();
264 assert_eq!(peaking.max_positional, 4);
265 }
266
267 #[test]
268 fn each_reserved_name_still_behaves_the_way_this_dump_says_it_does() {
269 for (name, _) in RESERVED {
270 assert!(
271 sva_engine::instantiate::is_reserved(name),
272 "`{name}` is listed as reserved but a parameter may still take it"
273 );
274 if name == "self" {
275 continue;
276 }
277 assert!(
278 sva_ast::parse_expr(name).is_ok(),
279 "`{name}` should parse as the special name this dump claims"
280 );
281 }
282 assert!(
283 !sva_engine::instantiate::is_reserved("cutoff"),
284 "an ordinary parameter name must not be reserved, or the claim says nothing"
285 );
286 assert!(
287 sva_ast::parse_expr("self").is_err(),
288 "`self` alone must still require a call form"
289 );
290 assert!(
291 sva_ast::parse_expr("self(t - 1sp)").is_ok(),
292 "`self(...)` must still parse as a bounded self-reference"
293 );
294 }
295}