use sva_ast::BUILTINS;
use sva_engine::overload::{notation, signature};
use sva_engine::{
Cast, Codomain, Held, MAX_WIDTH, Meaning, REGISTRY, Ty, Var, meaning, named_may_move,
recognized_named, shape_name,
};
use sva_formula::filter::{ALL_SHAPES, Shape};
use sva_formula::{FAMILIES, TABLE_VERSION};
use crate::json::{NONE, escape, list, pair_list, strings};
pub struct Callable {
pub name: &'static str,
pub required: usize,
pub max_positional: usize,
pub named: &'static [&'static str],
pub required_named: &'static [&'static str],
pub takes_gain: Option<bool>,
pub arguments: Vec<(&'static str, Meaning, bool)>,
pub positional: Vec<(&'static str, Option<Meaning>)>,
}
fn positional(builtin: &str) -> Vec<(&'static str, Option<Meaning>)> {
signature(builtin)
.map_or(&[][..], |s| s.params)
.iter()
.map(|p| (p.name, meaning(builtin, p.name)))
.collect()
}
fn meanings(builtin: &str, named: &'static [&'static str]) -> Vec<(&'static str, Meaning, bool)> {
named
.iter()
.map(|key| {
let held = meaning(builtin, key)
.unwrap_or_else(|| unreachable!("`{builtin}` takes `{key}` with no meaning"));
(*key, held, named_may_move(builtin, key))
})
.collect()
}
pub const UNIT_SUFFIXES: [(&str, &str); 11] = [
("khz", "kilohertz (x1000)"),
("ms", "milliseconds (seconds x0.001)"),
("sp", "samples"),
("hz", "hertz"),
("db", "decibels (log)"),
("ct", "cents (log)"),
("st", "semitones (log)"),
("b", "bars, resolved against bpm/meter"),
("s", "seconds"),
("m", "minutes (seconds x60)"),
("h", "hours (seconds x3600)"),
];
pub const NOTE_GRAMMAR: &str = "a bare identifier: a letter A-G, an optional accidental spelled \
s (sharp) or b (flat) -- never #, then a whole-number octave, e.g. A4, Cs4, Db4; A4 = 440 Hz, \
twelve-tone equal temperament, up to G9 where MIDI's 128 notes end";
pub const SPECIAL_FORMS: [(&str, &str); 7] = [
(
"sum",
"sum(index, lo, hi, expr) -- index is a name the series binds, not a value; hi may be \
inf, which makes it a series",
),
(
"crop",
"crop(x, start, end) -- windows x to [start, end) seconds, zero outside",
),
(
"join",
"join(a, b, ...) -- builds one wide value from 2..=8 mono args",
),
(
"ch",
"ch(x, index) -- extracts one component of a wide value by a literal index",
),
(
"x[i]",
"@x[i], self[i] -- the stored sample at integer index i, at the rate in use; @x(e) \
reads the instant e",
),
(
"idx",
"idx(time), idx(time, floor), idx(time, ceil) -- the sample index nearest a time, \
ties to even, or the one below or above it; written only inside an index",
),
(
"noise",
"noise(seed, period=, color=) -- one line per 1/period hertz, each of unit \
amplitude, so the series' own RMS is the square root of half its line count; scale \
it to the level the piece wants",
),
];
pub const NOT_SUPPORTED: [&str; 4] = [
"comparison operators: < > <= >= == !=",
"boolean operators: && || !",
"conditionals: if/else, a ternary",
"exponentiation `^`: write pow(base, exponent)",
];
pub struct Crossing {
pub name: &'static str,
pub rows: Vec<(&'static str, &'static str)>,
}
pub struct Builtins {
pub callables: Vec<Callable>,
pub casts: Vec<Crossing>,
pub table_version: u64,
pub families: &'static [(&'static str, &'static str)],
pub refusals: &'static [(&'static str, &'static str)],
pub unit_suffixes: &'static [(&'static str, &'static str)],
pub note_names: &'static str,
pub reserved: &'static [(&'static str, &'static str)],
pub special_forms: &'static [(&'static str, &'static str)],
pub not_supported: &'static [&'static str],
}
fn plain_callable(name: &'static str) -> Callable {
let sig = signature(name)
.unwrap_or_else(|| unreachable!("{name} is in BUILTINS but SIGNATURES does not cover it"));
Callable {
name,
required: sig.required(),
max_positional: if sig.variadic {
MAX_WIDTH
} else {
sig.params.len()
},
named: sig.named(),
required_named: &[],
takes_gain: None,
arguments: meanings(name, sig.named()),
positional: positional(name),
}
}
fn filter_callable(shape: Shape) -> Callable {
let gain = shape.takes_gain();
let max_positional = if shape == Shape::OnePole {
2
} else if gain {
4
} else {
3
};
let named = recognized_named(shape_name(shape)).unwrap_or(&[]);
Callable {
name: shape_name(shape),
required: 2,
max_positional,
named,
required_named: &[],
takes_gain: Some(gain),
arguments: meanings(shape_name(shape), named),
positional: positional(shape_name(shape)),
}
}
const HELD: [(Held, bool); 6] = [
(Held::Form(Var::T), false),
(Held::Form(Var::T), true),
(Held::Form(Var::F), false),
(Held::Form(Var::F), true),
(Held::Sampled, false),
(Held::Frames, false),
];
fn crossings() -> Vec<Crossing> {
Cast::NAMES
.into_iter()
.map(|name| {
let cast = match Cast::from_name(name) {
Some(Cast::Stft { .. }) => Cast::Stft {
window: 1024,
hop: 256,
},
Some(cast) => cast,
None => unreachable!("{name} is one of Cast::NAMES"),
};
Crossing {
name,
rows: HELD
.into_iter()
.filter_map(|(held, dual)| {
let ty = Ty {
dual,
..Ty::discrete(held, Codomain::Real)
};
cast.resolve(&[ty])
.ok()
.map(|out| (notation(ty), notation(out)))
})
.collect(),
}
})
.collect()
}
pub fn builtins() -> Builtins {
let mut callables: Vec<Callable> = BUILTINS.into_iter().map(plain_callable).collect();
callables.extend(ALL_SHAPES.into_iter().map(filter_callable));
Builtins {
callables,
casts: crossings(),
table_version: TABLE_VERSION,
families: &FAMILIES,
refusals: ®ISTRY,
unit_suffixes: &UNIT_SUFFIXES,
note_names: NOTE_GRAMMAR,
reserved: &sva_ast::RESERVED,
special_forms: &SPECIAL_FORMS,
not_supported: &NOT_SUPPORTED,
}
}
pub fn builtins_data(b: &Builtins) -> String {
let callables = list(&b.callables, |c| {
format!(
"\n {{ \"name\": \"{}\", \"required\": {}, \"max_positional\": {}, \"named\": {}, \
\"required_named\": {}, \"takes_gain\": {}, \"arguments\": {}, \"positional\": {} }}",
escape(c.name),
c.required,
c.max_positional,
strings(c.named),
strings(c.required_named),
c.takes_gain
.map_or_else(|| NONE.to_string(), |g| g.to_string()),
list(&c.arguments, argument_json),
list(&c.positional, positional_json)
)
});
let casts = list(&b.casts, |c| {
format!(
"\n {{ \"name\": \"{}\", \"crossings\": {} }}",
escape(c.name),
list(&c.rows, |(from, to)| format!(
"{{ \"from\": \"{from}\", \"to\": \"{to}\" }}"
))
)
});
format!(
"{{\n \"callables\": {callables},\n \"casts\": {casts},\n \
\"rule_table\": {{ \"version\": {}, \"families\": {} }},\n \
\"refusals\": {},\n \"unit_suffixes\": {},\n \"note_names\": \"{}\",\n \
\"reserved\": {},\n \"special_forms\": {},\n \
\"not_supported\": {}\n}}",
b.table_version,
pair_list(b.families, "name", "duals"),
pair_list(b.refusals, "code", "when"),
pair_list(b.unit_suffixes, "suffix", "meaning"),
escape(b.note_names),
pair_list(b.reserved, "name", "note"),
pair_list(b.special_forms, "name", "shape"),
strings(b.not_supported)
)
}
fn positional_json((name, m): &(&str, Option<Meaning>)) -> String {
let text =
|t: Option<&str>| t.map_or_else(|| NONE.to_string(), |t| format!("\"{}\"", escape(t)));
format!(
"{{ \"name\": \"{}\", \"meaning\": {}, \"unit\": {}, \"part\": {} }}",
escape(name),
text(m.map(|m| m.text)),
text(m.map(|m| m.unit)),
text(m.and_then(|m| m.part))
)
}
fn argument_json((name, m, moves): &(&str, Meaning, bool)) -> String {
format!(
"{{ \"name\": \"{}\", \"meaning\": \"{}\", \"unit\": \"{}\", \"part\": {}, \
\"moves\": {moves} }}",
escape(name),
escape(m.text),
escape(m.unit),
m.part
.map_or_else(|| NONE.to_string(), |p| format!("\"{}\"", escape(p)))
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_note_grammar_this_dump_states_is_what_note_actually_resolves() {
for note in ["A4", "Cs4", "Db4"] {
assert!(
sva_ast::note_midi(note).is_some(),
"{note} should be a note"
);
}
for not_a_note in ["H4", "Cs", "C99999999999"] {
assert!(
sva_ast::note_midi(not_a_note).is_none(),
"{not_a_note} is not the grammar this dump states"
);
}
}
#[test]
fn every_operator_this_dump_says_does_not_exist_actually_refuses_to_parse() {
for bad in [
"1 < 2",
"1 > 2",
"1 <= 2",
"1 >= 2",
"1 == 2",
"1 != 2",
"1 && 1",
"1 || 1",
"!1",
"1 ? 2 : 3",
"2 ^ 3",
] {
assert!(
sva_ast::parse_expr(bad).is_err(),
"`{bad}` parsed, so this dump's not_supported claim is stale"
);
}
}
#[test]
fn every_builtin_name_gets_exactly_one_callable_entry() {
let b = builtins();
assert_eq!(b.callables.len(), BUILTINS.len() + ALL_SHAPES.len());
for name in BUILTINS {
assert_eq!(
b.callables.iter().filter(|c| c.name == name).count(),
1,
"{name} should appear exactly once"
);
}
for shape in ALL_SHAPES {
let found = b
.callables
.iter()
.find(|c| c.name == shape_name(shape))
.unwrap_or_else(|| panic!("{} is missing", shape_name(shape)));
assert_eq!(found.takes_gain, Some(shape.takes_gain()));
}
}
#[test]
fn every_named_argument_says_what_it_means_and_in_what_unit() {
for c in builtins().callables {
let named: Vec<&str> = c.arguments.iter().map(|(k, ..)| *k).collect();
assert_eq!(named, c.named, "{}", c.name);
for (key, m, _) in &c.arguments {
assert!(!m.text.is_empty() && !m.unit.is_empty(), "{}.{key}", c.name);
}
}
let b = builtins();
let solver = b
.callables
.iter()
.find(|c| c.name == "chaigne_askenfelt")
.expect("the solver");
let (_, stiffness, moves) = solver.arguments[0];
assert!(!moves, "a solver's stiffness is one number");
let bore = b.callables.iter().find(|c| c.name == "darabundit_scavone");
let (name, held) = bore.expect("the bore").positional[0];
let held = held.expect("the bore states its positional");
assert_eq!(
(name, held.unit),
("length", "m"),
"the bore reads a length"
);
let crop = b.callables.iter().find(|c| c.name == "crop").expect("crop");
let bounds: Vec<(&str, Option<&str>)> = crop.positional[1..]
.iter()
.map(|(name, m)| (*name, m.map(|m| m.unit)))
.collect();
assert_eq!(
bounds,
[("start", Some("s")), ("end", Some("s"))],
"a window's bounds"
);
let lowpass = b
.callables
.iter()
.find(|c| c.name == "lowpass")
.expect("a filter");
assert!(
lowpass.arguments.iter().all(|(_, _, moves)| *moves),
"a cutoff may sweep"
);
assert_eq!(
(stiffness.text, stiffness.unit, stiffness.part),
("string stiffness (inharmonicity)", "none", Some("string"))
);
}
#[test]
fn one_pole_alone_has_no_room_for_q_or_gain() {
let b = builtins();
let lp = b.callables.iter().find(|c| c.name == "lp").unwrap();
assert_eq!(lp.max_positional, 2);
let lowpass = b.callables.iter().find(|c| c.name == "lowpass").unwrap();
assert_eq!(lowpass.max_positional, 3);
let peaking = b.callables.iter().find(|c| c.name == "peaking").unwrap();
assert_eq!(peaking.max_positional, 4);
}
#[test]
fn each_reserved_name_still_behaves_the_way_this_dump_says_it_does() {
for (name, _) in sva_ast::RESERVED {
assert!(
sva_ast::is_reserved(name),
"`{name}` is listed as reserved but a parameter may still take it"
);
if name == "self" {
continue;
}
assert!(
sva_ast::parse_expr(name).is_ok(),
"`{name}` should parse as the special name this dump claims"
);
}
assert!(
!sva_ast::is_reserved("cutoff"),
"an ordinary parameter name must not be reserved, or the claim says nothing"
);
assert!(
sva_ast::parse_expr("self").is_err(),
"`self` alone must still require a call form"
);
assert!(
sva_ast::parse_expr("self(t - 1sp)").is_ok(),
"`self(...)` must still parse as a bounded self-reference"
);
}
}