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