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