1use sva_ast::BUILTINS;
4use sva_engine::overload::{notation, signature};
5use sva_engine::{
6 Cast, Codomain, Held, Meaning, REGISTRY, Ty, Var, meaning, named_may_move, recognized_named,
7 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: Option<usize>,
19 pub named: &'static [&'static str],
20 pub required_named: &'static [&'static str],
23 pub takes_gain: Option<bool>,
24 pub arguments: Vec<(&'static str, Meaning, bool)>,
26 pub positional: Vec<(&'static str, Option<Meaning>)>,
28}
29
30fn positional(builtin: &str) -> Vec<(&'static str, Option<Meaning>)> {
31 signature(builtin)
32 .map_or(&[][..], |s| s.params)
33 .iter()
34 .map(|p| (p.name, meaning(builtin, p.name)))
35 .collect()
36}
37
38fn meanings(builtin: &str, named: &'static [&'static str]) -> Vec<(&'static str, Meaning, bool)> {
39 named
40 .iter()
41 .map(|key| {
42 let held = meaning(builtin, key)
43 .unwrap_or_else(|| unreachable!("`{builtin}` takes `{key}` with no meaning"));
44 (*key, held, named_may_move(builtin, key))
45 })
46 .collect()
47}
48
49pub const UNIT_SUFFIXES: [(&str, &str); 11] = [
51 ("khz", "kilohertz (x1000)"),
52 ("ms", "milliseconds (seconds x0.001)"),
53 ("sp", "samples"),
54 ("hz", "hertz"),
55 ("db", "decibels (log)"),
56 ("ct", "cents (log)"),
57 ("st", "semitones (log)"),
58 ("b", "bars, resolved against bpm/meter"),
59 ("s", "seconds"),
60 ("m", "minutes (seconds x60)"),
61 ("h", "hours (seconds x3600)"),
62];
63
64pub const NOTE_GRAMMAR: &str = "a bare identifier: a letter A-G, an optional accidental spelled \
65 s (sharp) or b (flat) -- never #, then a whole-number octave, e.g. A4, Cs4, Db4; A4 = 440 Hz, \
66 twelve-tone equal temperament, up to G9 where MIDI's 128 notes end";
67
68pub const SPECIAL_FORMS: [(&str, &str); 10] = [
70 (
71 "deg",
72 "deg(n) -- the tonic in variables/key times the ratio of scale degree n, a whole \
73 number folded from literals, counted from 1 through the scale variables/scale \
74 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 \
75 octave down, and each follows every edit of the key or its scale",
76 ),
77 (
78 "sum",
79 "sum(index, lo, hi, expr) -- index is a name the series binds, not a value; a finite \
80 sum is its terms, whatever each reads, and one whose index a node's binding reads is \
81 written out one node per index between whole-number bounds; hi may be inf, which \
82 makes it a series of closed forms",
83 ),
84 (
85 "crop",
86 "crop(x, start, end) -- windows x to [start, end) seconds, zero outside",
87 ),
88 (
89 "join",
90 "join(a, b, ...) -- builds one wide value from 2..=8 mono args",
91 ),
92 (
93 "pwl",
94 "pwl(x, t0, v0, t1, v1, ...) -- the straight lines through the knots (tk, vk), read \
95 at the time x: zero before t0 and from the last knot on, two knots at one time a jump \
96 read from its right. Each time is a constant with a unit, never falling, and each \
97 value a constant, so it is linear in its values; any number of knots, found by \
98 bisection or by stepping forward sample to sample, so its length costs nothing per \
99 sample, and a knot table counts against no body's size bound. Exact at every \
100 instant; its support is [t0, tn) and its bound from an instant on the largest |vk| \
101 from there. It holds no atoms: `lines`, `atoms`, `spectrum`, `envelope`, `pitch` \
102 and `derivative` of a form holding one refuse as cast.left_algebra, and read off \
103 sample(...) they are measured",
104 ),
105 (
106 "pwl_integral",
107 "pwl_integral(x, t0, v0, ...) -- the integral of the same pwl from t0 to x, exact: \
108 zero before t0, a parabola on each segment, the whole area from the last knot on. \
109 Its knots in hertz make a phase in cycles: cos(2*pi*pwl_integral(t, 0s, 440hz, 1s, \
110 880hz)) glides an octave in one second; its bound from an instant on is the largest \
111 it reaches from there, and it holds no atoms either",
112 ),
113 (
114 "ch",
115 "ch(x, index) -- extracts one component of a wide value by a literal index",
116 ),
117 (
118 "x[i]",
119 "@x[i], self[i] -- the stored sample at integer index i, at the rate in use; @x(e) \
120 reads the instant e",
121 ),
122 (
123 "idx",
124 "idx(time), idx(time, floor), idx(time, ceil) -- the sample index nearest a time, \
125 ties to even, or the one below or above it; written only inside an index",
126 ),
127 (
128 "noise",
129 "noise(seed, period=, color=) -- a line every 1/period hertz from 1/period up, each \
130 at a phase its seed draws; its level is a density in hertz that the period never \
131 moves: the line at f Hz has amplitude f^log2(color)/sqrt(period), so the lines hold \
132 power f^(2*log2(color))/2 a hertz, a flat noise B/2 over any B Hz, an RMS of \
133 sqrt(B/2), 100 under a 20 kHz ceiling; scale it to the level the piece wants",
134 ),
135];
136
137pub const NOT_SUPPORTED: [&str; 4] = [
140 "comparison operators: < > <= >= == !=",
141 "boolean operators: && || !",
142 "conditionals: if/else, a ternary",
143 "exponentiation `^`: write pow(base, exponent)",
144];
145
146pub struct Crossing {
148 pub name: &'static str,
149 pub rows: Vec<(&'static str, &'static str)>,
150}
151
152pub struct Builtins {
153 pub callables: Vec<Callable>,
154 pub casts: Vec<Crossing>,
155 pub fourier_dual_rules_version: u64,
156 pub families: &'static [(&'static str, &'static str)],
157 pub refusals: &'static [(&'static str, &'static str)],
158 pub unit_suffixes: &'static [(&'static str, &'static str)],
159 pub note_names: &'static str,
160 pub scales: &'static [(&'static str, &'static [f64], &'static str)],
161 pub reserved: &'static [(&'static str, &'static str)],
162 pub special_forms: &'static [(&'static str, &'static str)],
163 pub not_supported: &'static [&'static str],
164}
165
166fn plain_callable(name: &'static str) -> Callable {
167 let sig = signature(name)
168 .unwrap_or_else(|| unreachable!("{name} is in BUILTINS but SIGNATURES does not cover it"));
169 Callable {
170 name,
171 required: sig.required(),
172 max_positional: sig.most(),
173 named: sig.named(),
174 required_named: &[],
175 takes_gain: None,
176 arguments: meanings(name, sig.named()),
177 positional: positional(name),
178 }
179}
180
181fn filter_callable(shape: Shape) -> Callable {
184 let gain = shape.takes_gain();
185 let max_positional = if shape == Shape::OnePole {
186 2
187 } else if gain {
188 4
189 } else {
190 3
191 };
192 let named = recognized_named(shape_name(shape)).unwrap_or(&[]);
193 Callable {
194 name: shape_name(shape),
195 required: 2,
196 max_positional: Some(max_positional),
197 named,
198 required_named: &[],
199 takes_gain: Some(gain),
200 arguments: meanings(shape_name(shape), named),
201 positional: positional(shape_name(shape)),
202 }
203}
204
205const HELD: [(Held, bool); 6] = [
207 (Held::Form(Var::T), false),
208 (Held::Form(Var::T), true),
209 (Held::Form(Var::F), false),
210 (Held::Form(Var::F), true),
211 (Held::Sampled, false),
212 (Held::Frames, false),
213];
214
215fn crossings() -> Vec<Crossing> {
217 Cast::NAMES
218 .into_iter()
219 .map(|name| {
220 let cast = match Cast::from_name(name) {
221 Some(Cast::Stft { .. }) => Cast::Stft {
222 window: 1024,
223 hop: 256,
224 },
225 Some(cast) => cast,
226 None => unreachable!("{name} is one of Cast::NAMES"),
227 };
228 Crossing {
229 name,
230 rows: HELD
231 .into_iter()
232 .filter_map(|(held, dual)| {
233 let ty = Ty {
234 dual,
235 ..Ty::discrete(held, Codomain::Real)
236 };
237 cast.resolve(&[ty])
238 .ok()
239 .map(|out| (notation(ty), notation(out)))
240 })
241 .collect(),
242 }
243 })
244 .collect()
245}
246
247pub fn builtins() -> Builtins {
248 let mut callables: Vec<Callable> = BUILTINS.into_iter().map(plain_callable).collect();
249 callables.extend(ALL_SHAPES.into_iter().map(filter_callable));
250 Builtins {
251 callables,
252 casts: crossings(),
253 fourier_dual_rules_version: FOURIER_DUAL_RULES_VERSION,
254 families: &FAMILIES,
255 refusals: ®ISTRY,
256 unit_suffixes: &UNIT_SUFFIXES,
257 note_names: NOTE_GRAMMAR,
258 scales: &sva_ast::BUILTIN_SCALES,
259 reserved: &sva_ast::RESERVED,
260 special_forms: &SPECIAL_FORMS,
261 not_supported: &NOT_SUPPORTED,
262 }
263}
264
265pub fn builtins_data(b: &Builtins) -> String {
266 let callables = list(&b.callables, |c| {
267 format!(
268 "\n {{ \"name\": \"{}\", \"required\": {}, \"max_positional\": {}, \"named\": {}, \
269 \"required_named\": {}, \"takes_gain\": {}, \"arguments\": {}, \"positional\": {} }}",
270 escape(c.name),
271 c.required,
272 c.max_positional
273 .map_or_else(|| NONE.to_string(), |n| n.to_string()),
274 strings(c.named),
275 strings(c.required_named),
276 c.takes_gain
277 .map_or_else(|| NONE.to_string(), |g| g.to_string()),
278 list(&c.arguments, argument_json),
279 list(&c.positional, positional_json)
280 )
281 });
282 let casts = list(&b.casts, |c| {
283 format!(
284 "\n {{ \"name\": \"{}\", \"crossings\": {} }}",
285 escape(c.name),
286 list(&c.rows, |(from, to)| format!(
287 "{{ \"from\": \"{from}\", \"to\": \"{to}\" }}"
288 ))
289 )
290 });
291 format!(
292 "{{\n \"callables\": {callables},\n \"casts\": {casts},\n \
293 \"rule_table\": {{ \"version\": {}, \"families\": {} }},\n \
294 \"refusals\": {},\n \"unit_suffixes\": {},\n \"note_names\": \"{}\",\n \
295 \"scales\": {},\n \"reserved\": {},\n \"special_forms\": {},\n \
296 \"not_supported\": {}\n}}",
297 b.fourier_dual_rules_version,
298 pair_list(b.families, "name", "duals"),
299 pair_list(b.refusals, "code", "when"),
300 pair_list(b.unit_suffixes, "suffix", "meaning"),
301 escape(b.note_names),
302 list(b.scales, scale_json),
303 pair_list(b.reserved, "name", "note"),
304 pair_list(b.special_forms, "name", "shape"),
305 strings(b.not_supported)
306 )
307}
308
309fn scale_json((name, cents, tuning): &(&str, &[f64], &str)) -> String {
310 let cents: Vec<String> = cents.iter().map(f64::to_string).collect();
311 format!(
312 "\n {{ \"name\": \"{}\", \"cents\": [{}], \"tuning\": \"{}\" }}",
313 escape(name),
314 cents.join(", "),
315 escape(tuning)
316 )
317}
318
319fn positional_json((name, m): &(&str, Option<Meaning>)) -> String {
320 let text =
321 |t: Option<&str>| t.map_or_else(|| NONE.to_string(), |t| format!("\"{}\"", escape(t)));
322 format!(
323 "{{ \"name\": \"{}\", \"meaning\": {}, \"unit\": {}, \"part\": {} }}",
324 escape(name),
325 text(m.map(|m| m.text)),
326 text(m.map(|m| m.unit)),
327 text(m.and_then(|m| m.part))
328 )
329}
330
331fn argument_json((name, m, moves): &(&str, Meaning, bool)) -> String {
332 format!(
333 "{{ \"name\": \"{}\", \"meaning\": \"{}\", \"unit\": \"{}\", \"part\": {}, \
334 \"moves\": {moves} }}",
335 escape(name),
336 escape(m.text),
337 escape(m.unit),
338 m.part
339 .map_or_else(|| NONE.to_string(), |p| format!("\"{}\"", escape(p)))
340 )
341}
342
343#[cfg(test)]
344mod tests {
345 use super::*;
346
347 #[test]
348 fn the_note_grammar_this_dump_states_is_what_note_actually_resolves() {
349 for note in ["A4", "Cs4", "Db4"] {
350 assert!(
351 sva_ast::note_midi(note).is_some(),
352 "{note} should be a note"
353 );
354 }
355 for not_a_note in ["H4", "Cs", "C99999999999"] {
356 assert!(
357 sva_ast::note_midi(not_a_note).is_none(),
358 "{not_a_note} is not the grammar this dump states"
359 );
360 }
361 }
362
363 #[test]
364 fn every_operator_this_dump_says_does_not_exist_actually_refuses_to_parse() {
365 for bad in [
366 "1 < 2",
367 "1 > 2",
368 "1 <= 2",
369 "1 >= 2",
370 "1 == 2",
371 "1 != 2",
372 "1 && 1",
373 "1 || 1",
374 "!1",
375 "1 ? 2 : 3",
376 "2 ^ 3",
377 ] {
378 assert!(
379 sva_ast::parse_expr(bad).is_err(),
380 "`{bad}` parsed, so this dump's not_supported claim is stale"
381 );
382 }
383 }
384
385 #[test]
386 fn every_builtin_name_gets_exactly_one_callable_entry() {
387 let b = builtins();
388 assert_eq!(b.callables.len(), BUILTINS.len() + ALL_SHAPES.len());
389 for name in BUILTINS {
390 assert_eq!(
391 b.callables.iter().filter(|c| c.name == name).count(),
392 1,
393 "{name} should appear exactly once"
394 );
395 }
396 for shape in ALL_SHAPES {
397 let found = b
398 .callables
399 .iter()
400 .find(|c| c.name == shape_name(shape))
401 .unwrap_or_else(|| panic!("{} is missing", shape_name(shape)));
402 assert_eq!(found.takes_gain, Some(shape.takes_gain()));
403 }
404 }
405
406 #[test]
409 fn every_named_argument_says_what_it_means_and_in_what_unit() {
410 for c in builtins().callables {
411 let named: Vec<&str> = c.arguments.iter().map(|(k, ..)| *k).collect();
412 assert_eq!(named, c.named, "{}", c.name);
413 for (key, m, _) in &c.arguments {
414 assert!(!m.text.is_empty() && !m.unit.is_empty(), "{}.{key}", c.name);
415 }
416 }
417 let b = builtins();
418 let solver = b
419 .callables
420 .iter()
421 .find(|c| c.name == "chaigne_askenfelt")
422 .expect("the solver");
423 let (_, stiffness, moves) = solver.arguments[0];
424 assert!(!moves, "a solver's stiffness is one number");
425 let bore = b.callables.iter().find(|c| c.name == "darabundit_scavone");
426 let (name, held) = bore.expect("the bore").positional[0];
427 let held = held.expect("the bore states its positional");
428 assert_eq!(
429 (name, held.unit),
430 ("length", "m"),
431 "the bore reads a length"
432 );
433 let crop = b.callables.iter().find(|c| c.name == "crop").expect("crop");
434 let bounds: Vec<(&str, Option<&str>)> = crop.positional[1..]
435 .iter()
436 .map(|(name, m)| (*name, m.map(|m| m.unit)))
437 .collect();
438 assert_eq!(
439 bounds,
440 [("start", Some("s")), ("end", Some("s"))],
441 "a window's bounds"
442 );
443 let lowpass = b
444 .callables
445 .iter()
446 .find(|c| c.name == "lowpass")
447 .expect("a filter");
448 assert!(
449 lowpass.arguments.iter().all(|(_, _, moves)| *moves),
450 "a cutoff may sweep"
451 );
452 assert_eq!(
453 (stiffness.text, stiffness.unit, stiffness.part),
454 ("string stiffness (inharmonicity)", "none", Some("string"))
455 );
456 }
457
458 #[test]
459 fn every_built_in_scale_is_listed_with_its_cents() {
460 let listed = builtins_data(&builtins());
461 assert!(
462 listed.contains(
463 "{ \"name\": \"dorian\", \"cents\": [200, 300, 500, 700, 900, 1000, 1200]"
464 ),
465 "{listed}"
466 );
467 for (name, ..) in sva_ast::BUILTIN_SCALES {
468 assert!(listed.contains(&format!("\"name\": \"{name}\"")), "{name}");
469 }
470 }
471
472 #[test]
473 fn one_pole_alone_has_no_room_for_q_or_gain() {
474 let b = builtins();
475 let lp = b.callables.iter().find(|c| c.name == "lp").unwrap();
476 assert_eq!(lp.max_positional, Some(2));
477 let lowpass = b.callables.iter().find(|c| c.name == "lowpass").unwrap();
478 assert_eq!(lowpass.max_positional, Some(3));
479 let peaking = b.callables.iter().find(|c| c.name == "peaking").unwrap();
480 assert_eq!(peaking.max_positional, Some(4));
481 }
482
483 #[test]
484 fn each_reserved_name_still_behaves_the_way_this_dump_says_it_does() {
485 for (name, _) in sva_ast::RESERVED {
486 assert!(
487 sva_ast::is_reserved(name),
488 "`{name}` is listed as reserved but a parameter may still take it"
489 );
490 if name == "self" {
491 continue;
492 }
493 assert!(
494 sva_ast::parse_expr(name).is_ok(),
495 "`{name}` should parse as the special name this dump claims"
496 );
497 }
498 assert!(
499 !sva_ast::is_reserved("cutoff"),
500 "an ordinary parameter name must not be reserved, or the claim says nothing"
501 );
502 assert!(
503 sva_ast::parse_expr("self").is_err(),
504 "`self` alone must still require a call form"
505 );
506 assert!(
507 sva_ast::parse_expr("self(t - 1sp)").is_ok(),
508 "`self(...)` must still parse as a bounded self-reference"
509 );
510 }
511}