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