1use crate::comprehension::ast::Comprehension as AlgebraAst;
26use crate::comprehension::ast_legacy::{
27 Clause as LegacyClause, ClauseSource as LegacyClauseSource, Comprehension as LegacyAst,
28 ComprehensionMode as LegacyMode, Subspace as LegacySubspace, TraversalOrder as LegacyOrder,
29 ZipMode as LegacyZipMode,
30};
31use crate::comprehension::strategy::{StrategyName, ZipMode as AlgebraZipMode};
32
33use super::source_parser::{SourceParseError, parse_source};
34
35#[derive(Debug, Clone, PartialEq)]
37pub enum ConvertError {
38 SourceParse {
40 clause_var: String,
42 source: String,
44 cause: SourceParseError,
46 },
47 EmptyComprehension,
49 EmptyUnionSubspace,
51 ParallelArityMismatch {
54 vars: usize,
56 exprs: usize,
58 },
59 CustomOrderingRemoved {
62 function: String,
64 },
65}
66
67impl std::fmt::Display for ConvertError {
68 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69 match self {
70 ConvertError::SourceParse {
71 clause_var,
72 source,
73 cause,
74 } => write!(
75 f,
76 "clause {clause_var:?} source {source:?} failed to parse: {cause}"
77 ),
78 ConvertError::EmptyComprehension => f.write_str("comprehension has no clauses"),
79 ConvertError::EmptyUnionSubspace => f.write_str("union has empty sub-space"),
80 ConvertError::ParallelArityMismatch { vars, exprs } => {
81 write!(f, "parallel clause vars={vars} != exprs={exprs}")
82 }
83 ConvertError::CustomOrderingRemoved { function } => write!(
84 f,
85 "custom ordering {function:?} is no longer supported (spec §3.6)"
86 ),
87 }
88 }
89}
90
91impl std::error::Error for ConvertError {}
92
93pub fn legacy_to_algebra(legacy: &LegacyAst) -> Result<AlgebraAst, ConvertError> {
117 let body = match &legacy.mode {
118 LegacyMode::Cartesian(clauses) => convert_cartesian(clauses)?,
119 LegacyMode::Union(subspaces) => convert_union(subspaces)?,
120 };
121
122 let with_filter = if let Some(pred) = &legacy.filter {
123 AlgebraAst::filter(body, pred.clone())
124 } else {
125 body
126 };
127
128 let with_order = if let Some(order) = &legacy.order {
129 let (strategy, truncation) = convert_order(order)?;
130 AlgebraAst::order(with_filter, strategy, truncation)
131 } else {
132 with_filter
133 };
134
135 Ok(with_order)
136}
137
138fn convert_cartesian(clauses: &[LegacyClause]) -> Result<AlgebraAst, ConvertError> {
139 if clauses.is_empty() {
140 return Err(ConvertError::EmptyComprehension);
141 }
142 let algebra_children: Vec<AlgebraAst> = clauses
143 .iter()
144 .map(convert_clause)
145 .collect::<Result<_, _>>()?;
146 if algebra_children.len() == 1 {
147 Ok(algebra_children.into_iter().next().unwrap())
151 } else {
152 Ok(AlgebraAst::cartesian(algebra_children))
153 }
154}
155
156fn convert_union(subspaces: &[LegacySubspace]) -> Result<AlgebraAst, ConvertError> {
157 if subspaces.is_empty() {
158 return Err(ConvertError::EmptyComprehension);
159 }
160 let algebra_children: Vec<AlgebraAst> = subspaces
161 .iter()
162 .map(|s| {
163 if s.is_empty() {
164 Err(ConvertError::EmptyUnionSubspace)
165 } else {
166 convert_cartesian(&s.clauses)
167 }
168 })
169 .collect::<Result<_, _>>()?;
170 if algebra_children.len() == 1 {
171 Ok(algebra_children.into_iter().next().unwrap())
172 } else {
173 Ok(AlgebraAst::union(algebra_children))
174 }
175}
176
177fn convert_clause(clause: &LegacyClause) -> Result<AlgebraAst, ConvertError> {
178 match &clause.source {
179 LegacyClauseSource::Single(source_str) => {
180 let var = clause
181 .single_var()
182 .unwrap_or_else(|| clause.first_var())
183 .to_string();
184 let source = parse_source(source_str).map_err(|cause| ConvertError::SourceParse {
185 clause_var: var.clone(),
186 source: source_str.clone(),
187 cause,
188 })?;
189 Ok(AlgebraAst::clause(var, source))
190 }
191 LegacyClauseSource::Parallel { mode, exprs } => {
192 if clause.vars.len() != exprs.len() {
193 return Err(ConvertError::ParallelArityMismatch {
194 vars: clause.vars.len(),
195 exprs: exprs.len(),
196 });
197 }
198 let mut children = Vec::with_capacity(clause.vars.len());
202 for (var, expr) in clause.vars.iter().zip(exprs.iter()) {
203 let source = parse_source(expr).map_err(|cause| ConvertError::SourceParse {
204 clause_var: var.clone(),
205 source: expr.clone(),
206 cause,
207 })?;
208 children.push(AlgebraAst::clause(var.clone(), source));
209 }
210 let zip_mode = convert_zip_mode(*mode);
211 Ok(AlgebraAst::zip(children, zip_mode))
212 }
213 }
214}
215
216fn convert_zip_mode(legacy: LegacyZipMode) -> AlgebraZipMode {
217 match legacy {
218 LegacyZipMode::Strict => AlgebraZipMode::Strict,
219 LegacyZipMode::Truncate => AlgebraZipMode::Truncate,
220 LegacyZipMode::Cycle => AlgebraZipMode::Cycle,
221 }
222}
223
224fn convert_order(order: &LegacyOrder) -> Result<(StrategyName, Option<u64>), ConvertError> {
230 let pair = match order {
231 LegacyOrder::Lex { count } => (StrategyName::Lex, count.map(|n| n as u64)),
232 LegacyOrder::ReverseLex { count } => (StrategyName::ReverseLex, count.map(|n| n as u64)),
233 LegacyOrder::Diagonal { count } => (StrategyName::Diagonal, count.map(|n| n as u64)),
234 LegacyOrder::Antidiagonal { count } => {
235 (StrategyName::Antidiagonal, count.map(|n| n as u64))
236 }
237 LegacyOrder::Extrema { strata } => (StrategyName::Extrema, strata.map(|n| n as u64)),
238 LegacyOrder::Shells { depth, .. } => (StrategyName::Shells, depth.map(|n| n as u64)),
239 LegacyOrder::Halton { count } => (StrategyName::Halton, count.map(|n| n as u64)),
240 LegacyOrder::Sobol { count } => (StrategyName::Sobol, count.map(|n| n as u64)),
241 LegacyOrder::Lhs { count, .. } => (StrategyName::Lhs, count.map(|n| n as u64)),
242 LegacyOrder::Custom { function } => {
243 return Err(ConvertError::CustomOrderingRemoved {
244 function: function.clone(),
245 });
246 }
247 };
248 Ok(pair)
249}
250
251#[cfg(test)]
252mod tests {
253 use super::*;
254 use crate::comprehension::source::{LiteralValue, Source};
255
256 fn legacy_clause(var: &str, source: &str) -> LegacyClause {
257 LegacyClause::new(var, source)
258 }
259
260 #[test]
261 fn cartesian_single_clause_collapses_to_clause() {
262 let legacy = LegacyAst {
263 mode: LegacyMode::Cartesian(vec![legacy_clause("k", "1..10")]),
264 filter: None,
265 order: None,
266 };
267 let algebra = legacy_to_algebra(&legacy).unwrap();
268 match algebra {
269 AlgebraAst::Clause { name, source } => {
270 assert_eq!(name, "k");
271 assert!(matches!(
272 source,
273 Source::IntRange {
274 lo: 1,
275 hi: 10,
276 step: 1
277 }
278 ));
279 }
280 other => panic!("expected Clause, got {other:?}"),
281 }
282 }
283
284 #[test]
285 fn multi_clause_cartesian_becomes_algebra_cartesian() {
286 let legacy = LegacyAst {
287 mode: LegacyMode::Cartesian(vec![
288 legacy_clause("k", "1..10"),
289 legacy_clause("limit", "[10, 100, 1000]"),
290 ]),
291 filter: None,
292 order: None,
293 };
294 let algebra = legacy_to_algebra(&legacy).unwrap();
295 match algebra {
296 AlgebraAst::Cartesian { children } => {
297 assert_eq!(children.len(), 2);
298 match &children[0] {
300 AlgebraAst::Clause { name, source } => {
301 assert_eq!(name, "k");
302 assert!(matches!(
303 source,
304 Source::IntRange {
305 lo: 1,
306 hi: 10,
307 step: 1
308 }
309 ));
310 }
311 other => panic!("expected Clause, got {other:?}"),
312 }
313 match &children[1] {
315 AlgebraAst::Clause { name, source } => {
316 assert_eq!(name, "limit");
317 match source {
318 Source::Literal { values } => {
319 assert_eq!(values.len(), 3);
320 assert_eq!(values[0], LiteralValue::Int(10));
321 }
322 other => panic!("expected Literal, got {other:?}"),
323 }
324 }
325 other => panic!("expected Clause, got {other:?}"),
326 }
327 }
328 other => panic!("expected Cartesian, got {other:?}"),
329 }
330 }
331
332 #[test]
333 fn filter_wraps_body() {
334 let legacy = LegacyAst {
335 mode: LegacyMode::Cartesian(vec![legacy_clause("k", "1..10")]),
336 filter: Some("{k} > 5".to_string()),
337 order: None,
338 };
339 let algebra = legacy_to_algebra(&legacy).unwrap();
340 assert!(matches!(algebra, AlgebraAst::Filter { .. }));
341 }
342
343 #[test]
344 fn order_lex_with_count_round_trips() {
345 let legacy = LegacyAst {
346 mode: LegacyMode::Cartesian(vec![legacy_clause("k", "1..10")]),
347 filter: None,
348 order: Some(LegacyOrder::Lex { count: Some(5) }),
349 };
350 let algebra = legacy_to_algebra(&legacy).unwrap();
351 match algebra {
352 AlgebraAst::Order {
353 strategy: StrategyName::Lex,
354 truncation: Some(5),
355 ..
356 } => {}
357 other => panic!("expected Order(Lex, Some(5)), got {other:?}"),
358 }
359 }
360
361 #[test]
362 fn order_halton_with_count() {
363 let legacy = LegacyAst {
364 mode: LegacyMode::Cartesian(vec![
365 legacy_clause("k", "1..10"),
366 legacy_clause("limit", "1..100"),
367 ]),
368 filter: None,
369 order: Some(LegacyOrder::Halton { count: Some(20) }),
370 };
371 let algebra = legacy_to_algebra(&legacy).unwrap();
372 match algebra {
373 AlgebraAst::Order {
374 strategy: StrategyName::Halton,
375 truncation: Some(20),
376 ..
377 } => {}
378 other => panic!("expected Order(Halton, Some(20)), got {other:?}"),
379 }
380 }
381
382 #[test]
383 fn custom_ordering_rejected() {
384 let legacy = LegacyAst {
385 mode: LegacyMode::Cartesian(vec![legacy_clause("k", "1..10")]),
386 filter: None,
387 order: Some(LegacyOrder::Custom {
388 function: "my_fn".to_string(),
389 }),
390 };
391 let err = legacy_to_algebra(&legacy).unwrap_err();
392 assert!(matches!(err, ConvertError::CustomOrderingRemoved { .. }));
393 }
394
395 #[test]
396 fn union_of_subspaces() {
397 let legacy = LegacyAst {
398 mode: LegacyMode::Union(vec![
399 LegacySubspace::new(vec![
400 legacy_clause("k", "10"),
401 legacy_clause("limit", "[1, 2, 3]"),
402 ]),
403 LegacySubspace::new(vec![
404 legacy_clause("k", "100"),
405 legacy_clause("limit", "[10, 20, 30]"),
406 ]),
407 ]),
408 filter: None,
409 order: None,
410 };
411 let algebra = legacy_to_algebra(&legacy).unwrap();
412 match algebra {
413 AlgebraAst::Union { children } => assert_eq!(children.len(), 2),
414 other => panic!("expected Union, got {other:?}"),
415 }
416 }
417
418 #[test]
419 fn parallel_clause_becomes_zip() {
420 let parallel = LegacyClause::parallel(["x", "y"], ["1..3", "10..30"]);
421 let legacy = LegacyAst {
422 mode: LegacyMode::Cartesian(vec![parallel]),
423 filter: None,
424 order: None,
425 };
426 let algebra = legacy_to_algebra(&legacy).unwrap();
427 match algebra {
430 AlgebraAst::Zip {
431 children,
432 mode: AlgebraZipMode::Strict,
433 } => {
434 assert_eq!(children.len(), 2);
435 }
436 other => panic!("expected Zip, got {other:?}"),
437 }
438 }
439
440 #[test]
441 fn unparseable_source_falls_back_to_generator() {
442 let legacy = LegacyAst {
447 mode: LegacyMode::Cartesian(vec![legacy_clause("k", "totally nonsense")]),
448 filter: None,
449 order: None,
450 };
451 let algebra = legacy_to_algebra(&legacy).unwrap();
452 match algebra {
453 AlgebraAst::Clause { source, .. } => match source {
454 crate::comprehension::source::Source::Generator { expr, .. } => {
455 assert_eq!(expr, "totally nonsense");
456 }
457 other => panic!("expected Generator, got {other:?}"),
458 },
459 other => panic!("expected Clause, got {other:?}"),
460 }
461 }
462
463 }