polydat_core/iteration/comprehension/ir/op.rs
1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! Operator IR — spec §9.1.
5//!
6//! Every well-formed comprehension AST compiles to a finite
7//! sequence of these 8 opcodes. Every operator is a stream
8//! transducer; operands flow as tuple streams via
9//! `advance() -> Option<Tuple>`, never as materialized
10//! `Vec<Tuple>`. The two materialization barriers — non-Lex
11//! `ORDER_MATERIALIZE` and `ZIP(Cycle)`'s shorter-child
12//! buffering — are called out explicitly.
13
14use serde::{Deserialize, Serialize};
15
16use crate::iteration::comprehension::source::Source;
17use crate::iteration::comprehension::strategy::{StrategyName, ZipMode};
18
19/// The 8-opcode IR set (spec §9.1).
20#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
21#[serde(tag = "op", rename_all = "snake_case")]
22pub enum Op {
23 /// Push a single-name tuple stream produced by `source`.
24 /// Streaming; O(1) per pull above the source's own state.
25 PushClause {
26 /// The name the stream binds.
27 name: String,
28 /// Where its values come from.
29 source: Source,
30 },
31
32 /// Replace the top-N stream operands with one stream that
33 /// enumerates their cross product in Lex order. Streaming.
34 Cartesian {
35 /// Operands combined.
36 n: usize,
37 },
38
39 /// Replace the top-N stream operands with their lockstep
40 /// diagonal. Streaming under Strict/Truncate; `Cycle`
41 /// buffers each non-longest child.
42 Zip {
43 /// Operands combined.
44 n: usize,
45 /// The length policy.
46 mode: ZipMode,
47 },
48
49 /// Replace the top-N stream operands with a stream that
50 /// concatenates them in operand order. Streaming.
51 Union {
52 /// Operands concatenated.
53 n: usize,
54 },
55
56 /// Wrap the top operand with a per-tuple predicate check.
57 /// Streaming.
58 Filter {
59 /// The predicate, a boolean expression over the tuple.
60 predicate: String,
61 },
62
63 /// Wrap the top operand with a counter / pass-through.
64 /// Used for `order(Lex, _)` per spec §10.2 R1; this is
65 /// the "streaming" order opcode.
66 OrderStreaming {
67 /// The streaming order's kind.
68 kind: OrderStreamingKind,
69 /// The output cap, if any.
70 truncation: Option<u64>,
71 },
72
73 /// MATERIALIZATION BARRIER. Build a working set sufficient
74 /// for the strategy, apply the strategy, emit permuted
75 /// tuples. Truncation is the output cap.
76 ///
77 /// Per spec §10.2 R2: when the input is index-addressable
78 /// and the strategy has a closed-form push-down rule, the
79 /// working set shrinks from O(input) to O(output) — the
80 /// interpreter realizes this by drawing strategy-specific
81 /// multi-indices and looking each up against the input's
82 /// `IndexFn` rather than materializing the full input.
83 /// Whether R2 fires is encoded in `indexed`.
84 ///
85 /// `input_index_fn` carries the upstream comprehension's
86 /// addressing scheme (per spec §10.7.6 / §10.7.8) so the
87 /// strategy's indexed-form algorithms can dispatch
88 /// correctly without re-deriving the shape from observed
89 /// tuples (which would lose multi-axis lattice structure
90 /// after the flat materialization). `None` when the
91 /// upstream metadata propagator couldn't claim a closed-
92 /// form addressing function.
93 OrderMaterialize {
94 /// The strategy applied.
95 strategy: StrategyName,
96 /// The output cap, if any.
97 truncation: Option<u64>,
98 /// `true` when R2 push-down applies: the interpreter
99 /// should use the strategy's indexed form (draw
100 /// multi-indices, look up via input IndexFn).
101 /// `false` for the naïve form (materialize input,
102 /// then apply).
103 indexed: bool,
104 /// Upstream input's IndexFn at compile time (spec
105 /// §10.7.6). The interpreter passes this into the
106 /// [`crate::iteration::comprehension::strategies::EvaluatedInput`]
107 /// it builds for [`crate::iteration::comprehension::strategies::Strategy::apply`].
108 input_index_fn: Option<crate::iteration::comprehension::metadata::IndexFn>,
109 },
110
111 /// Bind the top stream as the comprehension's result.
112 /// Must be the last opcode in a well-formed Program.
113 Dispense,
114}
115
116/// Variant marker for [`Op::OrderStreaming`]. Today only Lex
117/// is streaming (per spec §6.2's "streaming order" table);
118/// the enum exists so future streaming strategies can land
119/// without changing the IR opcode set.
120#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
121#[serde(rename_all = "snake_case")]
122pub enum OrderStreamingKind {
123 /// Lexicographic order: the natural enumeration, counted.
124 Lex,
125}
126
127impl Op {
128 /// Arity for stack-effect computation: how many stream
129 /// operands this opcode pops, and how many it pushes.
130 /// Always pushes 1 for stream-producing ops; `Dispense`
131 /// pushes 0 (it consumes the final stream).
132 pub fn stack_effect(&self) -> (usize, usize) {
133 match self {
134 Op::PushClause { .. } => (0, 1),
135 Op::Cartesian { n } => (*n, 1),
136 Op::Zip { n, .. } => (*n, 1),
137 Op::Union { n } => (*n, 1),
138 Op::Filter { .. } => (1, 1),
139 Op::OrderStreaming { .. } => (1, 1),
140 Op::OrderMaterialize { .. } => (1, 1),
141 Op::Dispense => (1, 0),
142 }
143 }
144
145 /// `true` if this opcode is a materialization barrier per
146 /// spec §6.2 + §6.3. Used by the bounds checker.
147 pub fn is_barrier(&self) -> bool {
148 matches!(
149 self,
150 Op::OrderMaterialize { .. }
151 | Op::Zip {
152 mode: ZipMode::Cycle,
153 ..
154 }
155 )
156 }
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162
163 #[test]
164 fn stack_effect_basics() {
165 assert_eq!(
166 Op::PushClause {
167 name: "k".into(),
168 source: Source::Literal { values: vec![] },
169 }
170 .stack_effect(),
171 (0, 1)
172 );
173 assert_eq!(Op::Cartesian { n: 3 }.stack_effect(), (3, 1));
174 assert_eq!(Op::Dispense.stack_effect(), (1, 0));
175 }
176
177 #[test]
178 fn barrier_classification() {
179 assert!(
180 Op::OrderMaterialize {
181 strategy: StrategyName::Halton,
182 truncation: Some(10),
183 indexed: true,
184 input_index_fn: None,
185 }
186 .is_barrier()
187 );
188 assert!(
189 Op::Zip {
190 n: 2,
191 mode: ZipMode::Cycle
192 }
193 .is_barrier()
194 );
195 assert!(
196 !Op::Zip {
197 n: 2,
198 mode: ZipMode::Strict
199 }
200 .is_barrier()
201 );
202 assert!(
203 !Op::OrderStreaming {
204 kind: OrderStreamingKind::Lex,
205 truncation: None,
206 }
207 .is_barrier()
208 );
209 }
210
211 #[test]
212 fn serde_round_trip() {
213 let op = Op::OrderMaterialize {
214 strategy: StrategyName::Halton,
215 truncation: Some(50),
216 indexed: true,
217 input_index_fn: Some(
218 crate::iteration::comprehension::metadata::IndexFn::Lattice {
219 axis_sizes: vec![10, 5],
220 },
221 ),
222 };
223 let json = serde_json::to_string(&op).unwrap();
224 let back: Op = serde_json::from_str(&json).unwrap();
225 assert_eq!(op, back);
226 }
227}