Skip to main content

polydat_core/iteration/comprehension/surfaces/
compiled.rs

1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! `CompiledComprehension` — the entry point for the three
5//! consumption surfaces.
6//!
7//! Holds an `Arc<Program>` (immutable IR per spec §9.1). Each
8//! factory method on this handle (`coordinate_stream`,
9//! `scoped_kernel_stream`, `scope_once`) returns a fresh
10//! streamer with its own dispense state but shares the
11//! `Arc<Program>` — no recompilation across siblings (spec
12//! §9.5.2's "IR-sharing test" property).
13
14use std::sync::Arc;
15
16use crate::iteration::comprehension::ast::Comprehension;
17use crate::iteration::comprehension::eval_source::{EvalClass, SourceEval};
18use crate::iteration::comprehension::flatten::flatten_static_sources;
19use crate::iteration::comprehension::ir::{Program, compile as compile_to_ir};
20use crate::iteration::comprehension::optimize::optimize;
21use crate::iteration::comprehension::validate::{
22    Mode, ValidationError, ValidationReport, validate,
23};
24
25use crate::kernel::interp::NoScope;
26
27use super::coord_stream::CoordinateStream;
28use super::instance::{KernelScope, ScopedKernelInstance};
29use super::scope_once::scope_once_with;
30use super::scoped_stream::ScopedKernelStream;
31
32/// A comprehension that has been compiled to immutable IR
33/// and is ready to dispense. The single source of truth for
34/// the underlying program across the consumption surfaces.
35///
36/// Construction is via [`from_ast`](Self::from_ast) (compiles once) or
37/// [`from_program`](Self::from_program) (when the IR was compiled elsewhere).
38/// Cloning a `CompiledComprehension` is cheap — just an
39/// `Arc::clone` on the program.
40#[derive(Debug, Clone)]
41pub struct CompiledComprehension {
42    program: Arc<Program>,
43}
44
45impl CompiledComprehension {
46    /// Compile an AST: validation (§5, V1–V9) first, then the §10
47    /// optimizer, then the AST → IR pass, once. Validation and
48    /// optimization are stages of this compile
49    /// (comprehension_forms.md §5, §9.4, §10.6): a tree that
50    /// violates a V-axiom is refused here, and the §9.3 resource
51    /// bounds hold for the program this returns.
52    pub fn from_ast(ast: &Comprehension) -> Result<Self, ValidationError> {
53        Self::from_ast_with(ast, Mode::Permissive).map(|(compiled, _)| compiled)
54    }
55
56    /// [`from_ast`](Self::from_ast) under a validation mode
57    /// (comprehension_forms.md §5.8), with the validator's report: in
58    /// `Permissive` mode a degenerate composition is a warning in the
59    /// report and the comprehension compiles; in `Strict` mode it is
60    /// the error. Context-free sources are flattened first
61    /// (§10.7.0): a generator that references no name is evaluated
62    /// here, in the empty scope, and validated as the literal of its
63    /// values.
64    pub fn from_ast_with(
65        ast: &Comprehension,
66        mode: Mode,
67    ) -> Result<(Self, ValidationReport), ValidationError> {
68        let ast = flatten_static_sources(ast, &NoScope::new());
69        if let Some((name, references)) = first_context_required(&ast) {
70            return Err(ValidationError::ContextRequired { name, references });
71        }
72        if let Some((name, message)) = first_failed_static(&ast) {
73            return Err(ValidationError::SourceFailed { name, message });
74        }
75        let report = validate(&ast, mode)?;
76        Ok((
77            Self {
78                program: Arc::new(compile_to_ir(&optimize(ast))),
79            },
80            report,
81        ))
82    }
83
84    /// Wrap an already-compiled program (tests use this for
85    /// hand-built IR).
86    pub fn from_program(program: Arc<Program>) -> Self {
87        Self { program }
88    }
89
90    /// Access the underlying compiled program (immutable per
91    /// spec §9.1).
92    pub fn program(&self) -> &Program {
93        &self.program
94    }
95
96    /// Clone the `Arc<Program>` for sharing with other
97    /// handles. Used internally by the streamer factories.
98    pub(crate) fn program_arc(&self) -> Arc<Program> {
99        Arc::clone(&self.program)
100    }
101
102    /// **First-order surface** (spec §9.5).
103    ///
104    /// Return a fresh [`CoordinateStream`]. Each call
105    /// allocates new per-streamer state; siblings share the
106    /// underlying IR but dispense independently per spec
107    /// §9.5.2's independence contract.
108    pub fn coordinate_stream(&self) -> CoordinateStream {
109        CoordinateStream::new(self.program_arc())
110    }
111
112    /// **Second-order surface** (spec §9.5).
113    ///
114    /// Return a fresh [`ScopedKernelStream`] wrapping the
115    /// supplied parent kernel. Each `advance()` pulls one
116    /// coord tuple from the underlying IR and applies
117    /// `parent.scope(&coords)` to produce a
118    /// [`ScopedKernelInstance`].
119    ///
120    /// Independence: pulling from this stream does NOT
121    /// advance any [`CoordinateStream`] obtained from the
122    /// same `CompiledComprehension`.
123    pub fn scoped_kernel_stream<K: KernelScope>(&self, parent: K) -> ScopedKernelStream<K> {
124        ScopedKernelStream::new(self.program_arc(), parent)
125    }
126
127    /// **One-shot surface** (spec §9.5.3).
128    ///
129    /// Apply `parent.scope(coords)` directly, without
130    /// constructing any streamer. Pure function — no
131    /// cursor consulted, no dispense state advanced. Used
132    /// for replay, debugging, and point queries where a
133    /// specific coord tuple is already known.
134    pub fn scope_once<K: KernelScope>(
135        &self,
136        parent: &K,
137        coords: &crate::iteration::comprehension::strategies::Tuple,
138    ) -> ScopedKernelInstance<K::Scoped> {
139        scope_once_with(parent, coords)
140    }
141}
142
143/// The first clause of `ast` whose source needs a scope
144/// (comprehension_forms.md §10.7.0), with the names it references:
145/// the scope-less surfaces refuse such a comprehension by name.
146fn first_context_required(ast: &Comprehension) -> Option<(String, Vec<String>)> {
147    match ast {
148        Comprehension::Clause { name, source } => {
149            (source.eval_class() == EvalClass::ContextRequired).then(|| {
150                (
151                    name.clone(),
152                    source.referenced_names().into_iter().collect(),
153                )
154            })
155        }
156        Comprehension::Cartesian { children }
157        | Comprehension::Zip { children, .. }
158        | Comprehension::Union { children } => children.iter().find_map(first_context_required),
159        Comprehension::Filter { child, .. } | Comprehension::Order { child, .. } => {
160            first_context_required(child)
161        }
162    }
163}
164
165/// The first context-free generator the flatten could not evaluate,
166/// with the evaluator's message. After [`flatten_static_sources`] in
167/// the empty scope, a context-free generator that is still a call with
168/// no cardinality hint is one whose evaluation failed: an empty result
169/// carries a hint of 0 and a non-literal result its count. Nothing
170/// later evaluates it on the scope-less surfaces, so it is evaluated
171/// once more here for its message, and refused.
172fn first_failed_static(ast: &Comprehension) -> Option<(String, String)> {
173    use crate::iteration::comprehension::eval_source::EvalContext;
174    use crate::iteration::comprehension::source::Source;
175    match ast {
176        Comprehension::Clause { name, source } => match source {
177            Source::Generator {
178                cardinality_hint: None,
179                ..
180            } if source.eval_class() == EvalClass::Static => {
181                let scope = NoScope::new();
182                let ctx = EvalContext {
183                    var_name: name,
184                    scope: &scope,
185                    prefix: &[],
186                };
187                source
188                    .evaluate(Some(&ctx))
189                    .err()
190                    .map(|e| (name.clone(), e.to_string()))
191            }
192            _ => None,
193        },
194        Comprehension::Cartesian { children }
195        | Comprehension::Zip { children, .. }
196        | Comprehension::Union { children } => children.iter().find_map(first_failed_static),
197        Comprehension::Filter { child, .. } | Comprehension::Order { child, .. } => {
198            first_failed_static(child)
199        }
200    }
201}
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206    use crate::iteration::comprehension::source::{LiteralValue, Source};
207
208    fn clause(name: &str, vs: &[i64]) -> Comprehension {
209        Comprehension::clause(
210            name,
211            Source::Literal {
212                values: vs.iter().map(|n| LiteralValue::Int(*n)).collect(),
213            },
214        )
215    }
216
217    #[test]
218    fn from_ast_compiles_once() {
219        let ast = clause("k", &[1, 2, 3]);
220        let compiled = CompiledComprehension::from_ast(&ast).unwrap();
221        assert!(!compiled.program().is_empty());
222    }
223
224    /// Optimization is mandatory before IR compilation
225    /// (comprehension_forms.md §9.4, §10.6): `from_ast` compiles the
226    /// optimized tree, so a program it returns is the program of the
227    /// optimized AST, and where a rule fires it is not the program of
228    /// the raw one.
229    #[test]
230    fn from_ast_compiles_the_optimized_tree() {
231        // A nested cartesian: R0b (A2) flattens it, so the raw and
232        // optimized trees compile to different programs.
233        let inner = Comprehension::cartesian(vec![clause("a", &[1, 2]), clause("b", &[3])]);
234        let ast = Comprehension::cartesian(vec![inner, clause("c", &[4])]);
235        let compiled = CompiledComprehension::from_ast(&ast).unwrap();
236        assert_eq!(*compiled.program(), compile_to_ir(&optimize(ast.clone())));
237        assert_ne!(*compiled.program(), compile_to_ir(&ast));
238    }
239
240    /// Validation is a stage of the compile (comprehension_forms.md
241    /// §5): a tree that violates a V-axiom is refused by `from_ast`
242    /// with the axiom's error, not compiled.
243    #[test]
244    fn from_ast_refuses_a_tree_that_violates_a_v_axiom() {
245        // V1: a cartesian whose children bind the same name.
246        let ast = Comprehension::cartesian(vec![clause("k", &[1, 2]), clause("k", &[3, 4])]);
247        let err = CompiledComprehension::from_ast(&ast).unwrap_err();
248        assert!(
249            matches!(err, ValidationError::V1DuplicateName { ref name, .. } if name == "k"),
250            "{err}"
251        );
252        assert!(err.to_string().starts_with("V1:"), "{err}");
253    }
254
255    /// Validation modes (comprehension_forms.md §5.8): a degenerate
256    /// composition is a warning in the permissive report and the error
257    /// of a strict compile.
258    #[test]
259    fn from_ast_with_reports_or_refuses_a_degenerate_composition() {
260        use crate::iteration::comprehension::strategy::StrategyName;
261        use crate::iteration::comprehension::validate::ValidationWarning;
262        let ast = Comprehension::order(clause("k", &[1, 2, 3]), StrategyName::Extrema, Some(1));
263        let (_, report) = CompiledComprehension::from_ast_with(&ast, Mode::Permissive).unwrap();
264        assert!(matches!(
265            report.warnings.as_slice(),
266            [ValidationWarning::DegenerateGeometric { .. }]
267        ));
268        let err = CompiledComprehension::from_ast_with(&ast, Mode::Strict).unwrap_err();
269        assert!(matches!(err, ValidationError::StrictWarning(_)), "{err}");
270        assert!(err.to_string().starts_with("strict mode:"), "{err}");
271    }
272
273    #[test]
274    fn cloning_compiled_shares_arc() {
275        let ast = clause("k", &[1, 2, 3]);
276        let a = CompiledComprehension::from_ast(&ast).unwrap();
277        let b = a.clone();
278        // Same Arc — strong_count goes up.
279        let count = Arc::strong_count(&a.program);
280        assert!(count >= 2, "expected shared Arc, count = {count}");
281        drop(b);
282    }
283
284    #[test]
285    fn two_coordinate_streams_share_program() {
286        let ast = clause("k", &[1, 2, 3]);
287        let compiled = CompiledComprehension::from_ast(&ast).unwrap();
288        let _s1 = compiled.coordinate_stream();
289        let _s2 = compiled.coordinate_stream();
290        // Both streams hold an Arc; count is at least 3 (compiled +
291        // two streamers, possibly more if internal clones happen).
292        let count = Arc::strong_count(&compiled.program);
293        assert!(
294            count >= 3,
295            "expected shared program across streamers, count = {count}"
296        );
297    }
298
299    /// A context-required source has no coordinate stream
300    /// (comprehension_forms.md §9.5.2, §10.7.0): the scope-less
301    /// compile refuses it by name, with the names it needs, instead
302    /// of dispensing nothing.
303    #[test]
304    fn from_ast_refuses_a_context_required_source_by_name() {
305        let ast = Comprehension::cartesian(vec![
306            clause("k", &[1, 2]),
307            Comprehension::clause(
308                "j",
309                Source::Generator {
310                    expr: "pow2({n})".into(),
311                    cardinality_hint: None,
312                },
313            ),
314        ]);
315        let err = CompiledComprehension::from_ast(&ast).unwrap_err();
316        assert!(
317            matches!(
318                err,
319                ValidationError::ContextRequired { ref name, ref references }
320                    if name == "j" && references == &["n".to_string()]
321            ),
322            "{err}"
323        );
324        assert!(err.to_string().contains("traverse it with `for`"), "{err}");
325    }
326}