use serde::{Deserialize, Serialize};
use super::source::Source;
use super::strategy::{StrategyName, ZipMode};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "op", rename_all = "snake_case")]
pub enum Comprehension {
Clause { name: String, source: Source },
Cartesian { children: Vec<Comprehension> },
Zip { children: Vec<Comprehension>, mode: ZipMode },
Union { children: Vec<Comprehension> },
Filter { child: Box<Comprehension>, predicate: String },
Order { child: Box<Comprehension>, strategy: StrategyName, truncation: Option<u64> },
}
impl Comprehension {
pub fn clause<S: Into<String>>(name: S, source: Source) -> Self {
Comprehension::Clause { name: name.into(), source }
}
pub fn cartesian(children: Vec<Comprehension>) -> Self {
Comprehension::Cartesian { children }
}
pub fn zip(children: Vec<Comprehension>, mode: ZipMode) -> Self {
Comprehension::Zip { children, mode }
}
pub fn union(children: Vec<Comprehension>) -> Self {
Comprehension::Union { children }
}
pub fn filter<S: Into<String>>(child: Comprehension, predicate: S) -> Self {
Comprehension::Filter { child: Box::new(child), predicate: predicate.into() }
}
pub fn order(
child: Comprehension,
strategy: StrategyName,
truncation: Option<u64>,
) -> Self {
Comprehension::Order { child: Box::new(child), strategy, truncation }
}
pub fn coordinate_names(&self) -> Vec<String> {
let mut acc = Vec::new();
self.collect_coordinate_names(&mut acc);
acc
}
pub fn coordinate_specs(&self) -> Vec<(String, String)> {
let mut acc = Vec::new();
let mut seen = std::collections::HashSet::new();
self.collect_coordinate_specs(&mut acc, &mut seen);
acc
}
pub fn referenced_source_names(&self) -> std::collections::BTreeSet<String> {
use super::source::Source;
let mut out = std::collections::BTreeSet::new();
self.walk_sources(&mut |source| match source {
Source::WorkloadParamList { name, .. } => {
out.insert(name.clone());
}
Source::Generator { expr, .. } => {
out.extend(crate::dsl::refs::referenced_names(expr));
crate::dsl::refs::collect_string_interpolation_refs(expr, &mut out);
}
Source::Literal { .. }
| Source::IntRange { .. }
| Source::ContinuousInterval { .. }
| Source::Distribution { .. } => {}
});
out
}
fn walk_sources(&self, visit: &mut impl FnMut(&super::source::Source)) {
match self {
Comprehension::Clause { source, .. } => visit(source),
Comprehension::Cartesian { children }
| Comprehension::Zip { children, .. }
| Comprehension::Union { children } => {
for c in children {
c.walk_sources(visit);
}
}
Comprehension::Filter { child, .. } | Comprehension::Order { child, .. } => {
child.walk_sources(visit);
}
}
}
fn collect_coordinate_specs(
&self,
acc: &mut Vec<(String, String)>,
seen: &mut std::collections::HashSet<String>,
) {
use super::source::Source;
match self {
Comprehension::Clause { name, source } => {
if seen.insert(name.clone()) {
let spec_text = match source {
Source::IntRange { lo, hi, step } => {
if *step == 1 { format!("{lo}..{hi}") }
else { format!("{lo}..{hi}..{step}") }
}
Source::Literal { values } if values.len() == 1 => {
literal_value_text(&values[0])
}
Source::Literal { values } => {
values.iter().map(literal_value_text).collect::<Vec<_>>().join(", ")
}
Source::Generator { expr, .. } => expr.clone(),
Source::WorkloadParamList { name, .. } => format!("{{{name}}}"),
Source::ContinuousInterval { interval, .. } => {
if interval.hi_open { format!("{:?}..{:?}", interval.lo, interval.hi) }
else { format!("{:?}..={:?}", interval.lo, interval.hi) }
}
Source::Distribution { .. } => "<distribution>".to_string(),
};
acc.push((name.clone(), spec_text));
}
}
Comprehension::Cartesian { children } | Comprehension::Zip { children, .. } => {
for c in children {
c.collect_coordinate_specs(acc, seen);
}
}
Comprehension::Union { children } => {
for c in children {
c.collect_coordinate_specs(acc, seen);
}
}
Comprehension::Filter { child, .. } | Comprehension::Order { child, .. } => {
child.collect_coordinate_specs(acc, seen);
}
}
}
fn collect_coordinate_names(&self, acc: &mut Vec<String>) {
match self {
Comprehension::Clause { name, .. } => {
if !acc.contains(name) {
acc.push(name.clone());
}
}
Comprehension::Cartesian { children } | Comprehension::Zip { children, .. } => {
for c in children {
c.collect_coordinate_names(acc);
}
}
Comprehension::Union { children } => {
if let Some(first) = children.first() {
first.collect_coordinate_names(acc);
}
}
Comprehension::Filter { child, .. } | Comprehension::Order { child, .. } => {
child.collect_coordinate_names(acc);
}
}
}
pub fn is_clause(&self) -> bool {
matches!(self, Comprehension::Clause { .. })
}
pub fn is_combinator(&self) -> bool {
matches!(
self,
Comprehension::Cartesian { .. }
| Comprehension::Zip { .. }
| Comprehension::Union { .. }
)
}
pub fn is_modifier(&self) -> bool {
matches!(self, Comprehension::Filter { .. } | Comprehension::Order { .. })
}
pub fn children(&self) -> Box<dyn Iterator<Item = &Comprehension> + '_> {
match self {
Comprehension::Clause { .. } => Box::new(std::iter::empty()),
Comprehension::Cartesian { children } | Comprehension::Zip { children, .. } | Comprehension::Union { children } => {
Box::new(children.iter())
}
Comprehension::Filter { child, .. } | Comprehension::Order { child, .. } => {
Box::new(std::iter::once(child.as_ref()))
}
}
}
pub fn node_count(&self) -> usize {
1 + self.children().map(|c| c.node_count()).sum::<usize>()
}
pub fn depth(&self) -> usize {
1 + self
.children()
.map(|c| c.depth())
.max()
.unwrap_or(0)
}
}
fn literal_value_text(v: &super::source::LiteralValue) -> String {
use super::source::LiteralValue;
match v {
LiteralValue::Int(n) => n.to_string(),
LiteralValue::Float(f) => {
if f.fract() == 0.0 && f.is_finite() {
format!("{f:.1}")
} else {
format!("{f}")
}
}
LiteralValue::Bool(b) => b.to_string(),
LiteralValue::String(s) => {
let bare_ok = !s.is_empty()
&& s.chars().all(|c| c.is_alphanumeric() || c == '_');
if bare_ok {
s.clone()
} else {
format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\""))
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::iteration::comprehension::source::{LiteralValue, Source};
fn lit_int_clause(name: &str, values: &[i64]) -> Comprehension {
Comprehension::clause(
name,
Source::Literal {
values: values.iter().map(|n| LiteralValue::Int(*n)).collect(),
},
)
}
#[test]
fn clause_coordinates() {
let c = lit_int_clause("k", &[1, 2, 3]);
assert_eq!(c.coordinate_names(), vec!["k"]);
assert!(c.is_clause());
assert!(!c.is_combinator());
assert!(!c.is_modifier());
}
#[test]
fn continuous_interval_spec_text_round_trips_as_float() {
use crate::iteration::comprehension::cardinality::{Interval, ProductMeasure};
let c = Comprehension::clause(
"ef",
Source::ContinuousInterval {
interval: Interval { lo: 1.0, hi: 5.0, lo_open: false, hi_open: true },
measure: ProductMeasure::Uniform,
},
);
let (var, spec_text) = c.coordinate_specs().into_iter().next().unwrap();
assert_eq!(var, "ef");
let reparsed = crate::iteration::comprehension::spec::parse_source(&spec_text).unwrap();
assert!(
matches!(reparsed, Source::ContinuousInterval { .. }),
"reconstructed '{spec_text}' re-parsed to {reparsed:?}, expected ContinuousInterval"
);
}
#[test]
fn referenced_source_names_grammar_based() {
let bare = Comprehension::clause(
"eh",
Source::Generator { expr: "eh_values".into(), cardinality_hint: None },
);
let got: Vec<String> = bare.referenced_source_names().into_iter().collect();
assert_eq!(got, vec!["eh_values"]);
let call = Comprehension::clause(
"nbo",
Source::Generator { expr: "concat(nbo_v_values)".into(), cardinality_hint: None },
);
let got: Vec<String> = call.referenced_source_names().into_iter().collect();
assert_eq!(got, vec!["nbo_v_values"]);
let wpl = Comprehension::clause(
"p",
Source::WorkloadParamList { name: "profiles".into(), len_hint: None },
);
let got: Vec<String> = wpl.referenced_source_names().into_iter().collect();
assert_eq!(got, vec!["profiles"]);
let lit = lit_int_clause("k", &[1, 2, 3]);
assert!(lit.referenced_source_names().is_empty());
let cart = Comprehension::cartesian(vec![bare, call]);
let got: Vec<String> = cart.referenced_source_names().into_iter().collect();
assert_eq!(got, vec!["eh_values", "nbo_v_values"]);
}
#[test]
fn cartesian_coordinates_in_declaration_order() {
let c = Comprehension::cartesian(vec![
lit_int_clause("k", &[1, 2]),
lit_int_clause("limit", &[10, 20, 30]),
]);
assert_eq!(c.coordinate_names(), vec!["k", "limit"]);
assert!(c.is_combinator());
}
#[test]
fn zip_coordinates() {
let c = Comprehension::zip(
vec![
lit_int_clause("x", &[1, 2, 3]),
lit_int_clause("y", &[10, 20, 30]),
],
ZipMode::Strict,
);
assert_eq!(c.coordinate_names(), vec!["x", "y"]);
}
#[test]
fn union_takes_first_childs_shape() {
let a = Comprehension::cartesian(vec![
lit_int_clause("k", &[10]),
lit_int_clause("limit", &[10, 20]),
]);
let b = Comprehension::cartesian(vec![
lit_int_clause("k", &[100]),
lit_int_clause("limit", &[100, 200]),
]);
let u = Comprehension::union(vec![a, b]);
assert_eq!(u.coordinate_names(), vec!["k", "limit"]);
}
#[test]
fn filter_and_order_pass_through_coordinates() {
let inner = Comprehension::cartesian(vec![
lit_int_clause("k", &[1, 2]),
lit_int_clause("limit", &[10]),
]);
let filtered = Comprehension::filter(inner.clone(), "{k} > 0");
assert_eq!(filtered.coordinate_names(), vec!["k", "limit"]);
assert!(filtered.is_modifier());
let ordered = Comprehension::order(inner, StrategyName::Lex, Some(5));
assert_eq!(ordered.coordinate_names(), vec!["k", "limit"]);
assert!(ordered.is_modifier());
}
#[test]
fn node_count_and_depth() {
let inner = Comprehension::cartesian(vec![
lit_int_clause("k", &[1, 2]),
lit_int_clause("limit", &[10]),
]);
assert_eq!(inner.node_count(), 3);
assert_eq!(inner.depth(), 2);
let filtered = Comprehension::filter(inner, "{k} > 0");
assert_eq!(filtered.node_count(), 4);
assert_eq!(filtered.depth(), 3);
}
#[test]
fn round_trip_serde() {
let c = Comprehension::order(
Comprehension::filter(
Comprehension::cartesian(vec![
lit_int_clause("k", &[1, 2, 3]),
lit_int_clause("limit", &[10, 20]),
]),
"{k} * {limit} > 5",
),
StrategyName::Halton,
Some(10),
);
let json = serde_json::to_string(&c).unwrap();
let back: Comprehension = serde_json::from_str(&json).unwrap();
assert_eq!(c, back);
}
}