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, FOURIER_DUAL_RULES_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); 8] = [
69 (
70 "deg",
71 "deg(n) -- the tonic in variables/key times the ratio of scale degree n, a whole \
72 number folded from literals, counted from 1 through the scale variables/scale \
73 names, a built-in as `minor` or a scale file by ref; deg(7)*1st is a raised seventh, deg(5)/2 the fifth an \
74 octave down, and each follows every edit of the key or its scale",
75 ),
76 (
77 "sum",
78 "sum(index, lo, hi, expr) -- index is a name the series binds, not a value; a finite \
79 sum is its terms, whatever each reads, and one whose index a node's binding reads is \
80 written out one node per index between whole-number bounds; hi may be inf, which \
81 makes it a series of closed forms",
82 ),
83 (
84 "crop",
85 "crop(x, start, end) -- windows x to [start, end) seconds, zero outside",
86 ),
87 (
88 "join",
89 "join(a, b, ...) -- builds one wide value from 2..=8 mono args",
90 ),
91 (
92 "ch",
93 "ch(x, index) -- extracts one component of a wide value by a literal index",
94 ),
95 (
96 "x[i]",
97 "@x[i], self[i] -- the stored sample at integer index i, at the rate in use; @x(e) \
98 reads the instant e",
99 ),
100 (
101 "idx",
102 "idx(time), idx(time, floor), idx(time, ceil) -- the sample index nearest a time, \
103 ties to even, or the one below or above it; written only inside an index",
104 ),
105 (
106 "noise",
107 "noise(seed, period=, color=) -- one line per 1/period hertz, each of unit \
108 amplitude, so the series' own RMS is the square root of half its line count; scale \
109 it to the level the piece wants",
110 ),
111];
112
113pub const NOT_SUPPORTED: [&str; 4] = [
116 "comparison operators: < > <= >= == !=",
117 "boolean operators: && || !",
118 "conditionals: if/else, a ternary",
119 "exponentiation `^`: write pow(base, exponent)",
120];
121
122pub struct Crossing {
124 pub name: &'static str,
125 pub rows: Vec<(&'static str, &'static str)>,
126}
127
128pub struct Builtins {
129 pub callables: Vec<Callable>,
130 pub casts: Vec<Crossing>,
131 pub fourier_dual_rules_version: u64,
132 pub families: &'static [(&'static str, &'static str)],
133 pub refusals: &'static [(&'static str, &'static str)],
134 pub unit_suffixes: &'static [(&'static str, &'static str)],
135 pub note_names: &'static str,
136 pub scales: &'static [(&'static str, &'static [f64], &'static str)],
137 pub reserved: &'static [(&'static str, &'static str)],
138 pub special_forms: &'static [(&'static str, &'static str)],
139 pub not_supported: &'static [&'static str],
140}
141
142fn plain_callable(name: &'static str) -> Callable {
143 let sig = signature(name)
144 .unwrap_or_else(|| unreachable!("{name} is in BUILTINS but SIGNATURES does not cover it"));
145 Callable {
146 name,
147 required: sig.required(),
148 max_positional: if sig.variadic {
149 MAX_WIDTH
150 } else {
151 sig.params.len()
152 },
153 named: sig.named(),
154 required_named: &[],
155 takes_gain: None,
156 arguments: meanings(name, sig.named()),
157 positional: positional(name),
158 }
159}
160
161fn filter_callable(shape: Shape) -> Callable {
164 let gain = shape.takes_gain();
165 let max_positional = if shape == Shape::OnePole {
166 2
167 } else if gain {
168 4
169 } else {
170 3
171 };
172 let named = recognized_named(shape_name(shape)).unwrap_or(&[]);
173 Callable {
174 name: shape_name(shape),
175 required: 2,
176 max_positional,
177 named,
178 required_named: &[],
179 takes_gain: Some(gain),
180 arguments: meanings(shape_name(shape), named),
181 positional: positional(shape_name(shape)),
182 }
183}
184
185const HELD: [(Held, bool); 6] = [
187 (Held::Form(Var::T), false),
188 (Held::Form(Var::T), true),
189 (Held::Form(Var::F), false),
190 (Held::Form(Var::F), true),
191 (Held::Sampled, false),
192 (Held::Frames, false),
193];
194
195fn crossings() -> Vec<Crossing> {
197 Cast::NAMES
198 .into_iter()
199 .map(|name| {
200 let cast = match Cast::from_name(name) {
201 Some(Cast::Stft { .. }) => Cast::Stft {
202 window: 1024,
203 hop: 256,
204 },
205 Some(cast) => cast,
206 None => unreachable!("{name} is one of Cast::NAMES"),
207 };
208 Crossing {
209 name,
210 rows: HELD
211 .into_iter()
212 .filter_map(|(held, dual)| {
213 let ty = Ty {
214 dual,
215 ..Ty::discrete(held, Codomain::Real)
216 };
217 cast.resolve(&[ty])
218 .ok()
219 .map(|out| (notation(ty), notation(out)))
220 })
221 .collect(),
222 }
223 })
224 .collect()
225}
226
227pub fn builtins() -> Builtins {
228 let mut callables: Vec<Callable> = BUILTINS.into_iter().map(plain_callable).collect();
229 callables.extend(ALL_SHAPES.into_iter().map(filter_callable));
230 Builtins {
231 callables,
232 casts: crossings(),
233 fourier_dual_rules_version: FOURIER_DUAL_RULES_VERSION,
234 families: &FAMILIES,
235 refusals: ®ISTRY,
236 unit_suffixes: &UNIT_SUFFIXES,
237 note_names: NOTE_GRAMMAR,
238 scales: &sva_ast::BUILTIN_SCALES,
239 reserved: &sva_ast::RESERVED,
240 special_forms: &SPECIAL_FORMS,
241 not_supported: &NOT_SUPPORTED,
242 }
243}
244
245pub fn builtins_data(b: &Builtins) -> String {
246 let callables = list(&b.callables, |c| {
247 format!(
248 "\n {{ \"name\": \"{}\", \"required\": {}, \"max_positional\": {}, \"named\": {}, \
249 \"required_named\": {}, \"takes_gain\": {}, \"arguments\": {}, \"positional\": {} }}",
250 escape(c.name),
251 c.required,
252 c.max_positional,
253 strings(c.named),
254 strings(c.required_named),
255 c.takes_gain
256 .map_or_else(|| NONE.to_string(), |g| g.to_string()),
257 list(&c.arguments, argument_json),
258 list(&c.positional, positional_json)
259 )
260 });
261 let casts = list(&b.casts, |c| {
262 format!(
263 "\n {{ \"name\": \"{}\", \"crossings\": {} }}",
264 escape(c.name),
265 list(&c.rows, |(from, to)| format!(
266 "{{ \"from\": \"{from}\", \"to\": \"{to}\" }}"
267 ))
268 )
269 });
270 format!(
271 "{{\n \"callables\": {callables},\n \"casts\": {casts},\n \
272 \"rule_table\": {{ \"version\": {}, \"families\": {} }},\n \
273 \"refusals\": {},\n \"unit_suffixes\": {},\n \"note_names\": \"{}\",\n \
274 \"scales\": {},\n \"reserved\": {},\n \"special_forms\": {},\n \
275 \"not_supported\": {}\n}}",
276 b.fourier_dual_rules_version,
277 pair_list(b.families, "name", "duals"),
278 pair_list(b.refusals, "code", "when"),
279 pair_list(b.unit_suffixes, "suffix", "meaning"),
280 escape(b.note_names),
281 list(b.scales, scale_json),
282 pair_list(b.reserved, "name", "note"),
283 pair_list(b.special_forms, "name", "shape"),
284 strings(b.not_supported)
285 )
286}
287
288fn scale_json((name, cents, tuning): &(&str, &[f64], &str)) -> String {
289 let cents: Vec<String> = cents.iter().map(f64::to_string).collect();
290 format!(
291 "\n {{ \"name\": \"{}\", \"cents\": [{}], \"tuning\": \"{}\" }}",
292 escape(name),
293 cents.join(", "),
294 escape(tuning)
295 )
296}
297
298fn positional_json((name, m): &(&str, Option<Meaning>)) -> String {
299 let text =
300 |t: Option<&str>| t.map_or_else(|| NONE.to_string(), |t| format!("\"{}\"", escape(t)));
301 format!(
302 "{{ \"name\": \"{}\", \"meaning\": {}, \"unit\": {}, \"part\": {} }}",
303 escape(name),
304 text(m.map(|m| m.text)),
305 text(m.map(|m| m.unit)),
306 text(m.and_then(|m| m.part))
307 )
308}
309
310fn argument_json((name, m, moves): &(&str, Meaning, bool)) -> String {
311 format!(
312 "{{ \"name\": \"{}\", \"meaning\": \"{}\", \"unit\": \"{}\", \"part\": {}, \
313 \"moves\": {moves} }}",
314 escape(name),
315 escape(m.text),
316 escape(m.unit),
317 m.part
318 .map_or_else(|| NONE.to_string(), |p| format!("\"{}\"", escape(p)))
319 )
320}
321
322#[cfg(test)]
323mod tests {
324 use super::*;
325
326 #[test]
327 fn the_note_grammar_this_dump_states_is_what_note_actually_resolves() {
328 for note in ["A4", "Cs4", "Db4"] {
329 assert!(
330 sva_ast::note_midi(note).is_some(),
331 "{note} should be a note"
332 );
333 }
334 for not_a_note in ["H4", "Cs", "C99999999999"] {
335 assert!(
336 sva_ast::note_midi(not_a_note).is_none(),
337 "{not_a_note} is not the grammar this dump states"
338 );
339 }
340 }
341
342 #[test]
343 fn every_operator_this_dump_says_does_not_exist_actually_refuses_to_parse() {
344 for bad in [
345 "1 < 2",
346 "1 > 2",
347 "1 <= 2",
348 "1 >= 2",
349 "1 == 2",
350 "1 != 2",
351 "1 && 1",
352 "1 || 1",
353 "!1",
354 "1 ? 2 : 3",
355 "2 ^ 3",
356 ] {
357 assert!(
358 sva_ast::parse_expr(bad).is_err(),
359 "`{bad}` parsed, so this dump's not_supported claim is stale"
360 );
361 }
362 }
363
364 #[test]
365 fn every_builtin_name_gets_exactly_one_callable_entry() {
366 let b = builtins();
367 assert_eq!(b.callables.len(), BUILTINS.len() + ALL_SHAPES.len());
368 for name in BUILTINS {
369 assert_eq!(
370 b.callables.iter().filter(|c| c.name == name).count(),
371 1,
372 "{name} should appear exactly once"
373 );
374 }
375 for shape in ALL_SHAPES {
376 let found = b
377 .callables
378 .iter()
379 .find(|c| c.name == shape_name(shape))
380 .unwrap_or_else(|| panic!("{} is missing", shape_name(shape)));
381 assert_eq!(found.takes_gain, Some(shape.takes_gain()));
382 }
383 }
384
385 #[test]
388 fn every_named_argument_says_what_it_means_and_in_what_unit() {
389 for c in builtins().callables {
390 let named: Vec<&str> = c.arguments.iter().map(|(k, ..)| *k).collect();
391 assert_eq!(named, c.named, "{}", c.name);
392 for (key, m, _) in &c.arguments {
393 assert!(!m.text.is_empty() && !m.unit.is_empty(), "{}.{key}", c.name);
394 }
395 }
396 let b = builtins();
397 let solver = b
398 .callables
399 .iter()
400 .find(|c| c.name == "chaigne_askenfelt")
401 .expect("the solver");
402 let (_, stiffness, moves) = solver.arguments[0];
403 assert!(!moves, "a solver's stiffness is one number");
404 let bore = b.callables.iter().find(|c| c.name == "darabundit_scavone");
405 let (name, held) = bore.expect("the bore").positional[0];
406 let held = held.expect("the bore states its positional");
407 assert_eq!(
408 (name, held.unit),
409 ("length", "m"),
410 "the bore reads a length"
411 );
412 let crop = b.callables.iter().find(|c| c.name == "crop").expect("crop");
413 let bounds: Vec<(&str, Option<&str>)> = crop.positional[1..]
414 .iter()
415 .map(|(name, m)| (*name, m.map(|m| m.unit)))
416 .collect();
417 assert_eq!(
418 bounds,
419 [("start", Some("s")), ("end", Some("s"))],
420 "a window's bounds"
421 );
422 let lowpass = b
423 .callables
424 .iter()
425 .find(|c| c.name == "lowpass")
426 .expect("a filter");
427 assert!(
428 lowpass.arguments.iter().all(|(_, _, moves)| *moves),
429 "a cutoff may sweep"
430 );
431 assert_eq!(
432 (stiffness.text, stiffness.unit, stiffness.part),
433 ("string stiffness (inharmonicity)", "none", Some("string"))
434 );
435 }
436
437 #[test]
438 fn every_built_in_scale_is_listed_with_its_cents() {
439 let listed = builtins_data(&builtins());
440 assert!(
441 listed.contains(
442 "{ \"name\": \"dorian\", \"cents\": [200, 300, 500, 700, 900, 1000, 1200]"
443 ),
444 "{listed}"
445 );
446 for (name, ..) in sva_ast::BUILTIN_SCALES {
447 assert!(listed.contains(&format!("\"name\": \"{name}\"")), "{name}");
448 }
449 }
450
451 #[test]
452 fn one_pole_alone_has_no_room_for_q_or_gain() {
453 let b = builtins();
454 let lp = b.callables.iter().find(|c| c.name == "lp").unwrap();
455 assert_eq!(lp.max_positional, 2);
456 let lowpass = b.callables.iter().find(|c| c.name == "lowpass").unwrap();
457 assert_eq!(lowpass.max_positional, 3);
458 let peaking = b.callables.iter().find(|c| c.name == "peaking").unwrap();
459 assert_eq!(peaking.max_positional, 4);
460 }
461
462 #[test]
463 fn each_reserved_name_still_behaves_the_way_this_dump_says_it_does() {
464 for (name, _) in sva_ast::RESERVED {
465 assert!(
466 sva_ast::is_reserved(name),
467 "`{name}` is listed as reserved but a parameter may still take it"
468 );
469 if name == "self" {
470 continue;
471 }
472 assert!(
473 sva_ast::parse_expr(name).is_ok(),
474 "`{name}` should parse as the special name this dump claims"
475 );
476 }
477 assert!(
478 !sva_ast::is_reserved("cutoff"),
479 "an ordinary parameter name must not be reserved, or the claim says nothing"
480 );
481 assert!(
482 sva_ast::parse_expr("self").is_err(),
483 "`self` alone must still require a call form"
484 );
485 assert!(
486 sva_ast::parse_expr("self(t - 1sp)").is_ok(),
487 "`self(...)` must still parse as a bounded self-reference"
488 );
489 }
490}