onedrop_eval/
compiled_block.rs

1//! Compiled block: bundles a [`Vec<Node>`] (evalexpr fallback) with an
2//! optional [`CompiledBytecode`] lowering.
3//!
4//! Most preset hot loops (per-frame eqs, per-vertex warp eqs, custom-shape
5//! per-frame, custom-wave per-frame / per_frame_init, …) want the same
6//! "prefer bytecode VM, fall back to evalexpr Node walk" branching that
7//! [`onedrop-engine::engine::wave_phase`] already wires by hand for the
8//! per_point hot path. This newtype hoists that pattern into the eval
9//! crate so every call site picks it up uniformly.
10//!
11//! - `from_nodes(Vec<Node>)` — try-compiles the bytecode lazily; on
12//!   failure (an op the VM doesn't support: `rand`, `gmegabuf`, …) the
13//!   block keeps the Nodes and runtime falls back transparently.
14//! - `run(&mut MilkContext)` — single entry point that picks the
15//!   bytecode path when available. The bytecode path is infallible by
16//!   construction (`try_compile` is the only failure point); the
17//!   fallback discards per-equation errors silently.
18//! - `nodes()` / `bytecode()` — exposed for call sites that still want
19//!   per-equation error logging or special handling (sequential vs
20//!   parallel-samples in [`onedrop-engine::engine::wave_phase`]).
21
22use crate::bytecode::CompiledBytecode;
23use crate::context::MilkContext;
24use evalexpr::Node;
25
26/// A pre-compiled block of expressions ready to execute against a
27/// [`MilkContext`]. See module docs.
28#[derive(Debug, Clone)]
29pub struct CompiledBlock {
30    nodes: Vec<Node>,
31    bytecode: Option<CompiledBytecode>,
32}
33
34impl CompiledBlock {
35    /// Take ownership of `nodes` and try to lower them to bytecode
36    /// against `ctx`. Cold-variable names referenced by the block are
37    /// interned into `ctx.cold` so the emitted opcodes carry slab
38    /// indices.
39    ///
40    /// On lowering failure (any unsupported operator/function) the
41    /// block stores `bytecode: None` and the runtime falls back to
42    /// evalexpr's tree-walking interpreter — same observable
43    /// behaviour, just slower.
44    pub fn from_nodes(nodes: Vec<Node>, ctx: &mut MilkContext) -> Self {
45        let bytecode = if nodes.is_empty() {
46            None
47        } else {
48            CompiledBytecode::try_compile(&nodes, ctx).ok()
49        };
50        Self { nodes, bytecode }
51    }
52
53    /// Empty block — `run` is a no-op.
54    pub fn empty() -> Self {
55        Self {
56            nodes: Vec::new(),
57            bytecode: None,
58        }
59    }
60
61    /// Underlying evalexpr nodes. Always populated, even when bytecode
62    /// is also available (kept around as the fallback path for the few
63    /// runtime decisions that need per-node access — e.g. per-equation
64    /// error logging).
65    pub fn nodes(&self) -> &[Node] {
66        &self.nodes
67    }
68
69    /// Bytecode lowering when available. `None` means at least one
70    /// node uses an op the VM doesn't support; runtime falls back to
71    /// the Nodes path.
72    pub fn bytecode(&self) -> Option<&CompiledBytecode> {
73        self.bytecode.as_ref()
74    }
75
76    pub fn is_empty(&self) -> bool {
77        self.nodes.is_empty()
78    }
79
80    pub fn len(&self) -> usize {
81        self.nodes.len()
82    }
83
84    /// Execute the block against `ctx`. Prefers the bytecode VM when
85    /// available; otherwise iterates the evalexpr nodes and discards
86    /// per-equation errors silently. Call sites that want to log
87    /// per-equation failures should use [`CompiledBlock::nodes`] +
88    /// [`CompiledBlock::bytecode`] directly.
89    pub fn run(&self, ctx: &mut MilkContext) {
90        if let Some(bc) = &self.bytecode {
91            bc.run(ctx);
92        } else {
93            for node in &self.nodes {
94                let _ = node.eval_with_context_mut(ctx);
95            }
96        }
97    }
98}
99
100impl Default for CompiledBlock {
101    fn default() -> Self {
102        Self::empty()
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109    use crate::MilkEvaluator;
110
111    fn compile(eqs: &[&str]) -> CompiledBlock {
112        let mut ev = MilkEvaluator::new();
113        let owned: Vec<String> = eqs.iter().map(|s| s.to_string()).collect();
114        let nodes = ev.compile_batch(&owned).unwrap();
115        CompiledBlock::from_nodes(nodes, ev.context_mut())
116    }
117
118    #[test]
119    fn lowers_to_bytecode_when_supported() {
120        let block = compile(&["x = 0.5", "y = x * 2.0"]);
121        assert!(
122            block.bytecode().is_some(),
123            "simple arithmetic must lower to bytecode"
124        );
125        assert_eq!(block.len(), 2);
126    }
127
128    #[test]
129    fn falls_back_to_nodes_on_unsupported_op() {
130        // `lerp` has no bytecode opcode and isn't a registered evalexpr
131        // builtin either — the compile-fallback path keeps the block on
132        // the Node walker so the renderer doesn't crash on it.
133        let block = compile(&["x = lerp(0, 1, 0.5)"]);
134        assert!(
135            block.bytecode().is_none(),
136            "lerp() must keep block on Node path"
137        );
138        assert_eq!(block.len(), 1);
139    }
140
141    #[test]
142    fn empty_run_is_noop() {
143        let block = CompiledBlock::empty();
144        let mut ctx = MilkContext::new();
145        block.run(&mut ctx); // no panic
146        assert!(block.is_empty());
147        assert_eq!(block.len(), 0);
148    }
149
150    #[test]
151    fn run_bytecode_and_nodes_paths_match() {
152        // Bytecode-lowerable block.
153        let bc_block = compile(&["x = 0.25", "y = x * 4.0", "r = sin(x)"]);
154        assert!(bc_block.bytecode().is_some());
155        let mut ctx_bc = MilkContext::new();
156        bc_block.run(&mut ctx_bc);
157
158        // Force the Node fallback: same eqs, but discard the bytecode.
159        let mut nodes_block = bc_block.clone();
160        nodes_block.bytecode = None;
161        let mut ctx_nodes = MilkContext::new();
162        nodes_block.run(&mut ctx_nodes);
163
164        for name in ["x", "y", "r"] {
165            let a = ctx_bc.get(name).unwrap();
166            let b = ctx_nodes.get(name).unwrap();
167            assert!((a - b).abs() < 1e-12, "{name}: bytecode={a} nodes={b}");
168        }
169    }
170}