Skip to main content

sva_core/
builtins.rs

1// Concern: assembles the language's callable/syntactic vocabulary off the engine's own tables, as the `data` object | Non-concern: what a builtin renders | IO: none -> Builtins, JSON
2
3use 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    /// A subset of `named` with no default — a caller who omits one gets a refusal, not a
20    /// fallback. Every builtin leaves this empty today.
21    pub required_named: &'static [&'static str],
22    pub takes_gain: Option<bool>,
23    /// Each of `named`, in its order, beside what it means and whether it may move with `t`.
24    pub arguments: Vec<(&'static str, Meaning, bool)>,
25    /// The signature's positional names, each beside its meaning where the model states one.
26    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
48/// `(spelling, meaning)`, in the lexer's own longest-match-first order.
49pub 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
67/// `(name, its call shape)`.
68pub 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
113/// Confirmed against the lexer's and parser's own tables, not asserted from memory: no token
114/// exists for any of these, so none can appear in an expression at all.
115pub 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
122/// One written cast, and every representation it takes to which.
123pub 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
160/// `q` is refused outright on `Shape::OnePole` rather than merely unused, so its own positional
161/// ceiling sits below every other shape's.
162fn 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
184/// Every state a value's form can be in, so a crossing table shows which duals a cast needs.
185const 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
194/// Read off `Cast::resolve` rather than restated: a row exists where the cast answers.
195fn 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: &REGISTRY,
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    /// `builtins()` refuses to assemble a named argument with no meaning, so this reaching
373    /// every callable is the whole vocabulary explained.
374    #[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}