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