Skip to main content

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}