Skip to main content

polydat_core/iteration/comprehension/ir/
compile.rs

1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! AST → IR compiler — spec §9.1.
5//!
6//! Bottom-up tree walk: each AST node emits its children's IR
7//! sequences in left-to-right order, then its own operator(s).
8//! `cartesian` / `zip` / `union` use N-arity opcodes; `filter`
9//! / `order` use unary wrappers. The terminal `Dispense` is
10//! appended at the end.
11//!
12//! R1 and R2 (the metadata-driven catalog entries from spec
13//! §10.2) are realized here: `order(Lex, _)` compiles to
14//! `Op::OrderStreaming` (R1); `order(non-Lex, Some(n))` over
15//! an index-addressable input compiles to
16//! `Op::OrderMaterialize { indexed: true }` (R2); the naïve
17//! path uses `indexed: false`.
18
19use crate::iteration::comprehension::ast::Comprehension;
20use crate::iteration::comprehension::strategy::StrategyName;
21
22use super::op::{Op, OrderStreamingKind};
23use super::program::Program;
24
25/// Compile an optimized AST to a `Program`. The result is
26/// ready for execution by [`super::interpreter::interpret`].
27///
28/// Per spec §9.4 + §10.6 the input AST should have already
29/// been validated (§5) and optimized (§10). Compiling an
30/// un-optimized AST is well-defined but may produce
31/// catastrophic working sets (spec §10's motivating example).
32pub fn compile(ast: &Comprehension) -> Program {
33    let mut ops = Vec::new();
34    emit(ast, &mut ops);
35    ops.push(Op::Dispense);
36    Program::new(ops)
37}
38
39/// Recursive emit walker.
40fn emit(ast: &Comprehension, ops: &mut Vec<Op>) {
41    match ast {
42        Comprehension::Clause { name, source } => {
43            ops.push(Op::PushClause {
44                name: name.clone(),
45                source: source.clone(),
46            });
47        }
48        Comprehension::Cartesian { children } => {
49            for child in children {
50                emit(child, ops);
51            }
52            ops.push(Op::Cartesian { n: children.len() });
53        }
54        Comprehension::Zip { children, mode } => {
55            for child in children {
56                emit(child, ops);
57            }
58            ops.push(Op::Zip {
59                n: children.len(),
60                mode: *mode,
61            });
62        }
63        Comprehension::Union { children } => {
64            for child in children {
65                emit(child, ops);
66            }
67            ops.push(Op::Union { n: children.len() });
68        }
69        Comprehension::Filter { child, predicate } => {
70            emit(child, ops);
71            ops.push(Op::Filter {
72                predicate: predicate.clone(),
73            });
74        }
75        Comprehension::Order {
76            child,
77            strategy,
78            truncation,
79        } => {
80            emit(child, ops);
81            ops.push(order_op(child, *strategy, *truncation));
82        }
83    }
84}
85
86/// Choose between `OrderStreaming` (R1: Lex) and
87/// `OrderMaterialize` (R2: non-Lex with indexed push-down
88/// when the input's metadata is index-addressable).
89fn order_op(child: &Comprehension, strategy: StrategyName, truncation: Option<u64>) -> Op {
90    if matches!(strategy, StrategyName::Lex) {
91        return Op::OrderStreaming {
92            kind: OrderStreamingKind::Lex,
93            truncation,
94        };
95    }
96    // R2: check whether the input is index-addressable. The
97    // metadata propagator (spec §10.7) is the authority.
98    let metadata = child.metadata();
99    let indexed = metadata.index_addressable.is_some();
100    Op::OrderMaterialize {
101        strategy,
102        truncation,
103        indexed,
104        input_index_fn: metadata.index_addressable,
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111    use crate::iteration::comprehension::source::{LiteralValue, Source};
112    use crate::iteration::comprehension::strategy::ZipMode;
113
114    fn clause(name: &str, vs: &[i64]) -> Comprehension {
115        Comprehension::clause(
116            name,
117            Source::Literal {
118                values: vs.iter().map(|n| LiteralValue::Int(*n)).collect(),
119            },
120        )
121    }
122
123    #[test]
124    fn compile_single_clause_emits_3_opcodes() {
125        let ast = clause("k", &[1, 2, 3]);
126        let prog = compile(&ast);
127        assert_eq!(prog.len(), 2);
128        // [PUSH_CLAUSE, DISPENSE]
129        assert!(matches!(prog.ops()[0], Op::PushClause { .. }));
130        assert!(matches!(prog.ops()[1], Op::Dispense));
131    }
132
133    #[test]
134    fn compile_cartesian_emits_children_then_combinator() {
135        let ast = Comprehension::cartesian(vec![clause("a", &[1, 2]), clause("b", &[10, 20])]);
136        let prog = compile(&ast);
137        // [PUSH a, PUSH b, CARTESIAN(2), DISPENSE]
138        assert_eq!(prog.len(), 4);
139        match &prog.ops()[2] {
140            Op::Cartesian { n: 2 } => {}
141            other => panic!("expected Cartesian(2), got {other:?}"),
142        }
143    }
144
145    #[test]
146    fn compile_order_lex_emits_streaming() {
147        let inner = clause("k", &[1, 2, 3]);
148        let ast = Comprehension::order(inner, StrategyName::Lex, Some(2));
149        let prog = compile(&ast);
150        // [PUSH, ORDER_STREAMING(Lex, Some(2)), DISPENSE]
151        assert!(matches!(
152            prog.ops()[1],
153            Op::OrderStreaming {
154                kind: OrderStreamingKind::Lex,
155                truncation: Some(2)
156            }
157        ));
158    }
159
160    #[test]
161    fn compile_order_halton_emits_materialize_indexed() {
162        // halton over a cartesian (index-addressable) → R2 fires.
163        let cart = Comprehension::cartesian(vec![clause("a", &[1, 2, 3]), clause("b", &[10, 20])]);
164        let ast = Comprehension::order(cart, StrategyName::Halton, Some(3));
165        let prog = compile(&ast);
166        let last_non_dispense = &prog.ops()[prog.len() - 2];
167        match last_non_dispense {
168            Op::OrderMaterialize {
169                strategy: StrategyName::Halton,
170                truncation: Some(3),
171                indexed: true,
172                ..
173            } => {}
174            other => panic!("expected OrderMaterialize indexed, got {other:?}"),
175        }
176    }
177
178    #[test]
179    fn compile_order_halton_over_filter_is_naive() {
180        // Filter destroys index addressability → R2 does NOT
181        // fire → indexed = false.
182        let cart = Comprehension::cartesian(vec![clause("a", &[1, 2, 3]), clause("b", &[10, 20])]);
183        let filtered = Comprehension::filter(cart, "{a} > 0");
184        let ast = Comprehension::order(filtered, StrategyName::Halton, Some(2));
185        let prog = compile(&ast);
186        let order_op = prog
187            .ops()
188            .iter()
189            .find(|op| matches!(op, Op::OrderMaterialize { .. }))
190            .unwrap();
191        assert!(matches!(
192            order_op,
193            Op::OrderMaterialize { indexed: false, .. }
194        ));
195    }
196
197    #[test]
198    fn compile_zip_emits_zip_with_mode() {
199        let ast = Comprehension::zip(
200            vec![clause("x", &[1, 2, 3]), clause("y", &[10, 20, 30])],
201            ZipMode::Strict,
202        );
203        let prog = compile(&ast);
204        assert!(matches!(
205            prog.ops()[2],
206            Op::Zip {
207                n: 2,
208                mode: ZipMode::Strict
209            }
210        ));
211    }
212
213    #[test]
214    fn compile_terminates_with_dispense() {
215        let ast = clause("k", &[1]);
216        let prog = compile(&ast);
217        assert!(matches!(prog.ops().last(), Some(Op::Dispense)));
218    }
219}