1use serde::{Deserialize, Serialize};
33
34use super::ast::Comprehension;
35use super::cardinality::{CardinalityClass, Hybrid, Interval, ProductMeasure};
36use super::source::Source;
37use super::strategy::{StrategyName, ZipMode};
38
39#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
45pub struct Metadata {
46 pub cardinality: CardinalityClass,
48
49 pub index_addressable: Option<IndexFn>,
54
55 pub natural_order: NaturalOrder,
57
58 pub materialization: Materialization,
60}
61
62#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
70#[serde(tag = "kind", rename_all = "snake_case")]
71pub enum IndexFn {
72 Lattice {
76 axis_sizes: Vec<u64>,
78 },
79
80 Lockstep {
83 length: u64,
85 },
86
87 Modular {
91 axis_sizes: Vec<u64>,
93 },
94
95 Concatenation {
100 segment_sizes: Vec<u64>,
102 },
103
104 Continuous {
109 intervals: Vec<Interval>,
111 measure: ProductMeasure,
113 },
114
115 Hybrid {
119 discrete_axes: Vec<u64>,
121 continuous_axes: Vec<Interval>,
123 measure: ProductMeasure,
125 },
126}
127
128impl IndexFn {
129 pub fn has_continuous_axis(&self) -> bool {
133 matches!(self, IndexFn::Continuous { .. } | IndexFn::Hybrid { .. })
134 }
135
136 pub fn is_multi_axis_lattice(&self) -> bool {
141 matches!(self, IndexFn::Lattice { axis_sizes } if axis_sizes.len() >= 2)
142 }
143}
144
145#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
147#[serde(tag = "kind", rename_all = "snake_case")]
148pub enum NaturalOrder {
149 Lex,
152
153 Lockstep,
156
157 Sequential,
160
161 Strategy(StrategyName),
164
165 PendingSampling,
168}
169
170#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
172#[serde(tag = "kind", rename_all = "snake_case")]
173pub enum Materialization {
174 Streaming,
176
177 BoundedBarrier {
181 working_set_size: u64,
183 },
184
185 UnboundedBarrier,
190}
191
192impl Comprehension {
193 pub fn metadata(&self) -> Metadata {
206 match self {
207 Comprehension::Clause { source, .. } => clause_metadata(source),
208 Comprehension::Cartesian { children } => cartesian_metadata(children),
209 Comprehension::Zip { children, mode } => zip_metadata(children, *mode),
210 Comprehension::Union { children } => union_metadata(children),
211 Comprehension::Filter { child, .. } => filter_metadata(child),
212 Comprehension::Order {
213 child,
214 strategy,
215 truncation,
216 } => order_metadata(child, *strategy, *truncation),
217 }
218 }
219}
220
221fn clause_metadata(source: &Source) -> Metadata {
222 let cardinality = source.cardinality();
223 let (index_addressable, natural_order) = match &cardinality {
224 CardinalityClass::Bounded(n) => (
225 Some(IndexFn::Lattice {
226 axis_sizes: vec![*n],
227 }),
228 NaturalOrder::Lex,
229 ),
230 CardinalityClass::Continuous { intervals, measure } => (
231 Some(IndexFn::Continuous {
232 intervals: intervals.clone(),
233 measure: measure.clone(),
234 }),
235 NaturalOrder::PendingSampling,
236 ),
237 _ => (None, NaturalOrder::Lex),
240 };
241 Metadata {
242 cardinality,
243 index_addressable,
244 natural_order,
245 materialization: Materialization::Streaming,
246 }
247}
248
249fn cartesian_metadata(children: &[Comprehension]) -> Metadata {
250 let dependent = detect_dependent_sources(children);
254
255 let child_meta: Vec<Metadata> = children.iter().map(|c| c.metadata()).collect();
256 let cardinality = combine_cartesian_cardinality(&child_meta);
257
258 let index_addressable = if dependent {
259 None
260 } else {
261 combine_cartesian_index_fn(&child_meta)
262 };
263
264 let natural_order = if matches!(
265 cardinality,
266 CardinalityClass::Continuous { .. } | CardinalityClass::Hybrid(_)
267 ) {
268 NaturalOrder::PendingSampling
269 } else {
270 NaturalOrder::Lex
271 };
272
273 Metadata {
274 cardinality,
275 index_addressable,
276 natural_order,
277 materialization: Materialization::Streaming,
278 }
279}
280
281fn zip_metadata(children: &[Comprehension], mode: ZipMode) -> Metadata {
282 let child_meta: Vec<Metadata> = children.iter().map(|c| c.metadata()).collect();
283 let cardinality = combine_zip_cardinality(&child_meta, mode);
284 let index_addressable = combine_zip_index_fn(&child_meta, mode);
285
286 let materialization = match mode {
287 ZipMode::Strict | ZipMode::Truncate => Materialization::Streaming,
288 ZipMode::Cycle => {
289 let cards: Vec<u64> = child_meta
293 .iter()
294 .filter_map(|m| match &m.cardinality {
295 CardinalityClass::Bounded(n) | CardinalityClass::BoundedAtMost(n) => Some(*n),
296 _ => None,
297 })
298 .collect();
299 if cards.is_empty() {
300 Materialization::Streaming
301 } else {
302 let max = cards.iter().copied().max().unwrap_or(0);
303 let sum_non_longest: u64 = cards.iter().filter(|n| **n != max).sum();
304 Materialization::BoundedBarrier {
305 working_set_size: sum_non_longest,
306 }
307 }
308 }
309 };
310
311 Metadata {
312 cardinality,
313 index_addressable,
314 natural_order: NaturalOrder::Lockstep,
315 materialization,
316 }
317}
318
319fn union_metadata(children: &[Comprehension]) -> Metadata {
320 let child_meta: Vec<Metadata> = children.iter().map(|c| c.metadata()).collect();
321 let cardinality = combine_union_cardinality(&child_meta);
322 let index_addressable = combine_union_index_fn(&child_meta);
323 Metadata {
324 cardinality,
325 index_addressable,
326 natural_order: NaturalOrder::Sequential,
327 materialization: Materialization::Streaming,
328 }
329}
330
331fn filter_metadata(child: &Comprehension) -> Metadata {
332 let child_meta = child.metadata();
333 let cardinality = match &child_meta.cardinality {
334 CardinalityClass::Bounded(n) | CardinalityClass::BoundedAtMost(n) => {
335 CardinalityClass::BoundedAtMost(*n)
336 }
337 CardinalityClass::Unbounded => CardinalityClass::Unbounded,
338 CardinalityClass::Continuous { intervals, measure }
339 | CardinalityClass::ContinuousAtMost {
340 intervals,
341 measure_at_most: measure,
342 } => CardinalityClass::ContinuousAtMost {
343 intervals: intervals.clone(),
344 measure_at_most: measure.clone(),
345 },
346 CardinalityClass::Hybrid(h) => CardinalityClass::Hybrid(h.clone()),
347 };
348 Metadata {
349 cardinality,
350 index_addressable: None, natural_order: child_meta.natural_order,
352 materialization: child_meta.materialization,
353 }
354}
355
356fn order_metadata(
357 child: &Comprehension,
358 strategy: StrategyName,
359 truncation: Option<u64>,
360) -> Metadata {
361 let child_meta = child.metadata();
362 let cardinality = match (&child_meta.cardinality, truncation) {
363 (CardinalityClass::Continuous { .. }, Some(n))
365 | (CardinalityClass::ContinuousAtMost { .. }, Some(n))
366 | (CardinalityClass::Hybrid(_), Some(n))
367 if !matches!(strategy, StrategyName::Lex) =>
368 {
369 CardinalityClass::Bounded(n)
370 }
371 (CardinalityClass::Bounded(child_n), Some(n)) => {
373 CardinalityClass::Bounded((*child_n).min(n))
374 }
375 (CardinalityClass::BoundedAtMost(child_n), Some(n)) => {
376 CardinalityClass::BoundedAtMost((*child_n).min(n))
377 }
378 (_, Some(n)) => CardinalityClass::Bounded(n), (c, None) => c.clone(),
381 };
382
383 let (index_addressable, natural_order, materialization) = match strategy {
384 StrategyName::Lex => (
385 child_meta.index_addressable, NaturalOrder::Lex,
387 child_meta.materialization, ),
389 non_lex => {
390 let working_set_size =
393 strategy_working_set(non_lex, &child_meta.index_addressable, truncation);
394 (
395 None,
396 NaturalOrder::Strategy(non_lex),
397 Materialization::BoundedBarrier { working_set_size },
398 )
399 }
400 };
401
402 Metadata {
403 cardinality,
404 index_addressable,
405 natural_order,
406 materialization,
407 }
408}
409
410fn combine_cartesian_cardinality(children: &[Metadata]) -> CardinalityClass {
413 let mut has_continuous = false;
414 let mut has_discrete = false;
415 let mut has_unbounded = false;
416 let mut product: u64 = 1;
417 let mut overflow = false;
418 let mut discrete_axes: Vec<u64> = Vec::new();
419 let mut continuous_intervals: Vec<Interval> = Vec::new();
420 let mut continuous_measures: Vec<ProductMeasure> = Vec::new();
421
422 for m in children {
423 match &m.cardinality {
424 CardinalityClass::Bounded(n) => {
425 has_discrete = true;
426 discrete_axes.push(*n);
427 product = product.checked_mul(*n).unwrap_or_else(|| {
428 overflow = true;
429 u64::MAX
430 });
431 }
432 CardinalityClass::BoundedAtMost(n) => {
433 has_discrete = true;
434 discrete_axes.push(*n); product = product.checked_mul(*n).unwrap_or_else(|| {
436 overflow = true;
437 u64::MAX
438 });
439 }
440 CardinalityClass::Unbounded => {
441 has_unbounded = true;
442 has_discrete = true;
443 discrete_axes.push(0);
444 }
445 CardinalityClass::Continuous { intervals, measure }
446 | CardinalityClass::ContinuousAtMost {
447 intervals,
448 measure_at_most: measure,
449 } => {
450 has_continuous = true;
451 continuous_intervals.extend(intervals.iter().cloned());
452 continuous_measures.push(measure.clone());
453 }
454 CardinalityClass::Hybrid(h) => {
455 has_continuous = true;
456 has_discrete = true;
457 discrete_axes.extend(h.discrete_axes.iter().copied());
458 continuous_intervals.extend(h.continuous_axes.iter().cloned());
459 continuous_measures.push(h.measure.clone());
460 }
461 }
462 }
463
464 let _ = overflow; if has_continuous && has_discrete {
467 CardinalityClass::Hybrid(Hybrid {
468 discrete_axes,
469 continuous_axes: continuous_intervals,
470 measure: simplify_measures(continuous_measures),
471 })
472 } else if has_continuous {
473 CardinalityClass::Continuous {
474 intervals: continuous_intervals,
475 measure: simplify_measures(continuous_measures),
476 }
477 } else if has_unbounded {
478 CardinalityClass::Unbounded
479 } else {
480 CardinalityClass::Bounded(product)
481 }
482}
483
484fn combine_cartesian_index_fn(children: &[Metadata]) -> Option<IndexFn> {
485 let all_addressable = children.iter().all(|m| m.index_addressable.is_some());
487 if !all_addressable {
488 return None;
489 }
490
491 let mut all_discrete = true;
492 let mut all_continuous = true;
493 let mut discrete_axes: Vec<u64> = Vec::new();
494 let mut continuous_intervals: Vec<Interval> = Vec::new();
495 let mut continuous_measures: Vec<ProductMeasure> = Vec::new();
496
497 for m in children {
498 match m.index_addressable.as_ref().unwrap() {
499 IndexFn::Lattice { axis_sizes } => {
500 all_continuous = false;
501 discrete_axes.extend(axis_sizes.iter().copied());
502 }
503 IndexFn::Continuous { intervals, measure } => {
504 all_discrete = false;
505 continuous_intervals.extend(intervals.iter().cloned());
506 continuous_measures.push(measure.clone());
507 }
508 IndexFn::Hybrid {
509 discrete_axes: d,
510 continuous_axes: c,
511 measure,
512 } => {
513 all_discrete = false;
514 all_continuous = false;
515 discrete_axes.extend(d.iter().copied());
516 continuous_intervals.extend(c.iter().cloned());
517 continuous_measures.push(measure.clone());
518 }
519 IndexFn::Lockstep { .. } | IndexFn::Modular { .. } | IndexFn::Concatenation { .. } => {
525 return None;
526 }
527 }
528 }
529
530 if all_discrete {
531 Some(IndexFn::Lattice {
532 axis_sizes: discrete_axes,
533 })
534 } else if all_continuous {
535 Some(IndexFn::Continuous {
536 intervals: continuous_intervals,
537 measure: simplify_measures(continuous_measures),
538 })
539 } else {
540 Some(IndexFn::Hybrid {
541 discrete_axes,
542 continuous_axes: continuous_intervals,
543 measure: simplify_measures(continuous_measures),
544 })
545 }
546}
547
548fn combine_zip_cardinality(children: &[Metadata], mode: ZipMode) -> CardinalityClass {
549 let counts: Vec<Option<u64>> = children
552 .iter()
553 .map(|m| match &m.cardinality {
554 CardinalityClass::Bounded(n) | CardinalityClass::BoundedAtMost(n) => Some(*n),
555 CardinalityClass::Unbounded => None,
556 _ => None,
560 })
561 .collect();
562
563 match mode {
564 ZipMode::Strict => {
565 counts
567 .iter()
568 .find_map(|c| *c)
569 .map(CardinalityClass::Bounded)
570 .unwrap_or(CardinalityClass::Unbounded)
571 }
572 ZipMode::Truncate => {
573 let bounded: Vec<u64> = counts.iter().filter_map(|c| *c).collect();
574 if bounded.is_empty() {
575 CardinalityClass::Unbounded
576 } else {
577 CardinalityClass::Bounded(*bounded.iter().min().unwrap())
578 }
579 }
580 ZipMode::Cycle => {
581 let bounded: Vec<u64> = counts.iter().filter_map(|c| *c).collect();
582 if counts.iter().any(Option::is_none) {
583 CardinalityClass::Unbounded
584 } else if let Some(max) = bounded.iter().max() {
585 CardinalityClass::Bounded(*max)
586 } else {
587 CardinalityClass::Bounded(0)
588 }
589 }
590 }
591}
592
593fn combine_zip_index_fn(children: &[Metadata], mode: ZipMode) -> Option<IndexFn> {
594 let all_addressable = children.iter().all(|m| m.index_addressable.is_some());
595 if !all_addressable {
596 return None;
597 }
598 let counts: Vec<u64> = children
599 .iter()
600 .filter_map(|m| match &m.cardinality {
601 CardinalityClass::Bounded(n) | CardinalityClass::BoundedAtMost(n) => Some(*n),
602 _ => None,
603 })
604 .collect();
605 if counts.len() != children.len() {
606 return None;
607 }
608 match mode {
609 ZipMode::Strict | ZipMode::Truncate => {
610 let length = match mode {
611 ZipMode::Strict => counts[0],
612 ZipMode::Truncate => *counts.iter().min().unwrap(),
613 ZipMode::Cycle => unreachable!(),
614 };
615 Some(IndexFn::Lockstep { length })
616 }
617 ZipMode::Cycle => Some(IndexFn::Modular { axis_sizes: counts }),
618 }
619}
620
621fn combine_union_cardinality(children: &[Metadata]) -> CardinalityClass {
622 let mut sum: u64 = 0;
623 let mut any_unbounded = false;
624 let mut any_atmost = false;
625 for m in children {
626 match &m.cardinality {
627 CardinalityClass::Bounded(n) => {
628 sum = sum.saturating_add(*n);
629 }
630 CardinalityClass::BoundedAtMost(n) => {
631 sum = sum.saturating_add(*n);
632 any_atmost = true;
633 }
634 CardinalityClass::Unbounded => {
635 any_unbounded = true;
636 }
637 _ => any_unbounded = true,
639 }
640 }
641 if any_unbounded {
642 CardinalityClass::Unbounded
643 } else if any_atmost {
644 CardinalityClass::BoundedAtMost(sum)
645 } else {
646 CardinalityClass::Bounded(sum)
647 }
648}
649
650fn combine_union_index_fn(children: &[Metadata]) -> Option<IndexFn> {
651 let all_addressable = children.iter().all(|m| m.index_addressable.is_some());
652 if !all_addressable {
653 return None;
654 }
655 let segment_sizes: Vec<u64> = children
656 .iter()
657 .filter_map(|m| match &m.cardinality {
658 CardinalityClass::Bounded(n) | CardinalityClass::BoundedAtMost(n) => Some(*n),
659 _ => None,
660 })
661 .collect();
662 if segment_sizes.len() != children.len() {
663 return None;
664 }
665 Some(IndexFn::Concatenation { segment_sizes })
666}
667
668fn simplify_measures(measures: Vec<ProductMeasure>) -> ProductMeasure {
671 match measures.len() {
672 0 => ProductMeasure::Uniform,
673 1 => measures.into_iter().next().unwrap(),
674 _ => ProductMeasure::Product(measures),
675 }
676}
677
678fn strategy_working_set(
686 strategy: StrategyName,
687 input: &Option<IndexFn>,
688 truncation: Option<u64>,
689) -> u64 {
690 match (strategy, input, truncation) {
691 (StrategyName::Halton, Some(_), Some(n))
694 | (StrategyName::Sobol, Some(_), Some(n))
695 | (StrategyName::Shuffle, Some(_), Some(n))
696 | (StrategyName::ReverseLex, Some(_), Some(n)) => n,
697 (StrategyName::Lhs, Some(idx), Some(n)) => {
699 let dim = lattice_dim(idx).max(1);
700 n.saturating_mul(dim as u64)
701 }
702 (StrategyName::Extrema, Some(idx), Some(_k)) => index_fn_cardinality(idx),
711 (StrategyName::Shells, Some(_), Some(n))
713 | (StrategyName::Diagonal, Some(_), Some(n))
714 | (StrategyName::Antidiagonal, Some(_), Some(n)) => n,
715 (_, Some(idx), None) => index_fn_cardinality(idx),
717 (_, None, Some(n)) => n,
722 (_, None, None) => 0,
723 (StrategyName::Lex, Some(_), Some(n)) => n,
726 }
727}
728
729fn lattice_dim(idx: &IndexFn) -> usize {
730 match idx {
731 IndexFn::Lattice { axis_sizes } => axis_sizes.len(),
732 IndexFn::Continuous { intervals, .. } => intervals.len(),
733 IndexFn::Hybrid {
734 discrete_axes,
735 continuous_axes,
736 ..
737 } => discrete_axes.len() + continuous_axes.len(),
738 IndexFn::Lockstep { .. } | IndexFn::Modular { .. } => 1,
739 IndexFn::Concatenation { segment_sizes } => segment_sizes.len(),
740 }
741}
742
743fn index_fn_cardinality(idx: &IndexFn) -> u64 {
744 match idx {
745 IndexFn::Lattice { axis_sizes } => axis_sizes
746 .iter()
747 .copied()
748 .fold(1u64, |a, b| a.saturating_mul(b)),
749 IndexFn::Lockstep { length } => *length,
750 IndexFn::Modular { axis_sizes } => axis_sizes.iter().copied().max().unwrap_or(0),
751 IndexFn::Concatenation { segment_sizes } => segment_sizes
752 .iter()
753 .copied()
754 .fold(0u64, |a, b| a.saturating_add(b)),
755 IndexFn::Continuous { .. } | IndexFn::Hybrid { .. } => 0,
757 }
758}
759
760fn detect_dependent_sources(children: &[Comprehension]) -> bool {
764 let mut prior_names: Vec<String> = Vec::new();
765 for child in children {
766 for name in collect_source_name_references(child) {
769 if prior_names.contains(&name) {
770 return true;
771 }
772 }
773 for n in child.coordinate_names() {
775 if !prior_names.contains(&n) {
776 prior_names.push(n);
777 }
778 }
779 }
780 false
781}
782
783fn collect_source_name_references(c: &Comprehension) -> Vec<String> {
789 let mut out = Vec::new();
790 walk_source_refs(c, &mut out);
791 out
792}
793
794fn walk_source_refs(c: &Comprehension, out: &mut Vec<String>) {
795 match c {
796 Comprehension::Clause { source, .. } => {
797 extract_source_refs(source, out);
798 }
799 Comprehension::Cartesian { children }
800 | Comprehension::Zip { children, .. }
801 | Comprehension::Union { children } => {
802 for c in children {
803 walk_source_refs(c, out);
804 }
805 }
806 Comprehension::Filter { child, .. } | Comprehension::Order { child, .. } => {
807 walk_source_refs(child, out);
808 }
809 }
810}
811
812fn extract_source_refs(source: &Source, out: &mut Vec<String>) {
813 let s = match source {
814 Source::Generator { expr, .. } => expr.as_str(),
815 Source::WorkloadParamList { name, .. } => name.as_str(),
816 _ => return,
817 };
818 let bytes = s.as_bytes();
819 let mut i = 0;
820 while i < bytes.len() {
821 if bytes[i] == b'{'
822 && let Some(close) = s[i + 1..].find('}')
823 {
824 let name = s[i + 1..i + 1 + close].trim();
825 if !name.is_empty()
826 && name.chars().all(|c| c.is_alphanumeric() || c == '_')
827 && !out.contains(&name.to_string())
828 {
829 out.push(name.to_string());
830 }
831 i += close + 2;
832 continue;
833 }
834 i += 1;
835 }
836}
837
838#[cfg(test)]
839mod tests {
840 use super::*;
841 use crate::comprehension::source::{LiteralValue, Source};
842
843 fn clause(name: &str, vs: &[i64]) -> Comprehension {
844 Comprehension::clause(
845 name,
846 Source::Literal {
847 values: vs.iter().map(|n| LiteralValue::Int(*n)).collect(),
848 },
849 )
850 }
851
852 fn continuous_clause(name: &str) -> Comprehension {
853 Comprehension::clause(
854 name,
855 Source::ContinuousInterval {
856 interval: Interval::closed(0.0, 1.0),
857 measure: ProductMeasure::Uniform,
858 },
859 )
860 }
861
862 #[test]
863 fn clause_metadata_for_bounded_source() {
864 let m = clause("k", &[1, 2, 3]).metadata();
865 assert_eq!(m.cardinality, CardinalityClass::Bounded(3));
866 assert_eq!(
867 m.index_addressable,
868 Some(IndexFn::Lattice {
869 axis_sizes: vec![3]
870 })
871 );
872 assert_eq!(m.natural_order, NaturalOrder::Lex);
873 assert_eq!(m.materialization, Materialization::Streaming);
874 }
875
876 #[test]
877 fn clause_metadata_for_continuous_source() {
878 let m = continuous_clause("alpha").metadata();
879 assert!(matches!(m.cardinality, CardinalityClass::Continuous { .. }));
880 assert!(matches!(
881 m.index_addressable,
882 Some(IndexFn::Continuous { .. })
883 ));
884 assert_eq!(m.natural_order, NaturalOrder::PendingSampling);
885 assert_eq!(m.materialization, Materialization::Streaming);
886 }
887
888 #[test]
889 fn cartesian_metadata_combines_lattice_axes() {
890 let c =
891 Comprehension::cartesian(vec![clause("k", &[1, 2]), clause("limit", &[10, 20, 30])]);
892 let m = c.metadata();
893 assert_eq!(m.cardinality, CardinalityClass::Bounded(6));
894 assert_eq!(
895 m.index_addressable,
896 Some(IndexFn::Lattice {
897 axis_sizes: vec![2, 3]
898 })
899 );
900 assert_eq!(m.natural_order, NaturalOrder::Lex);
901 }
902
903 #[test]
904 fn cartesian_metadata_for_hybrid() {
905 let c =
906 Comprehension::cartesian(vec![clause("k", &[1, 2, 3, 4]), continuous_clause("theta")]);
907 let m = c.metadata();
908 match m.cardinality {
909 CardinalityClass::Hybrid(h) => {
910 assert_eq!(h.discrete_axes, vec![4]);
911 assert_eq!(h.continuous_axes.len(), 1);
912 }
913 other => panic!("expected Hybrid, got {other:?}"),
914 }
915 assert!(matches!(m.index_addressable, Some(IndexFn::Hybrid { .. })));
916 assert_eq!(m.natural_order, NaturalOrder::PendingSampling);
917 }
918
919 #[test]
920 fn dependent_cartesian_produces_none_addressable() {
921 let dependent = Comprehension::cartesian(vec![
923 clause("k", &[1, 2, 3]),
924 Comprehension::clause(
925 "replicas",
926 Source::Generator {
927 expr: "range(0, 2 * {k})".into(),
928 cardinality_hint: Some(6),
929 },
930 ),
931 ]);
932 let m = dependent.metadata();
933 assert!(m.index_addressable.is_none());
934 }
935
936 #[test]
937 fn zip_strict_produces_lockstep_index_fn() {
938 let c = Comprehension::zip(
939 vec![clause("x", &[1, 2, 3]), clause("y", &[10, 20, 30])],
940 ZipMode::Strict,
941 );
942 let m = c.metadata();
943 assert_eq!(m.index_addressable, Some(IndexFn::Lockstep { length: 3 }));
944 assert_eq!(m.natural_order, NaturalOrder::Lockstep);
945 assert_eq!(m.materialization, Materialization::Streaming);
946 }
947
948 #[test]
949 fn zip_cycle_produces_modular_index_fn_and_barrier() {
950 let c = Comprehension::zip(
951 vec![clause("k", &[1, 2, 3, 4, 5]), clause("color", &[1, 2, 3])],
952 ZipMode::Cycle,
953 );
954 let m = c.metadata();
955 match m.index_addressable {
956 Some(IndexFn::Modular { axis_sizes }) => {
957 assert_eq!(axis_sizes, vec![5, 3]);
958 }
959 other => panic!("expected Modular, got {other:?}"),
960 }
961 assert_eq!(
963 m.materialization,
964 Materialization::BoundedBarrier {
965 working_set_size: 3
966 }
967 );
968 }
969
970 #[test]
971 fn union_produces_concatenation_index_fn() {
972 let a = Comprehension::cartesian(vec![clause("k", &[1, 2]), clause("limit", &[10])]);
973 let b = Comprehension::cartesian(vec![clause("k", &[3, 4]), clause("limit", &[20])]);
974 let u = Comprehension::union(vec![a, b]);
975 let m = u.metadata();
976 assert_eq!(m.cardinality, CardinalityClass::Bounded(4));
977 assert_eq!(
978 m.index_addressable,
979 Some(IndexFn::Concatenation {
980 segment_sizes: vec![2, 2]
981 })
982 );
983 assert_eq!(m.natural_order, NaturalOrder::Sequential);
984 }
985
986 #[test]
987 fn filter_destroys_addressability() {
988 let inner =
989 Comprehension::cartesian(vec![clause("k", &[1, 2]), clause("limit", &[10, 20])]);
990 let filtered = Comprehension::filter(inner, "{k} > 0");
991 let m = filtered.metadata();
992 assert_eq!(m.cardinality, CardinalityClass::BoundedAtMost(4));
993 assert_eq!(m.index_addressable, None);
994 }
995
996 #[test]
997 fn lex_order_inherits_addressability() {
998 let inner =
999 Comprehension::cartesian(vec![clause("k", &[1, 2]), clause("limit", &[10, 20])]);
1000 let ordered = Comprehension::order(inner, StrategyName::Lex, Some(2));
1001 let m = ordered.metadata();
1002 assert_eq!(m.cardinality, CardinalityClass::Bounded(2));
1003 assert!(matches!(m.index_addressable, Some(IndexFn::Lattice { .. })));
1004 assert_eq!(m.natural_order, NaturalOrder::Lex);
1005 }
1006
1007 #[test]
1008 fn non_lex_order_drops_ast_level_addressability() {
1009 let inner =
1010 Comprehension::cartesian(vec![clause("k", &[1, 2]), clause("limit", &[10, 20])]);
1011 let ordered = Comprehension::order(inner, StrategyName::Halton, Some(2));
1012 let m = ordered.metadata();
1013 assert!(m.index_addressable.is_none());
1014 match m.natural_order {
1015 NaturalOrder::Strategy(StrategyName::Halton) => {}
1016 other => panic!("expected Strategy(Halton), got {other:?}"),
1017 }
1018 assert_eq!(
1019 m.materialization,
1020 Materialization::BoundedBarrier {
1021 working_set_size: 2
1022 }
1023 );
1024 }
1025
1026 #[test]
1027 fn continuous_sampling_yields_bounded_cardinality() {
1028 let inner =
1029 Comprehension::cartesian(vec![continuous_clause("alpha"), continuous_clause("beta")]);
1030 let ordered = Comprehension::order(inner, StrategyName::Halton, Some(100));
1031 let m = ordered.metadata();
1032 assert_eq!(m.cardinality, CardinalityClass::Bounded(100));
1033 assert_eq!(
1034 m.materialization,
1035 Materialization::BoundedBarrier {
1036 working_set_size: 100
1037 }
1038 );
1039 }
1040
1041 #[test]
1042 fn metadata_propagation_is_idempotent() {
1043 let c = Comprehension::order(
1044 Comprehension::filter(
1045 Comprehension::cartesian(vec![clause("k", &[1, 2, 3]), clause("limit", &[10, 20])]),
1046 "{k} * {limit} > 5",
1047 ),
1048 StrategyName::Halton,
1049 Some(5),
1050 );
1051 let m1 = c.metadata();
1052 let m2 = c.metadata();
1053 assert_eq!(m1, m2);
1054 }
1055
1056 #[test]
1057 fn has_continuous_axis_classifier() {
1058 let lat = IndexFn::Lattice {
1059 axis_sizes: vec![3, 4],
1060 };
1061 assert!(!lat.has_continuous_axis());
1062
1063 let cont = IndexFn::Continuous {
1064 intervals: vec![Interval::closed(0.0, 1.0)],
1065 measure: ProductMeasure::Uniform,
1066 };
1067 assert!(cont.has_continuous_axis());
1068 }
1069
1070 #[test]
1071 fn multi_axis_lattice_classifier() {
1072 assert!(
1073 IndexFn::Lattice {
1074 axis_sizes: vec![3, 4]
1075 }
1076 .is_multi_axis_lattice()
1077 );
1078 assert!(
1079 !IndexFn::Lattice {
1080 axis_sizes: vec![3]
1081 }
1082 .is_multi_axis_lattice()
1083 );
1084 assert!(
1085 !IndexFn::Continuous {
1086 intervals: vec![Interval::closed(0.0, 1.0), Interval::closed(0.0, 1.0)],
1087 measure: ProductMeasure::Uniform,
1088 }
1089 .is_multi_axis_lattice()
1090 );
1091 }
1092}