use sva_ast::{Arg, ByteSpan, Expr};
use sva_formula::{
Body, C64, Codomain, Edge, Fold, Held, NodeId, Origin, Part, Ty, Unary, Var, hash,
};
use sva_samples::physics::Varies;
use crate::arguments::{Argument, Called, Chosen};
use crate::cast::Cast;
use crate::error::EngineError;
use crate::instantiate::Cx;
use crate::lower::{Lowering, Piece};
use crate::time::Lattice;
use crate::typing::{Value, When};
use sva_ast::SERIES;
impl Lowering<'_> {
pub(super) fn call(
&mut self,
name: &str,
args: &[Arg],
span: ByteSpan,
cx: Cx,
var: Var,
) -> Result<Piece, EngineError> {
if name == SERIES {
return self.sum(args, span, cx, var);
}
if name == sva_ast::INDEX {
return Err(self.refused_at(
"type.index_outside_read",
"idx(...) names a sample index, which only an index read takes".to_string(),
"read by index, as @x[idx(t - 0.5b)], or write the time itself",
Some(span),
));
}
let written = args.iter().filter(|a| matches!(a, Arg::Pos(_))).count();
let keys: Vec<String> = args
.iter()
.filter_map(|a| match a {
Arg::Named(k, _) => Some(k.clone()),
Arg::Pos(_) => None,
})
.collect();
crate::overload::check_arity(name, written, &keys)
.map_err(|m| self.refuse(name, &m, Some(span)))?;
let mut chosen = Vec::new();
let named = self.named_values(name, args, span, cx, &mut chosen)?;
let view: Vec<(&str, f64)> = named.iter().map(|(k, v)| (k.as_str(), *v)).collect();
let solved = sva_ast::FINITE_DIFFERENCE.contains(&name);
if solved || sva_ast::MODAL.contains(&name) {
let Some(numbers) = positional_values(self, args, cx, &mut chosen) else {
return Err(EngineError::BadArity(name.to_string()));
};
if !solved {
self.note_call(name, span, positional_named(name, &numbers, &named), chosen);
return self.modal(name, &numbers, &view);
}
return self.solver(name, (&numbers, &view, args), span, chosen, (cx, var));
}
if name == "noise" {
let Some(numbers) = positional_values(self, args, cx, &mut chosen) else {
return Err(EngineError::BadArity(name.to_string()));
};
let [seed, ..] = numbers.as_slice() else {
return Err(EngineError::BadArity(name.to_string()));
};
self.note_call(name, span, positional_named(name, &numbers, &named), chosen);
return Ok(Piece::ClosedForm(Body::Series(Box::new(
sva_formula::noise(
*seed as u64,
named_or(&view, "period", 1.0),
named_or(&view, "color", 0.0),
),
))));
}
self.note_call(name, span, written_named(&named), chosen);
if let Some(cast) = Cast::from_name(name) {
let cast = match cast {
Cast::Stft { .. } => Cast::Stft {
window: self.step_count(args, "window", span, cx)?,
hop: self.step_count(args, "hop", span, cx)?,
},
other => other,
};
return self.cast(cast, args, span, cx, var);
}
if let Some(shape) = crate::vocabulary::shape(name) {
return self.filter(shape, args, span, cx, var);
}
if name == "rand" {
return self.drawn(args, span, cx, var);
}
let positional: Vec<&Expr> = args
.iter()
.filter_map(|a| match a {
Arg::Pos(x) => Some(x),
Arg::Named(..) => None,
})
.collect();
if let Some(piece) = self.wave(name, &positional, &view, cx, var)? {
return Ok(piece);
}
let mut pieces = Vec::with_capacity(positional.len());
for x in &positional {
pieces.push(self.walk(x, cx, var)?);
}
if let Some(held) = beside_infinity(name, &mut pieces) {
return Ok(held);
}
if pieces.iter().any(|p| matches!(p, Piece::Value(_))) {
return match (name, view.iter().find(|(k, _)| *k == "drive")) {
("sat", Some((_, drive))) => self.driven(pieces, *drive, span, var),
("crop", _) => self.sampled_crop(pieces, &view, span, var),
_ => self.operation(name, pieces, Some(span), var),
};
}
let bodies: Vec<Body> = pieces
.into_iter()
.map(|p| match p {
Piece::ClosedForm(f) => f,
Piece::Value(_) => unreachable!("every piece here stayed inside the closed form"),
})
.collect();
self.image(name, bodies, &positional, &view, span, var)
}
fn image(
&mut self,
name: &str,
bodies: Vec<Body>,
written: &[&Expr],
named: &[(&str, f64)],
span: ByteSpan,
var: Var,
) -> Result<Piece, EngineError> {
let read = |key: &str, fallback: f64| named_or(named, key, fallback);
let arity = |want: usize| -> Result<(), EngineError> {
match bodies.len() == want {
true => Ok(()),
false => Err(EngineError::BadArity(name.to_string())),
}
};
if numeric(name) {
let origin = self.typing.mark(self.node, Some(span));
let two = bodies.len() == 2;
return match arithmetic(name, bodies, named, var, origin) {
Some(body) => self.folded(body, var),
None if name == "pow" && two => Err(self.non_integer_power(written, span)),
None => Err(EngineError::BadArity(name.to_string())),
};
}
match name {
"crop" => {
arity(3)?;
let (rise, fall) = shoulders_of(named);
let Some((l, r)) = self.cropped(&bodies[1], &bodies[2], (rise, fall), var, span)?
else {
return Ok(Piece::ClosedForm(Body::Const(C64::ZERO)));
};
let fall = match r {
Edge::PosInf => 0.0,
_ => fall,
};
let of = self.part(first(bodies), Some(span));
Ok(Piece::ClosedForm(Body::Crop {
of,
l,
r,
rise,
fall,
}))
}
"delta" => {
arity(1)?;
let at = self.part(first(bodies), Some(span));
Ok(Piece::ClosedForm(Body::Delta {
at,
order: read("k", 0.0) as u16,
}))
}
"pv" => {
arity(1)?;
let at = self.part(first(bodies), Some(span));
Ok(Piece::ClosedForm(Body::Pv(at)))
}
"join" => {
let parts = self.parts(bodies, span);
Ok(Piece::ClosedForm(Body::Join(parts)))
}
"ch" => {
arity(2)?;
let Some(index) = super::constant_value(&bodies[1], var) else {
return Err(EngineError::BadArity(name.to_string()));
};
let of = self.part(first(bodies), Some(span));
Ok(Piece::ClosedForm(Body::Channel(of, index as u8)))
}
other => Err(EngineError::UnknownBuiltin(other.to_string())),
}
}
fn driven(
&mut self,
mut pieces: Vec<Piece>,
drive: f64,
span: ByteSpan,
var: Var,
) -> Result<Piece, EngineError> {
pieces.push(Piece::ClosedForm(Body::Const(C64::real(drive))));
let product = self.operation("*", pieces, Some(span), var)?;
self.operation("sat", vec![product], Some(span), var)
}
fn sampled_crop(
&mut self,
mut pieces: Vec<Piece>,
named: &[(&str, f64)],
span: ByteSpan,
var: Var,
) -> Result<Piece, EngineError> {
let (rise, fall) = shoulders_of(named);
if let [_, Piece::ClosedForm(l), Piece::ClosedForm(r)] = pieces.as_slice()
&& self.cropped(l, r, (rise, fall), var, span)?.is_none()
{
return Ok(Piece::ClosedForm(Body::Const(C64::ZERO)));
}
if rise > 0.0 || fall > 0.0 {
for shoulder in [rise, fall] {
pieces.push(Piece::ClosedForm(Body::Const(C64::real(shoulder))));
}
}
self.operation("crop", pieces, Some(span), var)
}
fn non_integer_power(&self, written: &[&Expr], span: ByteSpan) -> EngineError {
let [_, exponent] = written else {
unreachable!("pow checked its arity before it lowered either operand")
};
self.refused_at(
"type.non_integer_power",
format!(
"`pow` raises a signal to `{}`, which names no polynomial power.",
sva_ast::render_expr(exponent)
),
"write a whole exponent within 65535, or a positive constant base, which raises \
as exp(x*ln(base))",
Some(span),
)
}
fn window(
&mut self,
l: &Body,
r: &Body,
var: Var,
name: &str,
span: ByteSpan,
) -> Result<(Edge, Edge), EngineError> {
Ok((
self.edge(l, var, name, "start", span)?,
self.edge(r, var, name, "end", span)?,
))
}
fn cropped(
&mut self,
l: &Body,
r: &Body,
(rise, fall): (f64, f64),
var: Var,
span: ByteSpan,
) -> Result<Option<(Edge, Edge)>, EngineError> {
let (l, r) = self.window(l, r, var, "crop", span)?;
if l >= r {
return Ok(None);
}
self.shoulders(rise, fall, r.value() - l.value(), span)?;
Ok(Some((l, r)))
}
fn shoulders(&self, rise: f64, fall: f64, span: f64, at: ByteSpan) -> Result<(), EngineError> {
let refuse = |what: String| {
Err(self.refused_at(
"engine.bad_crop_shoulder",
what,
"write a rise and a fall at or above zero that together fit the window",
Some(at),
))
};
if rise < 0.0 || fall < 0.0 {
return refuse(format!(
"`crop` reads rise={rise} and fall={fall}; a shoulder \
opens forward in time."
));
}
if rise + fall > span {
return refuse(format!(
"`crop`'s rise={rise} and fall={fall} are {} together, longer than the {span} \
the window itself runs.",
rise + fall
));
}
Ok(())
}
fn edge(
&self,
body: &Body,
var: Var,
name: &str,
which: &str,
span: ByteSpan,
) -> Result<Edge, EngineError> {
let folded = crate::refs::fold_constants(self.typing, body);
let infinite = match *folded {
Body::Const(c) if c.im == 0.0 && c.re.is_infinite() => Some(c.re),
_ => None,
};
match infinite.or_else(|| super::constant_value(&folded, var)) {
Some(x) => Ok(Edge::at(x)),
None => Err(self.refused_at(
"engine.non_constant_argument",
format!("`{name}` reads `{which}` as a number, and this one moves."),
"write a constant there; a bound that moves with t names no window",
Some(span),
)),
}
}
fn parts(&mut self, bodies: Vec<Body>, span: ByteSpan) -> Vec<Part> {
bodies
.into_iter()
.map(|b| self.part(b, Some(span)))
.collect()
}
pub(super) fn finite_argument(
&self,
name: &str,
key: &str,
written: &Expr,
folded: Option<f64>,
) -> Result<(), EngineError> {
match folded {
Some(v) if !v.is_finite() => Err(self.infinite(&format!(
"`{name}` reads `{key}={}`, which is {v}, and an argument is a finite number",
sva_ast::render_expr(written)
))),
_ => Ok(()),
}
}
fn named_values(
&self,
name: &str,
args: &[Arg],
span: ByteSpan,
cx: Cx,
chosen: &mut Vec<Chosen>,
) -> Result<Vec<(String, f64)>, EngineError> {
let mut out = Vec::new();
for arg in args {
let Arg::Named(key, value) = arg else {
continue;
};
let folded = crate::loops::amount_choosing(self.inst, value, cx, chosen);
self.finite_argument(name, key, value, folded)?;
match folded.and_then(crate::loops::plain) {
Some(v) => out.push((key.clone(), v)),
None if crate::vocabulary::named_may_move(name, key) => {}
None => {
return Err(self.refused_at(
"engine.non_constant_argument",
format!(
"`{name}` reads `{key}={}` as one number, and it names none.",
sva_ast::render_expr(value)
),
"write a constant there: a number, a unit, or a ref naming one",
Some(span),
));
}
}
}
Ok(out)
}
fn chosen_value(&self, e: &Expr, cx: Cx, chosen: &mut Vec<Chosen>) -> Option<f64> {
crate::loops::plain(crate::loops::amount_choosing(self.inst, e, cx, chosen)?)
}
fn note_call(
&mut self,
name: &str,
at: ByteSpan,
arguments: Vec<Argument>,
chosen: Vec<Chosen>,
) {
if arguments.is_empty() && chosen.is_empty() || !self.grid.is_rate() {
return;
}
let call = (!arguments.is_empty()).then(|| Called {
name: name.to_string(),
at,
arguments,
});
self.typing.note(self.node, call, chosen);
}
fn solver(
&mut self,
name: &str,
(numbers, view, args): (&[f64], &[(&str, f64)], &[Arg]),
span: ByteSpan,
chosen: Vec<Chosen>,
(cx, var): (Cx, Var),
) -> Result<Piece, EngineError> {
let [first, ..] = numbers else {
return Err(EngineError::BadArity(name.to_string()));
};
let mut params = super::solvers::params(name, *first, view);
let mut varying = Vec::new();
for (key, varies) in sva_samples::physics::varying(name) {
let written = args.iter().find_map(|a| match a {
Arg::Named(k, x) if k == key => Some(x),
_ => None,
});
let Some(written) = written.filter(|_| !view.iter().any(|(k, _)| k == key)) else {
continue;
};
let id = self.automation((name, key), written, cx, var)?;
if let Value::ClosedForm(form) = self.typing.value(id)
&& let Some(v) = super::constant_value(&form.body, form.var)
{
*super::field(&mut params, key).expect("a varying field") = v + 0.0;
continue;
}
if *varies == Varies::Piecewise && !self.piecewise(id) {
return Err(self.moving_energy(name, key, span));
}
varying.push((*key, id));
}
if !params.valid() {
return Err(EngineError::refused(crate::error::Diagnostic {
code: "engine.physics_out_of_range".to_string(),
message: format!("`{name}` was given an argument outside the range it models"),
location: crate::error::Located::at(name, Some(span)),
help: "`sva-cli builtins` names every argument each solver takes".to_string(),
}));
}
let handed = super::solvers::handed(¶ms)
.into_iter()
.filter(|(key, _)| !varying.iter().any(|(k, _)| k == key))
.enumerate()
.map(|(at, (key, value))| Argument {
written: at == 0 || view.iter().any(|(k, _)| *k == key),
name: key,
value,
})
.collect();
self.note_call(name, span, handed, chosen);
let value = Value::Solver {
params: Box::new(params),
varying,
};
let ty = Ty::discrete(Held::Sampled, Codomain::Real);
Ok(Piece::Value(self.register(value, ty, var)))
}
fn piecewise(&self, id: NodeId) -> bool {
fn steps(f: &Body) -> bool {
match f {
Body::Const(_) => true,
Body::Crop { of, rise, fall, .. } => {
*rise == 0.0 && *fall == 0.0 && steps(&of.body)
}
Body::Add(parts) | Body::Mul(parts) | Body::Fold(Fold::Max | Fold::Min, parts) => {
parts.iter().all(|p| steps(&p.body))
}
Body::Div(a, b) => steps(&a.body) && super::constant::is_constant(&b.body),
_ => false,
}
}
crate::refs::substituted_closed_form(self.typing, id).is_some_and(|f| steps(&f.body))
}
fn moving_energy(&self, name: &str, key: &str, span: ByteSpan) -> EngineError {
self.refused_at(
"engine.moving_energy_parameter",
format!(
"`{name}` stores energy in `{key}`, which may only jump: a coefficient that \
moves between jumps can pump energy into the scheme"
),
"write it as numbers under crops that open and close without shoulders, as \
k*step(t - t0)",
Some(span),
)
}
pub(super) fn named_value(&self, e: &Expr, cx: Cx) -> Option<f64> {
crate::loops::plain(crate::loops::amount(self.inst, e, cx)?)
}
fn step_count(
&self,
args: &[Arg],
key: &str,
span: ByteSpan,
cx: Cx,
) -> Result<usize, EngineError> {
let Some(written) = args.iter().find_map(|a| match a {
Arg::Named(k, x) if k == key => Some(x),
_ => None,
}) else {
return Ok(0);
};
let count = crate::loops::time_of(self.inst, written, cx)
.filter(|at| at.scale.is_zero())
.and_then(|at| at.shift.div(self.grid.steps(crate::time::Q::ONE)?))
.filter(|n| n.is_integer() && n.num() > 0);
match count {
Some(n) => Ok(n.num() as usize),
None => Err(self.refused_at(
"cast.window_off_the_lattice",
format!(
"`stft`'s `{key}` is no whole number of steps at this rate: {}",
sva_ast::render_expr(written)
),
"write it in sp, as window=2048sp",
Some(span),
)),
}
}
fn drawn(
&mut self,
args: &[Arg],
span: ByteSpan,
cx: Cx,
var: Var,
) -> Result<Piece, EngineError> {
let bad = || EngineError::BadArity("rand".to_string());
let (key, seed) = rand_arguments(args, |x| self.named_value(x, cx)).ok_or_else(bad)?;
let ty = sva_formula::Ty::discrete(sva_formula::Held::Sampled, sva_formula::Codomain::Real);
let (grid, at) = match crate::loops::time_of(self.inst, key, cx) {
Some(at) if at.scale.is_zero() => {
let drawn = noise_at(seed, at.shift, self.inst.rate());
return Ok(Piece::ClosedForm(Body::Const(C64::real(drawn))));
}
Some(at) => {
let unplaced = || EngineError::BadArity("rand".to_string());
(self.grid.speed(at).ok_or_else(unplaced)?, When::At(at))
}
None => (self.grid, When::Moving(self.time(key, cx)?)),
};
let noise = self.typing.push(
crate::typing::Node {
name: self.node.to_string(),
ty,
var,
value: Value::Noise(seed),
grid,
},
None,
);
match at {
When::At(crate::time::Affine::NOW) => Ok(Piece::Value(noise)),
at => Ok(Piece::Value(self.reading(noise, at, span, var))),
}
}
}
pub(crate) fn rand_arguments(
args: &[Arg],
number: impl Fn(&Expr) -> Option<f64>,
) -> Option<(&Expr, u64)> {
let mut positional = args.iter().filter_map(|a| match a {
Arg::Pos(x) => Some(x),
Arg::Named(..) => None,
});
let key = positional.next()?;
let seed = match positional.next() {
Some(x) => number(x)?,
None => match args.iter().find_map(|a| match a {
Arg::Named(k, x) if k == "seed" => Some(x),
_ => None,
}) {
Some(x) => number(x)?,
None => 0.0,
},
};
Some((key, seed as u64))
}
pub(crate) fn noise_at(seed: u64, at: crate::time::Q, rate: u32) -> f64 {
match at
.mul(crate::time::Q::int(i64::from(rate)))
.and_then(crate::time::nearest)
{
Some(step) => hash::draw(seed, step),
None => f64::NAN,
}
}
pub(super) fn numeric(name: &str) -> bool {
Unary::from_name(name).is_some() || matches!(name, "max" | "min" | "pow")
}
pub(super) fn arithmetic(
name: &str,
bodies: Vec<Body>,
named: &[(&str, f64)],
var: Var,
origin: Origin,
) -> Option<Body> {
let bodies = match <[Body; 1]>::try_from(bodies) {
Ok([x]) => return unary(name, x, named, origin),
Err(bodies) => bodies,
};
let [a, b] = <[Body; 2]>::try_from(bodies).ok()?;
let fold = |op: Fold, a: Body, b: Body| {
Body::Fold(op, vec![Part::new(origin, a), Part::new(origin, b)])
};
match name {
"max" => Some(fold(Fold::Max, a, b)),
"min" => Some(fold(Fold::Min, a, b)),
"pow" => power(a, b, var, origin),
_ => None,
}
}
fn unary(name: &str, x: Body, named: &[(&str, f64)], origin: Origin) -> Option<Body> {
let part = |f: Body| Part::new(origin, f);
if name == "sat" {
let drive = Body::Const(C64::real(named_or(named, "drive", 1.0)));
let driven = Body::Mul(vec![part(x), part(drive)]);
return Some(Body::Apply(Unary::Sat, part(driven)));
}
if name == "step"
&& let Some(at) = rising_zero(&x)
{
return Some(Body::Crop {
of: part(Body::Const(C64::ONE)),
l: Edge::at(at),
r: Edge::PosInf,
rise: 0.0,
fall: 0.0,
});
}
Unary::from_name(name).map(|op| Body::Apply(op, part(x)))
}
fn first(bodies: Vec<Body>) -> Body {
bodies
.into_iter()
.next()
.expect("arity checked before the operand is read")
}
fn beside_infinity(name: &str, pieces: &mut Vec<Piece>) -> Option<Piece> {
let absorbs = match name {
"min" => f64::NEG_INFINITY,
"max" => f64::INFINITY,
_ => return None,
};
let infinite = |p: &Piece| match p {
Piece::ClosedForm(Body::Const(c)) if c.im == 0.0 && c.re.is_infinite() => Some(c.re),
_ => None,
};
let at = pieces.iter().position(|p| infinite(p).is_some())?;
if pieces.len() != 2 {
return None;
}
let held = infinite(&pieces[at]).expect("an infinite operand");
match held == absorbs {
true => Some(pieces.swap_remove(at)),
false => Some(pieces.swap_remove(1 - at)),
}
}
fn rising_zero(x: &Body) -> Option<f64> {
let (a, b) = sva_formula::affine::exact_affine(x)?;
let (a, b) = (a.is_real().then_some(a.re)?, b.is_real().then_some(b.re)?);
let at = -b / a;
(a > 0.0 && !at.is_nan()).then_some(at)
}
fn power(base: Body, exponent: Body, var: Var, origin: Origin) -> Option<Body> {
let part = |f: Body| Part::new(origin, f);
let n = super::constant_value(&exponent, var);
if let Some(n) = n.filter(|n| whole(*n)) {
return Some(Body::Pow(part(base), n as i32));
}
let b = super::constant_value(&base, var).filter(|b| *b > 0.0)?;
if let Some(n) = n {
return Some(Body::Const(C64::real(b.powf(n))));
}
let rate = Body::Const(C64::real(b.ln()));
let scaled = Body::Mul(vec![part(exponent), part(rate)]);
Some(Body::Apply(Unary::Exp, part(scaled)))
}
fn whole(n: f64) -> bool {
n.fract() == 0.0 && n.abs() <= f64::from(u16::MAX)
}
fn shoulders_of(named: &[(&str, f64)]) -> (f64, f64) {
(named_or(named, "rise", 0.0), named_or(named, "fall", 0.0))
}
pub(super) fn named_or(named: &[(&str, f64)], key: &str, fallback: f64) -> f64 {
named
.iter()
.find(|(k, _)| *k == key)
.map_or(fallback, |(_, v)| *v)
}
fn positional_values(
low: &Lowering,
args: &[Arg],
cx: Cx,
chosen: &mut Vec<Chosen>,
) -> Option<Vec<f64>> {
args.iter()
.filter_map(|a| match a {
Arg::Pos(x) => Some(low.chosen_value(x, cx, chosen)),
Arg::Named(..) => None,
})
.collect()
}
fn written_named(named: &[(String, f64)]) -> Vec<Argument> {
named
.iter()
.map(|(name, value)| Argument {
name: name.clone(),
value: *value,
written: true,
})
.collect()
}
fn positional_named(name: &str, numbers: &[f64], named: &[(String, f64)]) -> Vec<Argument> {
let params = crate::overload::signature(name).map_or(&[][..], |s| s.params);
let positional = params.iter().zip(numbers).map(|(p, value)| Argument {
name: p.name.to_string(),
value: *value,
written: true,
});
positional.chain(written_named(named)).collect()
}