use sva_ast::{Arg, BinOp, Expr, Literal};
use sva_formula::closed_form::map_children;
use sva_formula::{Body, C64, IndexId, Part, Series, Var};
use crate::error::{Diagnostic, EngineError, Located};
use crate::instantiate::{Cx, Instances, Node};
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Delay {
Steps(u32),
Secs(f64),
Varying,
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) enum SelfKind {
Series { gain: C64, delay: f64 },
Sampled,
Refuse(Box<EngineError>),
}
pub(crate) fn classify(inst: &Instances, e: &Expr, cx: Cx, at: &str) -> SelfKind {
match read(inst, e, cx, C64::ONE) {
Reading::Refused(tap) => SelfKind::Refuse(Box::new(
tap_refusal(tap, Located::at(at, None)).expect("a refused tap names its reason"),
)),
Reading::Free | Reading::Nonlinear => SelfKind::Sampled,
Reading::Linear {
gain,
delay: Delay::Steps(_),
} if gain.abs() > 1.0 => SelfKind::Refuse(Box::new(unbounded(gain, at))),
Reading::Linear {
delay: Delay::Steps(_) | Delay::Varying,
..
} => SelfKind::Sampled,
Reading::Linear {
gain,
delay: Delay::Secs(secs),
} => match gain.abs() < 1.0 {
true => SelfKind::Series { gain, delay: secs },
false => SelfKind::Refuse(Box::new(unbounded(gain, at))),
},
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) enum Tap {
At(Delay),
Zero,
Forward,
Fractional,
}
pub(crate) fn tap_of(inst: &Instances, arg: &Expr, cx: Cx) -> Tap {
match shift_of(inst, arg, cx) {
None => Tap::At(Delay::Varying),
Some(Shift::Now) => Tap::Zero,
Some(Shift::Secs(secs)) if secs > 0.0 => Tap::At(Delay::Secs(secs)),
Some(Shift::Steps(steps)) if steps > 0.0 && steps.fract() == 0.0 => {
Tap::At(Delay::Steps(steps as u32))
}
Some(Shift::Steps(steps)) if steps > 0.0 => Tap::Fractional,
Some(_) => Tap::Forward,
}
}
pub(crate) fn tap_refusal(tap: Tap, at: Located) -> Option<EngineError> {
let (code, message, help) = match tap {
Tap::At(_) => return None,
Tap::Zero => (
"samples.zero_delay_loop",
"a loop reaches no sample it has already written.",
"write self(t - 1sp) for a one-step loop",
),
Tap::Forward => (
"engine.forward_self_read",
"a loop reads its own output before it is written.",
"write self at an earlier time, as in self(t - 1sp)",
),
Tap::Fractional => (
"ref.fractional_shift_on_samples",
"a read on the grid moves by whole samples.",
"write a whole number of sp, or the delay in seconds",
),
};
Some(EngineError::refused(Diagnostic {
code: code.to_string(),
message: message.to_string(),
location: at,
help: help.to_string(),
}))
}
fn unbounded(gain: C64, at: &str) -> EngineError {
EngineError::refused(Diagnostic {
code: "type.self_gain_unbounded".to_string(),
message: format!("loop gain {} does not settle.", gain.abs()),
location: Located::at(at, None),
help: "write self(t - 1sp) for a sampled loop".to_string(),
})
}
enum Reading {
Free,
Linear { gain: C64, delay: Delay },
Refused(Tap),
Nonlinear,
}
fn read(inst: &Instances, e: &Expr, cx: Cx, gain: C64) -> Reading {
if let Some(r) = inst.follow(e, cx, |e2, cx2| read(inst, e2, cx2, gain)) {
return r;
}
match inst.node(e, cx) {
Node::Own { arg, .. } => match tap_of(inst, arg, cx) {
Tap::At(delay) => Reading::Linear { gain, delay },
refused => Reading::Refused(refused),
},
Node::Bin(op @ (BinOp::Add | BinOp::Sub), l, r) => {
let right = if op == BinOp::Sub { -gain } else { gain };
join(read(inst, l, cx, gain), read(inst, r, cx, right))
}
Node::Bin(BinOp::Mul, l, r) => match (holds(inst, l, cx), holds(inst, r, cx)) {
(false, true) => match constant(inst, l, cx) {
Some(k) => read(inst, r, cx, gain * k),
None => Reading::Nonlinear,
},
(true, false) => match constant(inst, r, cx) {
Some(k) => read(inst, l, cx, gain * k),
None => Reading::Nonlinear,
},
(false, false) => Reading::Free,
(true, true) => Reading::Nonlinear,
},
Node::Bin(BinOp::Div, l, r) => match (holds(inst, l, cx), holds(inst, r, cx)) {
(true, false) => match constant(inst, r, cx) {
Some(k) if !k.is_zero() => read(inst, l, cx, gain / k),
_ => Reading::Nonlinear,
},
(false, false) => Reading::Free,
_ => Reading::Nonlinear,
},
other => match holds_in(inst, &other, cx) {
true => Reading::Nonlinear,
false => Reading::Free,
},
}
}
fn join(a: Reading, b: Reading) -> Reading {
match (a, b) {
(Reading::Refused(tap), _) | (_, Reading::Refused(tap)) => Reading::Refused(tap),
(Reading::Nonlinear, _) | (_, Reading::Nonlinear) => Reading::Nonlinear,
(Reading::Free, other) | (other, Reading::Free) => other,
(
Reading::Linear {
gain: g1,
delay: d1,
},
Reading::Linear {
gain: g2,
delay: d2,
},
) if d1 == d2 => Reading::Linear {
gain: g1 + g2,
delay: d1,
},
_ => Reading::Nonlinear,
}
}
fn holds(inst: &Instances, e: &Expr, cx: Cx) -> bool {
holds_in(inst, &inst.node(e, cx), cx) || inst.holds_self(e, cx)
}
fn holds_in(inst: &Instances, node: &Node, cx: Cx) -> bool {
match node {
Node::Own { .. } => true,
Node::Read { arg, .. } => inst.holds_self(arg, cx),
Node::Call { args, .. } => args.iter().any(|a| {
let (sva_ast::Arg::Pos(x) | sva_ast::Arg::Named(_, x)) = a;
inst.holds_self(x, cx)
}),
Node::Bin(_, l, r) => inst.holds_self(l, cx) || inst.holds_self(r, cx),
Node::Lit(_) | Node::Name(_) => false,
}
}
fn constant(inst: &Instances, e: &Expr, cx: Cx) -> Option<C64> {
plain(amount(inst, e, cx)?).map(C64::real)
}
pub(crate) fn neumann(rest: &Body, gain: C64, delay: f64, index: IndexId) -> Body {
let log = C64::new(gain.abs().ln(), gain.im.atan2(gain.re));
let power = Body::Apply(
sva_formula::Unary::Exp,
Part::bare(Body::Mul(vec![
Part::bare(Body::Index(index)),
Part::bare(Body::Const(log)),
])),
);
let addends: Vec<&Body> = match rest {
Body::Add(parts) => parts.iter().map(|p| &*p.body).collect(),
other => vec![other],
};
let scaled: Vec<Part> = addends
.into_iter()
.map(|addend| {
Part::bare(Body::Mul(vec![
Part::bare(power.clone()),
Part::bare(moved(addend, index, delay)),
]))
})
.collect();
let term = match scaled.as_slice() {
[only] => (*only.body).clone(),
_ => Body::Add(scaled),
};
Body::Series(Box::new(Series {
index,
lo: 0,
hi: sva_formula::Bound::Infinite,
term: Part::bare(term),
}))
}
fn moved(f: &Body, index: IndexId, delay: f64) -> Body {
match f {
Body::Line => Body::Add(vec![
Part::bare(Body::Line),
Part::bare(Body::Mul(vec![
Part::bare(Body::Const(C64::real(-delay))),
Part::bare(Body::Index(index)),
])),
]),
other => map_children(other, |p| Part::new(p.origin, moved(&p.body, index, delay))),
}
}
pub(crate) fn expandable(
f: &Body,
var: Var,
of: &dyn Fn(sva_formula::NodeId) -> Option<(Body, Var)>,
) -> Option<Body> {
match f {
Body::Node(id) => {
let (body, held) = of(*id)?;
match held == var {
true => expandable(&body, var, of),
false => None,
}
}
other => {
let mut ok = true;
let out = map_children(other, |p| match expandable(&p.body, var, of) {
Some(body) => Part::new(p.origin, body),
None => {
ok = false;
p.clone()
}
});
ok.then_some(out)
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Shift {
Now,
Secs(f64),
Steps(f64),
}
pub fn shift_of(inst: &Instances, e: &Expr, cx: Cx) -> Option<Shift> {
let (time, secs, steps) = walk(inst, e, cx, 1.0)?;
if !time {
return None;
}
match (secs, steps) {
(0.0, 0.0) => Some(Shift::Now),
(secs, 0.0) => Some(Shift::Secs(-secs)),
(0.0, steps) => Some(Shift::Steps(-steps)),
_ => None,
}
}
fn walk(inst: &Instances, e: &Expr, cx: Cx, sign: f64) -> Option<(bool, f64, f64)> {
if let Some(r) = inst.follow(e, cx, |e2, cx2| walk(inst, e2, cx2, sign)) {
return r;
}
match inst.node(e, cx) {
Node::Name("t") => Some((true, 0.0, 0.0)),
Node::Bin(op @ (BinOp::Add | BinOp::Sub), l, r) => {
let (lt, ls, lg) = walk(inst, l, cx, sign)?;
let flip = if op == BinOp::Sub { -sign } else { sign };
let (rt, rs, rg) = walk(inst, r, cx, flip)?;
Some((lt || rt, ls + rs, lg + rg))
}
_ => {
let (secs, steps) = amount(inst, e, cx)?;
Some((false, sign * secs, sign * steps))
}
}
}
pub(crate) fn amount(inst: &Instances, e: &Expr, cx: Cx) -> Option<(f64, f64)> {
if let Some(r) = inst.follow(e, cx, |e2, cx2| amount(inst, e2, cx2)) {
return r;
}
match inst.node(e, cx) {
Node::Lit(Literal::Num(n)) => Some((*n, 0.0)),
Node::Lit(Literal::Samples(n)) => Some((0.0, *n)),
Node::Name("pi") => Some((std::f64::consts::PI, 0.0)),
Node::Name(other) => sva_formula::note::frequency(other).map(|hz| (hz, 0.0)),
Node::Bin(op, l, r) => {
let (a, b) = (amount(inst, l, cx)?, amount(inst, r, cx)?);
Some(match op {
BinOp::Add => (a.0 + b.0, a.1 + b.1),
BinOp::Sub => (a.0 - b.0, a.1 - b.1),
BinOp::Mul => scaled(a, b)?,
BinOp::Div => {
let by = plain(b)?;
(a.0 / by, a.1 / by)
}
BinOp::Mod => (crate::lower::constant_modulo(plain(a)?, plain(b)?)?, 0.0),
})
}
Node::Call { name, args, .. } => called(inst, name, args, cx),
Node::Read { path, arg, .. } if inst.is_now(arg, cx) => {
let (body, held) = inst.at(path)?;
amount(inst, body, held)
}
_ => None,
}
}
fn scaled(a: (f64, f64), b: (f64, f64)) -> Option<(f64, f64)> {
match (plain(a), plain(b)) {
(Some(k), _) => Some((k * b.0, k * b.1)),
(_, Some(k)) => Some((k * a.0, k * a.1)),
_ => None,
}
}
fn called(inst: &Instances, name: &str, args: &[Arg], cx: Cx) -> Option<(f64, f64)> {
let mut positional = Vec::new();
let mut named = Vec::new();
for arg in args {
match arg {
Arg::Pos(x) => positional.push(plain(amount(inst, x, cx)?)?),
Arg::Named(key, x) => named.push((key.as_str(), plain(amount(inst, x, cx)?)?)),
}
}
crate::lower::constant_call(name, &positional, &named).map(|n| (n, 0.0))
}
pub(crate) fn plain(amount: (f64, f64)) -> Option<f64> {
(amount.1 == 0.0 && amount.0.is_finite()).then_some(amount.0)
}