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