polydat_core/iteration/comprehension/surfaces/
compiled.rs1use 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#[derive(Debug, Clone)]
41pub struct CompiledComprehension {
42 program: Arc<Program>,
43}
44
45impl CompiledComprehension {
46 pub fn from_ast(ast: &Comprehension) -> Result<Self, ValidationError> {
53 Self::from_ast_with(ast, Mode::Permissive).map(|(compiled, _)| compiled)
54 }
55
56 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 pub fn from_program(program: Arc<Program>) -> Self {
87 Self { program }
88 }
89
90 pub fn program(&self) -> &Program {
93 &self.program
94 }
95
96 pub(crate) fn program_arc(&self) -> Arc<Program> {
99 Arc::clone(&self.program)
100 }
101
102 pub fn coordinate_stream(&self) -> CoordinateStream {
109 CoordinateStream::new(self.program_arc())
110 }
111
112 pub fn scoped_kernel_stream<K: KernelScope>(&self, parent: K) -> ScopedKernelStream<K> {
124 ScopedKernelStream::new(self.program_arc(), parent)
125 }
126
127 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
143fn 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
165fn 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 #[test]
230 fn from_ast_compiles_the_optimized_tree() {
231 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 #[test]
244 fn from_ast_refuses_a_tree_that_violates_a_v_axiom() {
245 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 #[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 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 let count = Arc::strong_count(&compiled.program);
293 assert!(
294 count >= 3,
295 "expected shared program across streamers, count = {count}"
296 );
297 }
298
299 #[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}