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=) -- one line per 1/period hertz, each of unit \
130 amplitude, so the series' own RMS is the square root of half its line count; scale \
131 it to the level the piece wants",
132 ),
133];
134
135pub const NOT_SUPPORTED: [&str; 4] = [
138 "comparison operators: < > <= >= == !=",
139 "boolean operators: && || !",
140 "conditionals: if/else, a ternary",
141 "exponentiation `^`: write pow(base, exponent)",
142];
143
144pub struct Crossing {
146 pub name: &'static str,
147 pub rows: Vec<(&'static str, &'static str)>,
148}
149
150pub struct Builtins {
151 pub callables: Vec<Callable>,
152 pub casts: Vec<Crossing>,
153 pub fourier_dual_rules_version: u64,
154 pub families: &'static [(&'static str, &'static str)],
155 pub refusals: &'static [(&'static str, &'static str)],
156 pub unit_suffixes: &'static [(&'static str, &'static str)],
157 pub note_names: &'static str,
158 pub scales: &'static [(&'static str, &'static [f64], &'static str)],
159 pub reserved: &'static [(&'static str, &'static str)],
160 pub special_forms: &'static [(&'static str, &'static str)],
161 pub not_supported: &'static [&'static str],
162}
163
164fn plain_callable(name: &'static str) -> Callable {
165 let sig = signature(name)
166 .unwrap_or_else(|| unreachable!("{name} is in BUILTINS but SIGNATURES does not cover it"));
167 Callable {
168 name,
169 required: sig.required(),
170 max_positional: sig.most(),
171 named: sig.named(),
172 required_named: &[],
173 takes_gain: None,
174 arguments: meanings(name, sig.named()),
175 positional: positional(name),
176 }
177}
178
179fn filter_callable(shape: Shape) -> Callable {
182 let gain = shape.takes_gain();
183 let max_positional = if shape == Shape::OnePole {
184 2
185 } else if gain {
186 4
187 } else {
188 3
189 };
190 let named = recognized_named(shape_name(shape)).unwrap_or(&[]);
191 Callable {
192 name: shape_name(shape),
193 required: 2,
194 max_positional: Some(max_positional),
195 named,
196 required_named: &[],
197 takes_gain: Some(gain),
198 arguments: meanings(shape_name(shape), named),
199 positional: positional(shape_name(shape)),
200 }
201}
202
203const HELD: [(Held, bool); 6] = [
205 (Held::Form(Var::T), false),
206 (Held::Form(Var::T), true),
207 (Held::Form(Var::F), false),
208 (Held::Form(Var::F), true),
209 (Held::Sampled, false),
210 (Held::Frames, false),
211];
212
213fn crossings() -> Vec<Crossing> {
215 Cast::NAMES
216 .into_iter()
217 .map(|name| {
218 let cast = match Cast::from_name(name) {
219 Some(Cast::Stft { .. }) => Cast::Stft {
220 window: 1024,
221 hop: 256,
222 },
223 Some(cast) => cast,
224 None => unreachable!("{name} is one of Cast::NAMES"),
225 };
226 Crossing {
227 name,
228 rows: HELD
229 .into_iter()
230 .filter_map(|(held, dual)| {
231 let ty = Ty {
232 dual,
233 ..Ty::discrete(held, Codomain::Real)
234 };
235 cast.resolve(&[ty])
236 .ok()
237 .map(|out| (notation(ty), notation(out)))
238 })
239 .collect(),
240 }
241 })
242 .collect()
243}
244
245pub fn builtins() -> Builtins {
246 let mut callables: Vec<Callable> = BUILTINS.into_iter().map(plain_callable).collect();
247 callables.extend(ALL_SHAPES.into_iter().map(filter_callable));
248 Builtins {
249 callables,
250 casts: crossings(),
251 fourier_dual_rules_version: FOURIER_DUAL_RULES_VERSION,
252 families: &FAMILIES,
253 refusals: ®ISTRY,
254 unit_suffixes: &UNIT_SUFFIXES,
255 note_names: NOTE_GRAMMAR,
256 scales: &sva_ast::BUILTIN_SCALES,
257 reserved: &sva_ast::RESERVED,
258 special_forms: &SPECIAL_FORMS,
259 not_supported: &NOT_SUPPORTED,
260 }
261}
262
263pub fn builtins_data(b: &Builtins) -> String {
264 let callables = list(&b.callables, |c| {
265 format!(
266 "\n {{ \"name\": \"{}\", \"required\": {}, \"max_positional\": {}, \"named\": {}, \
267 \"required_named\": {}, \"takes_gain\": {}, \"arguments\": {}, \"positional\": {} }}",
268 escape(c.name),
269 c.required,
270 c.max_positional
271 .map_or_else(|| NONE.to_string(), |n| n.to_string()),
272 strings(c.named),
273 strings(c.required_named),
274 c.takes_gain
275 .map_or_else(|| NONE.to_string(), |g| g.to_string()),
276 list(&c.arguments, argument_json),
277 list(&c.positional, positional_json)
278 )
279 });
280 let casts = list(&b.casts, |c| {
281 format!(
282 "\n {{ \"name\": \"{}\", \"crossings\": {} }}",
283 escape(c.name),
284 list(&c.rows, |(from, to)| format!(
285 "{{ \"from\": \"{from}\", \"to\": \"{to}\" }}"
286 ))
287 )
288 });
289 format!(
290 "{{\n \"callables\": {callables},\n \"casts\": {casts},\n \
291 \"rule_table\": {{ \"version\": {}, \"families\": {} }},\n \
292 \"refusals\": {},\n \"unit_suffixes\": {},\n \"note_names\": \"{}\",\n \
293 \"scales\": {},\n \"reserved\": {},\n \"special_forms\": {},\n \
294 \"not_supported\": {}\n}}",
295 b.fourier_dual_rules_version,
296 pair_list(b.families, "name", "duals"),
297 pair_list(b.refusals, "code", "when"),
298 pair_list(b.unit_suffixes, "suffix", "meaning"),
299 escape(b.note_names),
300 list(b.scales, scale_json),
301 pair_list(b.reserved, "name", "note"),
302 pair_list(b.special_forms, "name", "shape"),
303 strings(b.not_supported)
304 )
305}
306
307fn scale_json((name, cents, tuning): &(&str, &[f64], &str)) -> String {
308 let cents: Vec<String> = cents.iter().map(f64::to_string).collect();
309 format!(
310 "\n {{ \"name\": \"{}\", \"cents\": [{}], \"tuning\": \"{}\" }}",
311 escape(name),
312 cents.join(", "),
313 escape(tuning)
314 )
315}
316
317fn positional_json((name, m): &(&str, Option<Meaning>)) -> String {
318 let text =
319 |t: Option<&str>| t.map_or_else(|| NONE.to_string(), |t| format!("\"{}\"", escape(t)));
320 format!(
321 "{{ \"name\": \"{}\", \"meaning\": {}, \"unit\": {}, \"part\": {} }}",
322 escape(name),
323 text(m.map(|m| m.text)),
324 text(m.map(|m| m.unit)),
325 text(m.and_then(|m| m.part))
326 )
327}
328
329fn argument_json((name, m, moves): &(&str, Meaning, bool)) -> String {
330 format!(
331 "{{ \"name\": \"{}\", \"meaning\": \"{}\", \"unit\": \"{}\", \"part\": {}, \
332 \"moves\": {moves} }}",
333 escape(name),
334 escape(m.text),
335 escape(m.unit),
336 m.part
337 .map_or_else(|| NONE.to_string(), |p| format!("\"{}\"", escape(p)))
338 )
339}
340
341#[cfg(test)]
342mod tests {
343 use super::*;
344
345 #[test]
346 fn the_note_grammar_this_dump_states_is_what_note_actually_resolves() {
347 for note in ["A4", "Cs4", "Db4"] {
348 assert!(
349 sva_ast::note_midi(note).is_some(),
350 "{note} should be a note"
351 );
352 }
353 for not_a_note in ["H4", "Cs", "C99999999999"] {
354 assert!(
355 sva_ast::note_midi(not_a_note).is_none(),
356 "{not_a_note} is not the grammar this dump states"
357 );
358 }
359 }
360
361 #[test]
362 fn every_operator_this_dump_says_does_not_exist_actually_refuses_to_parse() {
363 for bad in [
364 "1 < 2",
365 "1 > 2",
366 "1 <= 2",
367 "1 >= 2",
368 "1 == 2",
369 "1 != 2",
370 "1 && 1",
371 "1 || 1",
372 "!1",
373 "1 ? 2 : 3",
374 "2 ^ 3",
375 ] {
376 assert!(
377 sva_ast::parse_expr(bad).is_err(),
378 "`{bad}` parsed, so this dump's not_supported claim is stale"
379 );
380 }
381 }
382
383 #[test]
384 fn every_builtin_name_gets_exactly_one_callable_entry() {
385 let b = builtins();
386 assert_eq!(b.callables.len(), BUILTINS.len() + ALL_SHAPES.len());
387 for name in BUILTINS {
388 assert_eq!(
389 b.callables.iter().filter(|c| c.name == name).count(),
390 1,
391 "{name} should appear exactly once"
392 );
393 }
394 for shape in ALL_SHAPES {
395 let found = b
396 .callables
397 .iter()
398 .find(|c| c.name == shape_name(shape))
399 .unwrap_or_else(|| panic!("{} is missing", shape_name(shape)));
400 assert_eq!(found.takes_gain, Some(shape.takes_gain()));
401 }
402 }
403
404 #[test]
407 fn every_named_argument_says_what_it_means_and_in_what_unit() {
408 for c in builtins().callables {
409 let named: Vec<&str> = c.arguments.iter().map(|(k, ..)| *k).collect();
410 assert_eq!(named, c.named, "{}", c.name);
411 for (key, m, _) in &c.arguments {
412 assert!(!m.text.is_empty() && !m.unit.is_empty(), "{}.{key}", c.name);
413 }
414 }
415 let b = builtins();
416 let solver = b
417 .callables
418 .iter()
419 .find(|c| c.name == "chaigne_askenfelt")
420 .expect("the solver");
421 let (_, stiffness, moves) = solver.arguments[0];
422 assert!(!moves, "a solver's stiffness is one number");
423 let bore = b.callables.iter().find(|c| c.name == "darabundit_scavone");
424 let (name, held) = bore.expect("the bore").positional[0];
425 let held = held.expect("the bore states its positional");
426 assert_eq!(
427 (name, held.unit),
428 ("length", "m"),
429 "the bore reads a length"
430 );
431 let crop = b.callables.iter().find(|c| c.name == "crop").expect("crop");
432 let bounds: Vec<(&str, Option<&str>)> = crop.positional[1..]
433 .iter()
434 .map(|(name, m)| (*name, m.map(|m| m.unit)))
435 .collect();
436 assert_eq!(
437 bounds,
438 [("start", Some("s")), ("end", Some("s"))],
439 "a window's bounds"
440 );
441 let lowpass = b
442 .callables
443 .iter()
444 .find(|c| c.name == "lowpass")
445 .expect("a filter");
446 assert!(
447 lowpass.arguments.iter().all(|(_, _, moves)| *moves),
448 "a cutoff may sweep"
449 );
450 assert_eq!(
451 (stiffness.text, stiffness.unit, stiffness.part),
452 ("string stiffness (inharmonicity)", "none", Some("string"))
453 );
454 }
455
456 #[test]
457 fn every_built_in_scale_is_listed_with_its_cents() {
458 let listed = builtins_data(&builtins());
459 assert!(
460 listed.contains(
461 "{ \"name\": \"dorian\", \"cents\": [200, 300, 500, 700, 900, 1000, 1200]"
462 ),
463 "{listed}"
464 );
465 for (name, ..) in sva_ast::BUILTIN_SCALES {
466 assert!(listed.contains(&format!("\"name\": \"{name}\"")), "{name}");
467 }
468 }
469
470 #[test]
471 fn one_pole_alone_has_no_room_for_q_or_gain() {
472 let b = builtins();
473 let lp = b.callables.iter().find(|c| c.name == "lp").unwrap();
474 assert_eq!(lp.max_positional, Some(2));
475 let lowpass = b.callables.iter().find(|c| c.name == "lowpass").unwrap();
476 assert_eq!(lowpass.max_positional, Some(3));
477 let peaking = b.callables.iter().find(|c| c.name == "peaking").unwrap();
478 assert_eq!(peaking.max_positional, Some(4));
479 }
480
481 #[test]
482 fn each_reserved_name_still_behaves_the_way_this_dump_says_it_does() {
483 for (name, _) in sva_ast::RESERVED {
484 assert!(
485 sva_ast::is_reserved(name),
486 "`{name}` is listed as reserved but a parameter may still take it"
487 );
488 if name == "self" {
489 continue;
490 }
491 assert!(
492 sva_ast::parse_expr(name).is_ok(),
493 "`{name}` should parse as the special name this dump claims"
494 );
495 }
496 assert!(
497 !sva_ast::is_reserved("cutoff"),
498 "an ordinary parameter name must not be reserved, or the claim says nothing"
499 );
500 assert!(
501 sva_ast::parse_expr("self").is_err(),
502 "`self` alone must still require a call form"
503 );
504 assert!(
505 sva_ast::parse_expr("self(t - 1sp)").is_ok(),
506 "`self(...)` must still parse as a bounded self-reference"
507 );
508 }
509}