use sva_ast::{Arg, ByteSpan, Expr};
use sva_formula::filter::Shape;
use sva_formula::{Body, C64, Codomain, Edge, Fold, Held, Origin, Part, Ty, Unary, Var, hash};
use crate::cast::Cast;
use crate::error::EngineError;
use crate::instantiate::Cx;
use crate::lower::{Lowering, Piece};
use crate::typing::Value;
use crate::vocabulary::SERIES;
impl<'g> Lowering<'_, 'g> {
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);
}
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 named = self.named_values(args, cx);
let view: Vec<(&str, f64)> = named.iter().map(|(k, v)| (k.as_str(), *v)).collect();
if let Some(cast) = Cast::from_name(name, &view) {
return self.cast(cast, args, span, cx, var);
}
if let Some(shape) = Shape::from_name(name) {
return self.filter(shape, args, span, cx, var);
}
if super::physics::MODAL.contains(&name) {
let Some(numbers) = positional_values(self, args, cx) else {
return Err(EngineError::BadArity(name.to_string()));
};
return self.modal(name, &numbers, &view);
}
if name == "noise" {
let Some(numbers) = positional_values(self, args, cx) else {
return Err(EngineError::BadArity(name.to_string()));
};
let [seed, ..] = numbers.as_slice() else {
return Err(EngineError::BadArity(name.to_string()));
};
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),
),
))));
}
if crate::overload::FINITE_DIFFERENCE.contains(&name) {
let Some(numbers) = positional_values(self, args, cx) else {
return Err(EngineError::BadArity(name.to_string()));
};
let [first, ..] = numbers.as_slice() else {
return Err(EngineError::BadArity(name.to_string()));
};
let params = super::solvers::params(name, *first, &view);
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 value = Value::Solver(Box::new(params));
let ty = Ty::discrete(Held::Sampled, Codomain::Real);
return Ok(Piece::Value(self.register(value, ty, var)));
}
let positional: Vec<&Expr> = args
.iter()
.filter_map(|a| match a {
Arg::Pos(x) => Some(x),
Arg::Named(..) => None,
})
.collect();
if name == "rand" {
return self.drawn(&positional, &view, cx, span, var);
}
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 pieces.iter().any(|p| matches!(p, Piece::Value(_))) {
return 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.here(Some(span)));
return match arithmetic(name, &bodies, named, var, origin) {
Some(body) => Ok(Piece::ClosedForm(body)),
None if name == "pow" && bodies.len() == 2 => {
Err(self.non_integer_power(written, span))
}
None => Err(EngineError::BadArity(name.to_string())),
};
}
match name {
"crop" => {
arity(3)?;
let (l, r) = self.window(&bodies[1], &bodies[2], var, name, span)?;
let (rise, fall) = (read("rise", 0.0), read("fall", 0.0));
self.shoulders(rise, fall, r.value() - l.value(), span)?;
let of = self.part(bodies[0].clone(), Some(span));
Ok(Piece::ClosedForm(Body::Crop {
of,
l,
r,
rise,
fall,
}))
}
"delta" => {
arity(1)?;
let at = self.part(bodies[0].clone(), Some(span));
Ok(Piece::ClosedForm(Body::Delta {
at,
order: read("k", 0.0) as u16,
}))
}
"pv" => {
arity(1)?;
let at = self.part(bodies[0].clone(), 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(bodies[0].clone(), Some(span));
Ok(Piece::ClosedForm(Body::Channel(of, index as u8)))
}
other => Err(EngineError::UnknownBuiltin(other.to_string())),
}
}
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 i32, 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, "a", span)?,
self.edge(r, var, name, "b", span)?,
))
}
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);
match 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()
}
fn named_values(&self, args: &[Arg], cx: Cx) -> Vec<(String, f64)> {
args.iter()
.filter_map(|a| match a {
Arg::Named(key, value) => Some((key.clone(), self.named_value(value, cx)?)),
Arg::Pos(_) => None,
})
.collect()
}
pub(super) fn named_value(&self, e: &Expr, cx: Cx) -> Option<f64> {
crate::loops::plain(crate::loops::amount(self.inst, e, cx)?)
}
fn drawn(
&mut self,
positional: &[&Expr],
named: &[(&str, f64)],
cx: Cx,
span: ByteSpan,
var: Var,
) -> Result<Piece, EngineError> {
let (key, seed) = match positional {
[] => {
let seed = named_or(named, "seed", 0.0) as u64;
let drawn = hash::keyed("", seed) as f64 / u64::MAX as f64;
return Ok(Piece::ClosedForm(Body::Const(C64::real(drawn))));
}
[key] => (*key, named_or(named, "seed", 0.0)),
[key, seed, ..] => (
*key,
self.named_value(seed, cx)
.ok_or_else(|| EngineError::BadArity("rand".to_string()))?,
),
};
let seed = seed as u64;
let Piece::ClosedForm(body) = self.walk(key, cx, var)? else {
return Err(self.refused_at(
"type.samples_in_closed_form",
"`rand` reads a key this node already sampled.".to_string(),
"hash a closed form in t, or read the buffer where it is written",
Some(span),
));
};
let body = crate::refs::fold_constants(self.typing, &body);
if let Some(n) = super::constant_value(&body, var) {
return Ok(Piece::ClosedForm(Body::Const(C64::real(hash::draw(
seed, n,
)))));
}
let of = self.part(body, Some(span));
Ok(Piece::ClosedForm(Body::Keyed { seed, of }))
}
}
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: &[Body],
named: &[(&str, f64)],
var: Var,
origin: Origin,
) -> Option<Body> {
let part = |f: Body| Part::new(origin, f);
let unary = |op: Unary, x: &Body| Body::Apply(op, part(x.clone()));
let fold =
|op: Fold, a: &Body, b: &Body| Body::Fold(op, vec![part(a.clone()), part(b.clone())]);
if name == "sat" {
let [x] = bodies else {
return None;
};
let drive = Body::Const(C64::real(named_or(named, "drive", 1.0)));
let driven = Body::Mul(vec![part(x.clone()), part(drive)]);
return Some(Body::Apply(Unary::Sat, part(driven)));
}
if let ([x], Some(op)) = (bodies, Unary::from_name(name)) {
return Some(unary(op, x));
}
Some(match (name, bodies) {
("max", [a, b]) => fold(Fold::Max, a, b),
("min", [a, b]) => fold(Fold::Min, a, b),
("pow", [base, exponent]) => return power(base, exponent, var, origin),
_ => return None,
})
}
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.clone()), 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.clone()), part(rate)]);
Some(Body::Apply(Unary::Exp, part(scaled)))
}
fn whole(n: f64) -> bool {
n.fract() == 0.0 && n.abs() <= f64::from(i32::MAX)
}
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) -> Option<Vec<f64>> {
args.iter()
.filter_map(|a| match a {
Arg::Pos(x) => Some(low.named_value(x, cx)),
Arg::Named(..) => None,
})
.collect()
}