1use sva_ast::BUILTINS;
4use sva_engine::overload::{notation, signature};
5use sva_engine::{
6 Cast, Codomain, Held, MAX_WIDTH, Meaning, REGISTRY, Ty, Var, meaning, named_may_move,
7 recognized_named, shape_name,
8};
9use sva_formula::filter::{ALL_SHAPES, Shape};
10use sva_formula::{FAMILIES, TABLE_VERSION};
11
12use crate::json::{NONE, escape, list, pair_list, strings};
13
14pub struct Callable {
15 pub name: &'static str,
16 pub required: usize,
17 pub max_positional: usize,
18 pub named: &'static [&'static str],
19 pub required_named: &'static [&'static str],
22 pub takes_gain: Option<bool>,
23 pub arguments: Vec<(&'static str, Meaning, bool)>,
25 pub positional: Vec<(&'static str, Option<Meaning>)>,
27}
28
29fn positional(builtin: &str) -> Vec<(&'static str, Option<Meaning>)> {
30 signature(builtin)
31 .map_or(&[][..], |s| s.params)
32 .iter()
33 .map(|p| (p.name, meaning(builtin, p.name)))
34 .collect()
35}
36
37fn meanings(builtin: &str, named: &'static [&'static str]) -> Vec<(&'static str, Meaning, bool)> {
38 named
39 .iter()
40 .map(|key| {
41 let held = meaning(builtin, key)
42 .unwrap_or_else(|| unreachable!("`{builtin}` takes `{key}` with no meaning"));
43 (*key, held, named_may_move(builtin, key))
44 })
45 .collect()
46}
47
48pub const UNIT_SUFFIXES: [(&str, &str); 11] = [
50 ("khz", "kilohertz (x1000)"),
51 ("ms", "milliseconds (seconds x0.001)"),
52 ("sp", "samples"),
53 ("hz", "hertz"),
54 ("db", "decibels (log)"),
55 ("ct", "cents (log)"),
56 ("st", "semitones (log)"),
57 ("b", "bars, resolved against bpm/meter"),
58 ("s", "seconds"),
59 ("m", "minutes (seconds x60)"),
60 ("h", "hours (seconds x3600)"),
61];
62
63pub const NOTE_GRAMMAR: &str = "a bare identifier: a letter A-G, an optional accidental spelled \
64 s (sharp) or b (flat) -- never #, then a whole-number octave, e.g. A4, Cs4, Db4; A4 = 440 Hz, \
65 twelve-tone equal temperament, up to G9 where MIDI's 128 notes end";
66
67pub const SPECIAL_FORMS: [(&str, &str); 7] = [
69 (
70 "sum",
71 "sum(index, lo, hi, expr) -- index is a name the series binds, not a value; a finite \
72 sum is its terms, whatever each reads, and one whose index a node's binding reads is \
73 written out one node per index between whole-number bounds; hi may be inf, which \
74 makes it a series of closed forms",
75 ),
76 (
77 "crop",
78 "crop(x, start, end) -- windows x to [start, end) seconds, zero outside",
79 ),
80 (
81 "join",
82 "join(a, b, ...) -- builds one wide value from 2..=8 mono args",
83 ),
84 (
85 "ch",
86 "ch(x, index) -- extracts one component of a wide value by a literal index",
87 ),
88 (
89 "x[i]",
90 "@x[i], self[i] -- the stored sample at integer index i, at the rate in use; @x(e) \
91 reads the instant e",
92 ),
93 (
94 "idx",
95 "idx(time), idx(time, floor), idx(time, ceil) -- the sample index nearest a time, \
96 ties to even, or the one below or above it; written only inside an index",
97 ),
98 (
99 "noise",
100 "noise(seed, period=, color=) -- one line per 1/period hertz, each of unit \
101 amplitude, so the series' own RMS is the square root of half its line count; scale \
102 it to the level the piece wants",
103 ),
104];
105
106pub const NOT_SUPPORTED: [&str; 4] = [
109 "comparison operators: < > <= >= == !=",
110 "boolean operators: && || !",
111 "conditionals: if/else, a ternary",
112 "exponentiation `^`: write pow(base, exponent)",
113];
114
115pub struct Crossing {
117 pub name: &'static str,
118 pub rows: Vec<(&'static str, &'static str)>,
119}
120
121pub struct Builtins {
122 pub callables: Vec<Callable>,
123 pub casts: Vec<Crossing>,
124 pub table_version: u64,
125 pub families: &'static [(&'static str, &'static str)],
126 pub refusals: &'static [(&'static str, &'static str)],
127 pub unit_suffixes: &'static [(&'static str, &'static str)],
128 pub note_names: &'static str,
129 pub reserved: &'static [(&'static str, &'static str)],
130 pub special_forms: &'static [(&'static str, &'static str)],
131 pub not_supported: &'static [&'static str],
132}
133
134fn plain_callable(name: &'static str) -> Callable {
135 let sig = signature(name)
136 .unwrap_or_else(|| unreachable!("{name} is in BUILTINS but SIGNATURES does not cover it"));
137 Callable {
138 name,
139 required: sig.required(),
140 max_positional: if sig.variadic {
141 MAX_WIDTH
142 } else {
143 sig.params.len()
144 },
145 named: sig.named(),
146 required_named: &[],
147 takes_gain: None,
148 arguments: meanings(name, sig.named()),
149 positional: positional(name),
150 }
151}
152
153fn filter_callable(shape: Shape) -> Callable {
156 let gain = shape.takes_gain();
157 let max_positional = if shape == Shape::OnePole {
158 2
159 } else if gain {
160 4
161 } else {
162 3
163 };
164 let named = recognized_named(shape_name(shape)).unwrap_or(&[]);
165 Callable {
166 name: shape_name(shape),
167 required: 2,
168 max_positional,
169 named,
170 required_named: &[],
171 takes_gain: Some(gain),
172 arguments: meanings(shape_name(shape), named),
173 positional: positional(shape_name(shape)),
174 }
175}
176
177const HELD: [(Held, bool); 6] = [
179 (Held::Form(Var::T), false),
180 (Held::Form(Var::T), true),
181 (Held::Form(Var::F), false),
182 (Held::Form(Var::F), true),
183 (Held::Sampled, false),
184 (Held::Frames, false),
185];
186
187fn crossings() -> Vec<Crossing> {
189 Cast::NAMES
190 .into_iter()
191 .map(|name| {
192 let cast = match Cast::from_name(name) {
193 Some(Cast::Stft { .. }) => Cast::Stft {
194 window: 1024,
195 hop: 256,
196 },
197 Some(cast) => cast,
198 None => unreachable!("{name} is one of Cast::NAMES"),
199 };
200 Crossing {
201 name,
202 rows: HELD
203 .into_iter()
204 .filter_map(|(held, dual)| {
205 let ty = Ty {
206 dual,
207 ..Ty::discrete(held, Codomain::Real)
208 };
209 cast.resolve(&[ty])
210 .ok()
211 .map(|out| (notation(ty), notation(out)))
212 })
213 .collect(),
214 }
215 })
216 .collect()
217}
218
219pub fn builtins() -> Builtins {
220 let mut callables: Vec<Callable> = BUILTINS.into_iter().map(plain_callable).collect();
221 callables.extend(ALL_SHAPES.into_iter().map(filter_callable));
222 Builtins {
223 callables,
224 casts: crossings(),
225 table_version: TABLE_VERSION,
226 families: &FAMILIES,
227 refusals: ®ISTRY,
228 unit_suffixes: &UNIT_SUFFIXES,
229 note_names: NOTE_GRAMMAR,
230 reserved: &sva_ast::RESERVED,
231 special_forms: &SPECIAL_FORMS,
232 not_supported: &NOT_SUPPORTED,
233 }
234}
235
236pub fn builtins_data(b: &Builtins) -> String {
237 let callables = list(&b.callables, |c| {
238 format!(
239 "\n {{ \"name\": \"{}\", \"required\": {}, \"max_positional\": {}, \"named\": {}, \
240 \"required_named\": {}, \"takes_gain\": {}, \"arguments\": {}, \"positional\": {} }}",
241 escape(c.name),
242 c.required,
243 c.max_positional,
244 strings(c.named),
245 strings(c.required_named),
246 c.takes_gain
247 .map_or_else(|| NONE.to_string(), |g| g.to_string()),
248 list(&c.arguments, argument_json),
249 list(&c.positional, positional_json)
250 )
251 });
252 let casts = list(&b.casts, |c| {
253 format!(
254 "\n {{ \"name\": \"{}\", \"crossings\": {} }}",
255 escape(c.name),
256 list(&c.rows, |(from, to)| format!(
257 "{{ \"from\": \"{from}\", \"to\": \"{to}\" }}"
258 ))
259 )
260 });
261 format!(
262 "{{\n \"callables\": {callables},\n \"casts\": {casts},\n \
263 \"rule_table\": {{ \"version\": {}, \"families\": {} }},\n \
264 \"refusals\": {},\n \"unit_suffixes\": {},\n \"note_names\": \"{}\",\n \
265 \"reserved\": {},\n \"special_forms\": {},\n \
266 \"not_supported\": {}\n}}",
267 b.table_version,
268 pair_list(b.families, "name", "duals"),
269 pair_list(b.refusals, "code", "when"),
270 pair_list(b.unit_suffixes, "suffix", "meaning"),
271 escape(b.note_names),
272 pair_list(b.reserved, "name", "note"),
273 pair_list(b.special_forms, "name", "shape"),
274 strings(b.not_supported)
275 )
276}
277
278fn positional_json((name, m): &(&str, Option<Meaning>)) -> String {
279 let text =
280 |t: Option<&str>| t.map_or_else(|| NONE.to_string(), |t| format!("\"{}\"", escape(t)));
281 format!(
282 "{{ \"name\": \"{}\", \"meaning\": {}, \"unit\": {}, \"part\": {} }}",
283 escape(name),
284 text(m.map(|m| m.text)),
285 text(m.map(|m| m.unit)),
286 text(m.and_then(|m| m.part))
287 )
288}
289
290fn argument_json((name, m, moves): &(&str, Meaning, bool)) -> String {
291 format!(
292 "{{ \"name\": \"{}\", \"meaning\": \"{}\", \"unit\": \"{}\", \"part\": {}, \
293 \"moves\": {moves} }}",
294 escape(name),
295 escape(m.text),
296 escape(m.unit),
297 m.part
298 .map_or_else(|| NONE.to_string(), |p| format!("\"{}\"", escape(p)))
299 )
300}
301
302#[cfg(test)]
303mod tests {
304 use super::*;
305
306 #[test]
307 fn the_note_grammar_this_dump_states_is_what_note_actually_resolves() {
308 for note in ["A4", "Cs4", "Db4"] {
309 assert!(
310 sva_ast::note_midi(note).is_some(),
311 "{note} should be a note"
312 );
313 }
314 for not_a_note in ["H4", "Cs", "C99999999999"] {
315 assert!(
316 sva_ast::note_midi(not_a_note).is_none(),
317 "{not_a_note} is not the grammar this dump states"
318 );
319 }
320 }
321
322 #[test]
323 fn every_operator_this_dump_says_does_not_exist_actually_refuses_to_parse() {
324 for bad in [
325 "1 < 2",
326 "1 > 2",
327 "1 <= 2",
328 "1 >= 2",
329 "1 == 2",
330 "1 != 2",
331 "1 && 1",
332 "1 || 1",
333 "!1",
334 "1 ? 2 : 3",
335 "2 ^ 3",
336 ] {
337 assert!(
338 sva_ast::parse_expr(bad).is_err(),
339 "`{bad}` parsed, so this dump's not_supported claim is stale"
340 );
341 }
342 }
343
344 #[test]
345 fn every_builtin_name_gets_exactly_one_callable_entry() {
346 let b = builtins();
347 assert_eq!(b.callables.len(), BUILTINS.len() + ALL_SHAPES.len());
348 for name in BUILTINS {
349 assert_eq!(
350 b.callables.iter().filter(|c| c.name == name).count(),
351 1,
352 "{name} should appear exactly once"
353 );
354 }
355 for shape in ALL_SHAPES {
356 let found = b
357 .callables
358 .iter()
359 .find(|c| c.name == shape_name(shape))
360 .unwrap_or_else(|| panic!("{} is missing", shape_name(shape)));
361 assert_eq!(found.takes_gain, Some(shape.takes_gain()));
362 }
363 }
364
365 #[test]
368 fn every_named_argument_says_what_it_means_and_in_what_unit() {
369 for c in builtins().callables {
370 let named: Vec<&str> = c.arguments.iter().map(|(k, ..)| *k).collect();
371 assert_eq!(named, c.named, "{}", c.name);
372 for (key, m, _) in &c.arguments {
373 assert!(!m.text.is_empty() && !m.unit.is_empty(), "{}.{key}", c.name);
374 }
375 }
376 let b = builtins();
377 let solver = b
378 .callables
379 .iter()
380 .find(|c| c.name == "chaigne_askenfelt")
381 .expect("the solver");
382 let (_, stiffness, moves) = solver.arguments[0];
383 assert!(!moves, "a solver's stiffness is one number");
384 let bore = b.callables.iter().find(|c| c.name == "darabundit_scavone");
385 let (name, held) = bore.expect("the bore").positional[0];
386 let held = held.expect("the bore states its positional");
387 assert_eq!(
388 (name, held.unit),
389 ("length", "m"),
390 "the bore reads a length"
391 );
392 let crop = b.callables.iter().find(|c| c.name == "crop").expect("crop");
393 let bounds: Vec<(&str, Option<&str>)> = crop.positional[1..]
394 .iter()
395 .map(|(name, m)| (*name, m.map(|m| m.unit)))
396 .collect();
397 assert_eq!(
398 bounds,
399 [("start", Some("s")), ("end", Some("s"))],
400 "a window's bounds"
401 );
402 let lowpass = b
403 .callables
404 .iter()
405 .find(|c| c.name == "lowpass")
406 .expect("a filter");
407 assert!(
408 lowpass.arguments.iter().all(|(_, _, moves)| *moves),
409 "a cutoff may sweep"
410 );
411 assert_eq!(
412 (stiffness.text, stiffness.unit, stiffness.part),
413 ("string stiffness (inharmonicity)", "none", Some("string"))
414 );
415 }
416
417 #[test]
418 fn one_pole_alone_has_no_room_for_q_or_gain() {
419 let b = builtins();
420 let lp = b.callables.iter().find(|c| c.name == "lp").unwrap();
421 assert_eq!(lp.max_positional, 2);
422 let lowpass = b.callables.iter().find(|c| c.name == "lowpass").unwrap();
423 assert_eq!(lowpass.max_positional, 3);
424 let peaking = b.callables.iter().find(|c| c.name == "peaking").unwrap();
425 assert_eq!(peaking.max_positional, 4);
426 }
427
428 #[test]
429 fn each_reserved_name_still_behaves_the_way_this_dump_says_it_does() {
430 for (name, _) in sva_ast::RESERVED {
431 assert!(
432 sva_ast::is_reserved(name),
433 "`{name}` is listed as reserved but a parameter may still take it"
434 );
435 if name == "self" {
436 continue;
437 }
438 assert!(
439 sva_ast::parse_expr(name).is_ok(),
440 "`{name}` should parse as the special name this dump claims"
441 );
442 }
443 assert!(
444 !sva_ast::is_reserved("cutoff"),
445 "an ordinary parameter name must not be reserved, or the claim says nothing"
446 );
447 assert!(
448 sva_ast::parse_expr("self").is_err(),
449 "`self` alone must still require a call form"
450 );
451 assert!(
452 sva_ast::parse_expr("self(t - 1sp)").is_ok(),
453 "`self(...)` must still parse as a bounded self-reference"
454 );
455 }
456}