Skip to main content

sva_engine/render/
terms.rs

1// Concern: a stream's note sum, one node per term, those it retired folded into one slot | Non-concern: when a term's support ends (stream.rs), rebuilding nodes | IO: (Expr) -> Handle; (gone) -> ()
2
3use std::sync::atomic::{AtomicU32, Ordering};
4
5use sva_ast::{Address, Arg, BinOp, ByteSpan, Expr, Literal};
6use sva_samples::Extent;
7
8use crate::typing::{SumSlot, Typing};
9
10/// The node a stream defines as the sum of its terms, for its expression to read as `@notes`.
11pub const NOTES: &str = "notes";
12
13#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
14pub struct Handle(pub u32);
15
16impl Handle {
17    pub(super) fn node(self) -> String {
18        format!("{NOTES}#{}", self.0)
19    }
20}
21
22/// Every stream draws from one count, so a handle names one term of one stream.
23static HANDLES: AtomicU32 = AtomicU32::new(0);
24
25#[derive(Clone)]
26struct Term {
27    handle: Handle,
28    expr: Expr,
29    /// False once removed: it plays on, cut there, under no handle.
30    addressed: bool,
31    landed: i64,
32}
33
34impl Term {
35    fn holds(&self, handle: Handle) -> bool {
36        self.addressed && self.handle == handle
37    }
38}
39
40#[derive(Clone, Default)]
41pub(super) struct Terms {
42    terms: Vec<Term>,
43    /// The hull of the supports of every term it retired.
44    retired: Option<Extent>,
45}
46
47impl Terms {
48    pub(super) fn count(&self) -> usize {
49        self.terms.len()
50    }
51
52    pub(super) fn is_empty(&self) -> bool {
53        self.terms.is_empty()
54    }
55
56    pub(super) fn exprs(&self) -> impl Iterator<Item = &Expr> {
57        self.terms.iter().map(|t| &t.expr)
58    }
59
60    pub(super) fn nodes(&self) -> impl Iterator<Item = (String, Expr)> {
61        self.terms.iter().map(|t| (t.handle.node(), t.expr.clone()))
62    }
63
64    pub(super) fn handles(&self) -> impl Iterator<Item = Handle> {
65        self.terms.iter().map(|t| t.handle)
66    }
67
68    pub(super) fn added(&self, expr: Expr) -> (Terms, Handle) {
69        let mut next = self.clone();
70        let handle = Handle(HANDLES.fetch_add(1, Ordering::Relaxed));
71        next.terms.push(Term {
72            handle,
73            expr,
74            addressed: true,
75            landed: 0,
76        });
77        (next, handle)
78    }
79
80    fn held(&mut self, handle: Handle) -> Option<&mut Term> {
81        self.terms.iter_mut().find(|t| t.holds(handle))
82    }
83
84    pub(super) fn landed(&self, handle: Handle) -> Option<i64> {
85        Some(self.terms.iter().find(|t| t.holds(handle))?.landed)
86    }
87
88    pub(super) fn land(&mut self, handle: Handle, at: i64) {
89        if let Some(term) = self.held(handle) {
90            term.landed = at;
91        }
92    }
93
94    /// `None` where the stream no longer holds `handle`; `expr` of the sample its add landed at.
95    pub(super) fn replaced(&self, handle: Handle, expr: impl FnOnce(i64) -> Expr) -> Option<Terms> {
96        let mut next = self.clone();
97        let term = next.held(handle)?;
98        term.expr = expr(term.landed);
99        Some(next)
100    }
101
102    /// The term cropped at `at` seconds on the sum's own clock.
103    pub(super) fn removed(&self, handle: Handle, at: f64) -> Option<Terms> {
104        let mut next = self.clone();
105        let term = next.held(handle)?;
106        let never = Expr::Bin(
107            BinOp::Sub,
108            Box::new(Expr::Lit(Literal::Num(0.0))),
109            Box::new(Expr::Var("inf".to_string())),
110        );
111        term.expr = Expr::Call {
112            name: "crop".to_string(),
113            args: vec![
114                Arg::Pos(term.expr.clone()),
115                Arg::Pos(never),
116                Arg::Pos(Expr::Lit(Literal::Num(at))),
117            ],
118            span: SPAN,
119        };
120        term.addressed = false;
121        Some(next)
122    }
123
124    pub(super) fn sum(&self) -> Expr {
125        self.terms
126            .iter()
127            .map(|t| Expr::Ref {
128                path: t.handle.node(),
129                arg: Box::new(Expr::Var("t".to_string())),
130                binds: Vec::new(),
131                address: Address::Time,
132                span: SPAN,
133            })
134            .reduce(|sum, t| Expr::Bin(BinOp::Add, Box::new(sum), Box::new(t)))
135            .unwrap_or(Expr::Lit(Literal::Num(0.0)))
136    }
137
138    /// `notes` named by its terms, and by the supports of those it retired.
139    pub(super) fn name(&self, tys: &mut Typing) {
140        let Some(notes) = tys.id(NOTES) else {
141            return;
142        };
143        let live = self.terms.iter();
144        let live = live.map(|t| tys.id(&t.handle.node()).map(SumSlot::Node));
145        let retired = self.retired.map(|support| Some(SumSlot::Retired(support)));
146        if let Some(slots) = live.chain(retired).collect::<Option<Vec<_>>>() {
147            tys.name_sum(notes, slots);
148        }
149    }
150
151    /// Drops every term `gone` names, its support, as `support` gives it, kept in the hull of
152    /// those retired. True where any went.
153    pub(super) fn prune(
154        &mut self,
155        gone: &dyn Fn(Handle) -> bool,
156        support: &dyn Fn(Handle) -> Option<Extent>,
157    ) -> bool {
158        let live = self.terms.len();
159        let (went, kept): (Vec<Term>, _) = std::mem::take(&mut self.terms)
160            .into_iter()
161            .partition(|t| gone(t.handle));
162        self.terms = kept;
163        for ended in went.iter().filter_map(|t| support(t.handle)) {
164            self.retired = Some(self.retired.map_or(ended, |hull| hull.hull(ended)));
165        }
166        self.terms.len() != live
167    }
168}
169
170const SPAN: ByteSpan = ByteSpan { start: 0, end: 0 };
171
172/// `expr` with its sample 0 at sample `at` of the stream: every `t` in it read `at` earlier.
173pub(super) fn placed(expr: &Expr, at: i64) -> Expr {
174    let moved = |e: &Expr| Box::new(placed(e, at));
175    match expr {
176        Expr::Var(name) if name == "t" && at != 0 => Expr::Bin(
177            BinOp::Sub,
178            Box::new(expr.clone()),
179            Box::new(Expr::Lit(Literal::Samples(at as f64))),
180        ),
181        Expr::Lit(_) | Expr::Var(_) => expr.clone(),
182        Expr::Bin(op, l, r) => Expr::Bin(*op, moved(l), moved(r)),
183        Expr::Call { name, args, span } => Expr::Call {
184            name: name.clone(),
185            args: args
186                .iter()
187                .map(|arg| match arg {
188                    Arg::Pos(e) => Arg::Pos(placed(e, at)),
189                    Arg::Named(n, e) => Arg::Named(n.clone(), placed(e, at)),
190                })
191                .collect(),
192            span: *span,
193        },
194        Expr::Ref {
195            path,
196            arg,
197            binds,
198            address,
199            span,
200        } => Expr::Ref {
201            path: path.clone(),
202            arg: moved(arg),
203            binds: binds
204                .iter()
205                .map(|(n, e)| (n.clone(), placed(e, at)))
206                .collect(),
207            address: *address,
208            span: *span,
209        },
210        Expr::SelfRef { arg, address, span } => Expr::SelfRef {
211            arg: moved(arg),
212            address: *address,
213            span: *span,
214        },
215        Expr::Indexed { name, arg, span } => Expr::Indexed {
216            name: name.clone(),
217            arg: moved(arg),
218            span: *span,
219        },
220    }
221}
222
223#[cfg(test)]
224mod tests {
225    use super::*;
226
227    /// However many terms came and went, `notes` is named by those sounding and one slot for
228    /// the rest: the hull of their supports.
229    #[test]
230    fn notes_is_named_by_its_sounding_terms_and_one_retired_slot() {
231        let mut terms = Terms::default();
232        let mut last = None;
233        for k in 0..1_000 {
234            let (next, handle) = terms.added(Expr::Lit(Literal::Num(1.0)));
235            terms = next;
236            let ended = |h: Handle| Some(h) == last;
237            assert_eq!(terms.prune(&ended, &|_| Some(Extent::new(k, k + 1))), k > 0);
238            last = Some(handle);
239        }
240        let dir = std::env::temp_dir().join(format!("sva-terms-{}", std::process::id()));
241        std::fs::create_dir_all(&dir).expect("a directory");
242        std::fs::write(dir.join("one"), "1\n").expect("a node file");
243        let mut graph = sva_ast::parse_composition(&dir).expect("a composition");
244        for (name, body) in std::iter::once((NOTES.to_string(), terms.sum())).chain(terms.nodes()) {
245            assert!(graph.define(&name, body));
246        }
247        let mut tys = crate::types(&graph, NOTES).expect("typed");
248        terms.name(&mut tys);
249        let (notes, sounding) = (tys.id(NOTES), last.and_then(|h| tys.id(&h.node())));
250        let slots = notes.and_then(|notes| tys.sum_slots(notes)).expect("named");
251        let sounding = SumSlot::Node(sounding.expect("the last term"));
252        assert_eq!(slots, [sounding, SumSlot::Retired(Extent::new(1, 1_000))]);
253    }
254}