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 `input_index_fn`'s presence: the strategy
84 /// reads the index function from the evaluated input and routes
85 /// accordingly, so the op carries no second flag saying so.
86 ///
87 /// `input_index_fn` carries the upstream comprehension's
88 /// addressing scheme (per spec §10.7.6 / §10.7.8) so the
89 /// strategy's indexed-form algorithms can dispatch
90 /// correctly without re-deriving the shape from observed
91 /// tuples (which would lose multi-axis lattice structure
92 /// after the flat materialization). `None` when the
93 /// upstream metadata propagator couldn't claim a closed-
94 /// form addressing function.
95 OrderMaterialize {
96 /// The strategy applied.
97 strategy: StrategyName,
98 /// The output cap, if any.
99 truncation: Option<u64>,
100 /// The authored seed a seeded strategy (`Shuffle`, `Lhs`)
101 /// derives its state from; its fixed default when `None`.
102 seed: Option<u64>,
103 /// Upstream input's IndexFn at compile time (spec
104 /// §10.7.6). The interpreter passes this into the
105 /// [`crate::iteration::comprehension::strategies::EvaluatedInput`]
106 /// it builds for [`crate::iteration::comprehension::strategies::Strategy::apply`].
107 input_index_fn: Option<crate::iteration::comprehension::metadata::IndexFn>,
108 },
109
110 /// Bind the top stream as the comprehension's result.
111 /// Must be the last opcode in a well-formed Program.
112 Dispense,
113}
114
115/// Variant marker for [`Op::OrderStreaming`]. Today only Lex
116/// is streaming (per spec §6.2's "streaming order" table);
117/// the enum exists so future streaming strategies can land
118/// without changing the IR opcode set.
119#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
120#[serde(rename_all = "snake_case")]
121pub enum OrderStreamingKind {
122 /// Lexicographic order: the natural enumeration, counted.
123 Lex,
124}
125
126impl Op {
127 /// Arity for stack-effect computation: how many stream
128 /// operands this opcode pops, and how many it pushes.
129 /// Always pushes 1 for stream-producing ops; `Dispense`
130 /// pushes 0 (it consumes the final stream).
131 pub fn stack_effect(&self) -> (usize, usize) {
132 match self {
133 Op::PushClause { .. } => (0, 1),
134 Op::Cartesian { n } => (*n, 1),
135 Op::Zip { n, .. } => (*n, 1),
136 Op::Union { n } => (*n, 1),
137 Op::Filter { .. } => (1, 1),
138 Op::OrderStreaming { .. } => (1, 1),
139 Op::OrderMaterialize { .. } => (1, 1),
140 Op::Dispense => (1, 0),
141 }
142 }
143
144 /// `true` if this opcode is a materialization barrier per
145 /// spec §6.2 + §6.3. Used by the bounds checker.
146 pub fn is_barrier(&self) -> bool {
147 matches!(
148 self,
149 Op::OrderMaterialize { .. }
150 | Op::Zip {
151 mode: ZipMode::Cycle,
152 ..
153 }
154 )
155 }
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161
162 #[test]
163 fn stack_effect_basics() {
164 assert_eq!(
165 Op::PushClause {
166 name: "k".into(),
167 source: Source::Literal { values: vec![] },
168 }
169 .stack_effect(),
170 (0, 1)
171 );
172 assert_eq!(Op::Cartesian { n: 3 }.stack_effect(), (3, 1));
173 assert_eq!(Op::Dispense.stack_effect(), (1, 0));
174 }
175
176 #[test]
177 fn barrier_classification() {
178 assert!(
179 Op::OrderMaterialize {
180 strategy: StrategyName::Halton,
181 truncation: Some(10),
182 seed: None,
183 input_index_fn: None,
184 }
185 .is_barrier()
186 );
187 assert!(
188 Op::Zip {
189 n: 2,
190 mode: ZipMode::Cycle
191 }
192 .is_barrier()
193 );
194 assert!(
195 !Op::Zip {
196 n: 2,
197 mode: ZipMode::Strict
198 }
199 .is_barrier()
200 );
201 assert!(
202 !Op::OrderStreaming {
203 kind: OrderStreamingKind::Lex,
204 truncation: None,
205 }
206 .is_barrier()
207 );
208 }
209
210 #[test]
211 fn serde_round_trip() {
212 let op = Op::OrderMaterialize {
213 strategy: StrategyName::Halton,
214 truncation: Some(50),
215 seed: None,
216 input_index_fn: Some(
217 crate::iteration::comprehension::metadata::IndexFn::Lattice {
218 axis_sizes: vec![10, 5],
219 },
220 ),
221 };
222 let json = serde_json::to_string(&op).unwrap();
223 let back: Op = serde_json::from_str(&json).unwrap();
224 assert_eq!(op, back);
225 }
226}