Skip to main content

sva_engine/
schedule.rs

1// Concern: orders nodes dependencies-first and picks which a reading materializes | Non-concern: what a held node contains (render.rs), reading one (refs.rs) | IO: (Instances, asks) -> Schedule
2
3use std::collections::{BTreeSet, HashMap};
4
5use sva_formula::{Held, NodeId, Var};
6
7use crate::cast::Cast;
8use crate::error::EngineError;
9use crate::instantiate::Instances;
10use crate::query::Ask;
11use crate::typing::{Typing, Value, When};
12
13/// One dependencies-first walk: the groups, and which are loops.
14pub struct Order<'i> {
15    pub groups: Vec<Vec<String>>,
16    inst: &'i Instances,
17}
18
19impl<'i> Order<'i> {
20    pub fn deps(&self, path: &str) -> &'i [String] {
21        self.inst.deps(path)
22    }
23
24    /// A group of one not reading itself is no loop; any other is.
25    pub fn is_loop(&self, group: &[String]) -> bool {
26        is_loop(self.inst, group)
27    }
28}
29
30pub(crate) fn is_loop(inst: &Instances, group: &[String]) -> bool {
31    match group {
32        [only] => inst.deps(only).iter().any(|d| d == only),
33        _ => true,
34    }
35}
36
37struct Frame {
38    node: String,
39    refs: Vec<String>,
40    idx: usize,
41}
42
43/// A node two roots reach is grouped once, whichever reached it first.
44pub fn schedule_from<'i>(inst: &'i Instances, roots: &[String]) -> Result<Order<'i>, EngineError> {
45    let mut walk = Walk {
46        inst,
47        within: &|_| true,
48        index: HashMap::new(),
49        low: HashMap::new(),
50        open: Vec::new(),
51        next: 0,
52        groups: Vec::new(),
53    };
54    for root in roots {
55        if !inst.holds(root) {
56            return Err(EngineError::UnknownNode(root.to_string()));
57        }
58        walk.from(root);
59    }
60    Ok(Order {
61        groups: walk.groups,
62        inst,
63    })
64}
65
66/// The groups of `region`, dependencies first.
67pub(crate) fn grouped(
68    inst: &Instances,
69    starts: &[String],
70    region: &dyn Fn(&str) -> bool,
71) -> Vec<Vec<String>> {
72    let mut walk = Walk {
73        inst,
74        within: region,
75        index: HashMap::new(),
76        low: HashMap::new(),
77        open: Vec::new(),
78        next: 0,
79        groups: Vec::new(),
80    };
81    for start in starts.iter().filter(|s| region(s)) {
82        walk.from(start);
83    }
84    walk.groups
85}
86
87struct Walk<'a> {
88    inst: &'a Instances,
89    within: &'a dyn Fn(&str) -> bool,
90    index: HashMap<String, usize>,
91    low: HashMap<String, usize>,
92    open: Vec<String>,
93    next: usize,
94    groups: Vec<Vec<String>>,
95}
96
97impl Walk<'_> {
98    fn reads(&self, node: &str) -> Vec<String> {
99        let reads = self.inst.deps(node).iter();
100        reads.filter(|read| (self.within)(read)).cloned().collect()
101    }
102
103    fn from(&mut self, root: &str) {
104        if self.index.contains_key(root) {
105            return;
106        }
107        self.index.insert(root.to_string(), self.next);
108        self.low.insert(root.to_string(), self.next);
109        self.next += 1;
110        self.open.push(root.to_string());
111        let seed = self.reads(root);
112        let mut stack = vec![Frame {
113            node: root.to_string(),
114            refs: seed,
115            idx: 0,
116        }];
117
118        while let Some(frame) = stack.last_mut() {
119            if frame.idx < frame.refs.len() {
120                let target = frame.refs[frame.idx].clone();
121                let node = frame.node.clone();
122                frame.idx += 1;
123                match self.index.get(&target).copied() {
124                    None => {
125                        self.index.insert(target.clone(), self.next);
126                        self.low.insert(target.clone(), self.next);
127                        self.next += 1;
128                        self.open.push(target.clone());
129                        let refs = self.reads(&target);
130                        stack.push(Frame {
131                            node: target,
132                            refs,
133                            idx: 0,
134                        });
135                    }
136                    Some(at) if self.open.contains(&target) => {
137                        let mine = self.low[&node];
138                        self.low.insert(node, mine.min(at));
139                    }
140                    Some(_) => {}
141                }
142                continue;
143            }
144
145            let node = frame.node.clone();
146            let mine = self.low[&node];
147            stack.pop();
148            if let Some(parent) = stack.last() {
149                let above = self.low[&parent.node];
150                self.low.insert(parent.node.clone(), above.min(mine));
151            }
152            if mine == self.index[&node] {
153                let at = self
154                    .open
155                    .iter()
156                    .rposition(|n| *n == node)
157                    .expect("a root of its group is still open");
158                let mut group = self.open.split_off(at);
159                group.sort();
160                self.groups.push(group);
161            }
162        }
163    }
164}
165
166/// What a render has to hold, and what it can leave as a closed form.
167#[derive(Clone, Debug, Default, PartialEq)]
168pub struct Schedule {
169    /// What a reading holds for itself, the root among them.
170    pub wanted: Vec<NodeId>,
171    /// The nodes a reading asked a closed form of, none already held.
172    pub compose: Vec<NodeId>,
173}
174
175/// A closed form is held as samples only under a buffer reading, a ledger, or the root.
176pub fn plan(typing: &Typing, root: NodeId, asks: &[Ask]) -> Schedule {
177    let mut wanted: BTreeSet<NodeId> = BTreeSet::new();
178    let mut compose: Vec<NodeId> = Vec::new();
179    let audio = asks.is_empty();
180    for ask in asks {
181        let Some(id) = typing.id(&ask.node) else {
182            continue;
183        };
184        // FORMAT 14.2: `bindings` and `arguments` are structural and `flops` counts; none composes.
185        if matches!(
186            ask.representation,
187            crate::query::Representation::Bindings
188                | crate::query::Representation::Arguments
189                | crate::query::Representation::Flops
190        ) {
191            continue;
192        }
193        match ask.representation.consumes(typing.ty(id).is_closed_form()) {
194            sva_samples::Consumes::ClosedForm if !compose.contains(&id) => compose.push(id),
195            sva_samples::Consumes::ClosedForm => {}
196            _ => {
197                wanted.insert(id);
198            }
199        }
200        if let crate::query::Representation::Ledger { depth } = ask.representation {
201            attributed(typing, id, depth, &mut wanted);
202        }
203    }
204    if audio || !wanted.is_empty() {
205        wanted.insert(root);
206    }
207    compose.retain(|id| !wanted.contains(id));
208    Schedule {
209        wanted: wanted.into_iter().collect(),
210        compose,
211    }
212}
213
214/// A ledger names every ref under its target, so each is a buffer of its own.
215fn attributed(typing: &Typing, id: NodeId, depth: usize, wanted: &mut BTreeSet<NodeId>) {
216    // Level by level, as the reading walks: a node is held at its shortest chain's depth.
217    let mut seen = BTreeSet::from([id]);
218    let mut level = vec![id];
219    for _ in 0..depth {
220        let mut next = Vec::new();
221        while let Some(held) = level.pop() {
222            for operand in read_operands(typing, held) {
223                if !seen.insert(operand) {
224                    continue;
225                }
226                wanted.insert(operand);
227                match typing.name(operand) == typing.name(held) {
228                    true => level.push(operand),
229                    false => next.push(operand),
230                }
231            }
232        }
233        level = next;
234    }
235}
236
237/// A node reading its own output is one program, whatever its width: a component on its own
238/// would read the loop at its own width.
239pub(crate) fn holds_self(typing: &Typing, id: NodeId, seen: &mut BTreeSet<NodeId>) -> bool {
240    if !seen.insert(id) {
241        return false;
242    }
243    match typing.value(id) {
244        Value::SelfAt { .. } => true,
245        Value::Cast(Cast::Sample, _) | Value::Read { .. } => false,
246        Value::Cast(_, source) => holds_self(typing, *source, seen),
247        Value::Op { args, .. } => args.iter().any(|a| holds_self(typing, *a, seen)),
248        Value::Filter {
249            x, cutoff, q, gain, ..
250        } => [x, cutoff, q, gain]
251            .into_iter()
252            .any(|operand| holds_self(typing, *operand, seen)),
253        Value::Solver { varying, .. } => varying.iter().any(|(_, a)| holds_self(typing, *a, seen)),
254        Value::ClosedForm(_) | Value::Noise(_) | Value::Stored(_) => false,
255    }
256}
257
258/// Which nodes under `id` a render has to hold before it can hold `id` itself: the buffers its
259/// program reads, an operation, filter or read under it being part of that program.
260pub(crate) fn materialized_operands(typing: &Typing, id: NodeId) -> Vec<NodeId> {
261    let sampled = |set: Vec<NodeId>| -> Vec<NodeId> {
262        let mut out = Vec::new();
263        for op in set {
264            if typing.ty(op).is_closed_form() {
265                continue;
266            }
267            let inlined = matches!(
268                typing.value(op),
269                Value::Op { .. } | Value::Filter { .. } | Value::Read { .. }
270            );
271            match inlined || holds_self(typing, op, &mut BTreeSet::new()) {
272                true => out.extend(materialized_operands(typing, op)),
273                false => out.push(op),
274            }
275        }
276        out
277    };
278    match typing.value(id) {
279        Value::ClosedForm(_) | Value::Noise(_) | Value::Stored(_) => Vec::new(),
280        Value::SelfAt { at, .. } => sampled(at.moving()),
281        Value::Solver { varying, .. } => sampled(varying.iter().map(|(_, a)| *a).collect()),
282        Value::Cast(Cast::Sample, source) => vec![*source],
283        Value::Read { source, at, .. } => {
284            let mut out = match (at, anywhere(typing, *source)) {
285                (When::Moving(_) | When::Step(_), true) => Vec::new(),
286                _ => vec![*source],
287            };
288            out.extend(sampled(at.moving()));
289            out
290        }
291        Value::Cast(_, source) => sampled(vec![*source]),
292        Value::Op { args, .. } => sampled(args.clone()),
293        Value::Filter {
294            x, cutoff, q, gain, ..
295        } => sampled(vec![*x, *cutoff, *q, *gain]),
296    }
297}
298
299/// Whether a read of `id` has a value at any instant, stored or not.
300pub(crate) fn anywhere(typing: &Typing, id: NodeId) -> bool {
301    match typing.value(id) {
302        Value::Noise(_) => true,
303        Value::Cast(Cast::Sample, of) => typing.ty(*of).held == Held::Form(Var::T),
304        Value::ClosedForm(form) => form.var == Var::T,
305        _ => false,
306    }
307}
308
309/// Every ref one node reads.
310pub(crate) fn read_operands(typing: &Typing, id: NodeId) -> Vec<NodeId> {
311    match typing.value(id) {
312        Value::ClosedForm(form) => crate::refs::nodes_in(&form.body),
313        _ if typing.ty(id).is_closed_form() => {
314            let mut out = Vec::new();
315            reads_under(typing, id, &mut BTreeSet::new(), &mut out);
316            out.dedup();
317            out
318        }
319        _ => materialized_operands(typing, id),
320    }
321}
322
323/// A closed-form-typed node that holds no written one still reads refs through its operands.
324fn reads_under(typing: &Typing, id: NodeId, seen: &mut BTreeSet<NodeId>, out: &mut Vec<NodeId>) {
325    if !seen.insert(id) {
326        return;
327    }
328    match typing.value(id) {
329        Value::ClosedForm(form) => out.extend(crate::refs::nodes_in(&form.body)),
330        Value::Read { source, .. } => out.push(*source),
331        Value::Cast(_, source) => read_through(typing, *source, seen, out),
332        Value::Op { args, .. } => {
333            for arg in args {
334                read_through(typing, *arg, seen, out);
335            }
336        }
337        Value::Filter {
338            x, cutoff, q, gain, ..
339        } => {
340            for operand in [x, cutoff, q, gain] {
341                read_through(typing, *operand, seen, out);
342            }
343        }
344        Value::Solver { varying, .. } => {
345            for (_, arg) in varying {
346                read_through(typing, *arg, seen, out);
347            }
348        }
349        Value::SelfAt { .. } | Value::Noise(_) | Value::Stored(_) => {}
350    }
351}
352
353/// A written form under an operand is the term read, whatever transform sits between.
354fn read_through(typing: &Typing, id: NodeId, seen: &mut BTreeSet<NodeId>, out: &mut Vec<NodeId>) {
355    let Value::ClosedForm(_) = typing.value(id) else {
356        return reads_under(typing, id, seen, out);
357    };
358    if seen.insert(id) {
359        out.push(id);
360    }
361}