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 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}