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