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