use std::collections::HashSet;
use sva_ast::{Arg, BinOp, ByteSpan, Expr, Literal, render_expr};
use crate::error::{Diagnostic, EngineError, Located};
use crate::instantiate::{Cx, Instances, Node, RELEASE, ScopeId};
use crate::loops::{Shift, shift_of};
use crate::typing::Typing;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Use {
Free,
Bare,
Causal,
Gated { never: bool },
}
#[derive(Debug, Default)]
pub(crate) struct Never {
zero: HashSet<(usize, ScopeId)>,
}
impl Never {
pub(crate) fn holds(&self, e: &Expr, cx: Cx) -> bool {
self.zero.contains(&key(e, cx))
}
}
fn key(e: &Expr, cx: Cx) -> (usize, ScopeId) {
(std::ptr::from_ref(e) as usize, cx.scope)
}
struct Offense {
term: String,
span: Option<ByteSpan>,
why: &'static str,
}
pub(crate) fn check(
inst: &Instances,
typing: &Typing,
path: &str,
e: &Expr,
cx: Cx,
) -> Result<(Use, Never), EngineError> {
let mut walk = Walk {
inst,
typing,
never: Never::default(),
};
match walk.term(e, cx) {
Ok(Use::Bare) => Err(refusal(
path,
Offense {
term: RELEASE.to_string(),
span: None,
why: "is the whole node, so every sample reads it",
},
)),
Ok(found) => Ok((found, walk.never)),
Err(offense) => Err(refusal(path, offense)),
}
}
fn refusal(path: &str, offense: Offense) -> EngineError {
EngineError::refused(Diagnostic {
code: "engine.release_not_causal".to_string(),
message: format!(
"`{}` {}, so a sample before release would depend on when it comes.",
offense.term, offense.why
),
location: Located::at(path, offense.span),
help: "read release only as a crop's end with no fall, as the start of a crop the \
term lives in, or through a past read of such a term"
.to_string(),
})
}
struct Walk<'a, 'g> {
inst: &'a Instances<'g>,
typing: &'a Typing,
never: Never,
}
impl Walk<'_, '_> {
fn term(&mut self, e: &Expr, cx: Cx) -> Result<Use, Offense> {
if matches!(e, Expr::Var(name) if name == RELEASE) {
return Ok(Use::Bare);
}
if let Expr::Call { name, args, span } = e
&& self.inst.binds(cx.scope, name).is_some()
{
for a in args {
let (Arg::Pos(x) | Arg::Named(_, x)) = a;
if self.term(x, cx)? != Use::Free {
return Err(offense(
e,
Some(*span),
"reads a parameter at a time set by release",
));
}
}
}
let inst = self.inst;
if let Some(found) = inst.follow(e, cx, |e2, cx2| self.term(e2, cx2)) {
return found;
}
let found = match inst.node(e, cx) {
Node::Lit(_) | Node::Name(_) => Ok(Use::Free),
Node::Bin(op, l, r) => self.binary(e, op, l, r, cx),
Node::Call { name, args, span } => self.call(e, name, args, span, cx),
Node::Read { path, arg, span } => self.read(e, path, arg, span, cx),
Node::Own { arg, span } => match self.term(arg, cx)? {
Use::Free => Ok(Use::Free),
_ => Err(offense(
e,
Some(span),
"reads the node's own output at a time set by release",
)),
},
};
if let Ok(Use::Gated { never: true }) = found {
self.never.zero.insert(key(e, cx));
}
found
}
fn binary(&mut self, e: &Expr, op: BinOp, l: &Expr, r: &Expr, cx: Cx) -> Result<Use, Offense> {
let (left, right) = (self.term(l, cx)?, self.term(r, cx)?);
let bare = || Err(offense(e, None, "does arithmetic on release itself"));
match (op, left, right) {
(BinOp::Mul, Use::Gated { never: a }, Use::Gated { never: b }) => {
Ok(Use::Gated { never: a || b })
}
(BinOp::Mul, Use::Gated { never }, _) | (BinOp::Mul, _, Use::Gated { never }) => {
Ok(Use::Gated { never })
}
(BinOp::Div, Use::Gated { never }, Use::Free | Use::Causal) => Ok(Use::Gated { never }),
(_, Use::Bare, _) | (_, _, Use::Bare) => bare(),
(BinOp::Div, _, Use::Gated { .. }) => Err(offense(
e,
None,
"divides by a term that is zero before release",
)),
(BinOp::Add | BinOp::Sub, Use::Gated { never: a }, Use::Gated { never: b }) => {
Ok(Use::Gated { never: a && b })
}
(_, Use::Free, Use::Free) => Ok(Use::Free),
_ => Ok(Use::Causal),
}
}
fn call(
&mut self,
e: &Expr,
name: &str,
args: &[Arg],
span: ByteSpan,
cx: Cx,
) -> Result<Use, Offense> {
let positional: Vec<&Expr> = args
.iter()
.filter_map(|a| match a {
Arg::Pos(x) => Some(x),
Arg::Named(..) => None,
})
.collect();
if let ("crop", [of, start, end]) = (name, positional.as_slice()) {
return self.crop(e, [of, start, end], args, span, cx);
}
let mut held = Use::Free;
for a in args {
let (Arg::Pos(x) | Arg::Named(_, x)) = a;
held = match self.term(x, cx)? {
Use::Bare if matches!(a, Arg::Named(key, _) if key == RELEASE && lands(name)) => {
Use::Causal
}
Use::Bare => {
return Err(offense(
e,
Some(span),
"hands release to a builtin as a number",
));
}
Use::Free => held,
_ => Use::Causal,
};
}
if held != Use::Free && WHOLE_SIGNAL.contains(&name) {
return Err(offense(
e,
Some(span),
"transforms the whole signal, the part after release included",
));
}
Ok(held)
}
fn crop(
&mut self,
e: &Expr,
[of, start, end]: [&Expr; 3],
args: &[Arg],
span: ByteSpan,
cx: Cx,
) -> Result<Use, Offense> {
let (start_use, end_use) = (self.term(start, cx)?, self.term(end, cx)?);
for a in args {
let Arg::Named(key, x) = a else {
continue;
};
if self.term(x, cx)? != Use::Free {
return Err(offense(e, Some(span), "shapes a shoulder by release"));
}
if key == "fall" && end_use == Use::Bare && !zero(x) {
return Err(offense(
e,
Some(span),
"fades out into release, and the fade begins before it",
));
}
}
match (start_use, end_use) {
(Use::Bare, Use::Free | Use::Bare) => Ok(Use::Gated {
never: never(self.inst, cx.scope),
}),
(Use::Free, Use::Bare) => match self.term(of, cx)? {
Use::Bare => Err(offense(e, Some(span), "crops release itself")),
_ => Ok(Use::Causal),
},
(Use::Free, Use::Free) => match self.term(of, cx)? {
Use::Bare => Err(offense(e, Some(span), "crops release itself")),
inner => Ok(inner),
},
_ => Err(offense(
e,
Some(span),
"places a window edge by a term that reads release",
)),
}
}
fn read(
&mut self,
e: &Expr,
path: &str,
arg: &Expr,
span: ByteSpan,
cx: Cx,
) -> Result<Use, Offense> {
if self.term(arg, cx)? != Use::Free {
return Err(offense(
e,
Some(span),
"reads a node at a time set by release",
));
}
let held = self.typing.release_use(path);
if held == Use::Free {
return Ok(Use::Free);
}
match shift_of(self.inst, arg, cx) {
Some(Shift::Now) => Ok(held),
Some(Shift::Secs(by)) if by >= 0.0 => Ok(held),
Some(Shift::Steps(by)) if by >= 0.0 => Ok(held),
_ => Err(offense(
e,
Some(span),
"reads a node that uses release ahead of its own time",
)),
}
}
}
pub(crate) fn never(inst: &Instances, scope: ScopeId) -> bool {
let mut scope = scope;
loop {
match inst.binds(scope, RELEASE) {
None => return true,
Some(bound) => match bound.expr {
Expr::Var(name) if name == RELEASE => scope = bound.scope,
_ => return false,
},
}
}
}
fn lands(name: &str) -> bool {
name == "chaigne_askenfelt"
}
const WHOLE_SIGNAL: [&str; 4] = ["fourier", "ifourier", "stft", "istft"];
fn zero(e: &Expr) -> bool {
matches!(e, Expr::Lit(Literal::Num(v)) if *v == 0.0)
}
fn offense(e: &Expr, span: Option<ByteSpan>, why: &'static str) -> Offense {
Offense {
term: render_expr(e),
span,
why,
}
}