1use serde::{Deserialize, Serialize};
21
22use super::source::Source;
23use super::strategy::{StrategyName, ZipMode};
24
25#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
36#[serde(tag = "op", rename_all = "snake_case")]
37pub enum Comprehension {
38 Clause {
41 name: String,
43 source: Source,
45 },
46
47 Cartesian {
50 children: Vec<Comprehension>,
52 },
53
54 Zip {
57 children: Vec<Comprehension>,
59 mode: ZipMode,
61 },
62
63 Union {
67 children: Vec<Comprehension>,
69 },
70
71 Filter {
75 child: Box<Comprehension>,
77 predicate: String,
79 },
80
81 Order {
85 child: Box<Comprehension>,
87 strategy: StrategyName,
89 truncation: Option<u64>,
91 },
92}
93
94impl Comprehension {
95 pub fn clause<S: Into<String>>(name: S, source: Source) -> Self {
97 Comprehension::Clause {
98 name: name.into(),
99 source,
100 }
101 }
102
103 pub fn cartesian(children: Vec<Comprehension>) -> Self {
105 Comprehension::Cartesian { children }
106 }
107
108 pub fn zip(children: Vec<Comprehension>, mode: ZipMode) -> Self {
111 Comprehension::Zip { children, mode }
112 }
113
114 pub fn union(children: Vec<Comprehension>) -> Self {
116 Comprehension::Union { children }
117 }
118
119 pub fn filter<S: Into<String>>(child: Comprehension, predicate: S) -> Self {
121 Comprehension::Filter {
122 child: Box::new(child),
123 predicate: predicate.into(),
124 }
125 }
126
127 pub fn order(child: Comprehension, strategy: StrategyName, truncation: Option<u64>) -> Self {
129 Comprehension::Order {
130 child: Box::new(child),
131 strategy,
132 truncation,
133 }
134 }
135
136 pub fn coordinate_names(&self) -> Vec<String> {
142 let mut acc = Vec::new();
143 self.collect_coordinate_names(&mut acc);
144 acc
145 }
146
147 pub fn coordinate_specs(&self) -> Vec<(String, String)> {
157 let mut acc = Vec::new();
158 let mut seen = std::collections::HashSet::new();
159 self.collect_coordinate_specs(&mut acc, &mut seen);
160 acc
161 }
162
163 pub fn referenced_source_names(&self) -> std::collections::BTreeSet<String> {
176 use super::source::Source;
177 let mut out = std::collections::BTreeSet::new();
178 self.walk_sources(&mut |source| match source {
179 Source::WorkloadParamList { name, .. } => {
180 out.insert(name.clone());
181 }
182 Source::Generator { expr, .. } => {
183 out.extend(crate::refs::referenced_names(expr));
191 crate::refs::collect_string_interpolation_refs(expr, &mut out);
192 }
193 Source::Literal { .. }
194 | Source::IntRange { .. }
195 | Source::ContinuousInterval { .. }
196 | Source::Distribution { .. } => {}
197 });
198 out
199 }
200
201 fn walk_sources(&self, visit: &mut impl FnMut(&super::source::Source)) {
203 match self {
204 Comprehension::Clause { source, .. } => visit(source),
205 Comprehension::Cartesian { children }
206 | Comprehension::Zip { children, .. }
207 | Comprehension::Union { children } => {
208 for c in children {
209 c.walk_sources(visit);
210 }
211 }
212 Comprehension::Filter { child, .. } | Comprehension::Order { child, .. } => {
213 child.walk_sources(visit);
214 }
215 }
216 }
217
218 fn collect_coordinate_specs(
219 &self,
220 acc: &mut Vec<(String, String)>,
221 seen: &mut std::collections::HashSet<String>,
222 ) {
223 use super::source::Source;
224 match self {
225 Comprehension::Clause { name, source } => {
226 if seen.insert(name.clone()) {
227 let spec_text = match source {
228 Source::IntRange { lo, hi, step } => {
229 if *step == 1 {
230 format!("{lo}..{hi}")
231 } else {
232 format!("{lo}..{hi}..{step}")
233 }
234 }
235 Source::Literal { values } if values.len() == 1 => {
236 literal_value_text(&values[0])
237 }
238 Source::Literal { values } => values
239 .iter()
240 .map(literal_value_text)
241 .collect::<Vec<_>>()
242 .join(", "),
243 Source::Generator { expr, .. } => expr.clone(),
244 Source::WorkloadParamList { name, .. } => format!("{{{name}}}"),
245 Source::ContinuousInterval { interval, .. } => {
246 if interval.hi_open {
252 format!("{:?}..{:?}", interval.lo, interval.hi)
253 } else {
254 format!("{:?}..={:?}", interval.lo, interval.hi)
255 }
256 }
257 Source::Distribution { .. } => "<distribution>".to_string(),
258 };
259 acc.push((name.clone(), spec_text));
260 }
261 }
262 Comprehension::Cartesian { children } | Comprehension::Zip { children, .. } => {
263 for c in children {
264 c.collect_coordinate_specs(acc, seen);
265 }
266 }
267 Comprehension::Union { children } => {
268 for c in children {
269 c.collect_coordinate_specs(acc, seen);
270 }
271 }
272 Comprehension::Filter { child, .. } | Comprehension::Order { child, .. } => {
273 child.collect_coordinate_specs(acc, seen);
274 }
275 }
276 }
277
278 fn collect_coordinate_names(&self, acc: &mut Vec<String>) {
279 match self {
280 Comprehension::Clause { name, .. } => {
281 if !acc.contains(name) {
282 acc.push(name.clone());
283 }
284 }
285 Comprehension::Cartesian { children } | Comprehension::Zip { children, .. } => {
286 for c in children {
287 c.collect_coordinate_names(acc);
288 }
289 }
290 Comprehension::Union { children } => {
291 if let Some(first) = children.first() {
294 first.collect_coordinate_names(acc);
295 }
296 }
297 Comprehension::Filter { child, .. } | Comprehension::Order { child, .. } => {
298 child.collect_coordinate_names(acc);
299 }
300 }
301 }
302
303 pub fn is_clause(&self) -> bool {
305 matches!(self, Comprehension::Clause { .. })
306 }
307
308 pub fn is_combinator(&self) -> bool {
310 matches!(
311 self,
312 Comprehension::Cartesian { .. }
313 | Comprehension::Zip { .. }
314 | Comprehension::Union { .. }
315 )
316 }
317
318 pub fn is_modifier(&self) -> bool {
320 matches!(
321 self,
322 Comprehension::Filter { .. } | Comprehension::Order { .. }
323 )
324 }
325
326 pub fn children(&self) -> Box<dyn Iterator<Item = &Comprehension> + '_> {
329 match self {
330 Comprehension::Clause { .. } => Box::new(std::iter::empty()),
331 Comprehension::Cartesian { children }
332 | Comprehension::Zip { children, .. }
333 | Comprehension::Union { children } => Box::new(children.iter()),
334 Comprehension::Filter { child, .. } | Comprehension::Order { child, .. } => {
335 Box::new(std::iter::once(child.as_ref()))
336 }
337 }
338 }
339
340 pub fn node_count(&self) -> usize {
344 1 + self.children().map(|c| c.node_count()).sum::<usize>()
345 }
346
347 pub fn depth(&self) -> usize {
351 1 + self.children().map(|c| c.depth()).max().unwrap_or(0)
352 }
353}
354
355fn literal_value_text(v: &super::source::LiteralValue) -> String {
362 use super::source::LiteralValue;
363 match v {
364 LiteralValue::Int(n) => n.to_string(),
365 LiteralValue::Float(f) => {
366 if f.fract() == 0.0 && f.is_finite() {
367 format!("{f:.1}")
368 } else {
369 format!("{f}")
370 }
371 }
372 LiteralValue::Bool(b) => b.to_string(),
373 LiteralValue::Json(j) => j.to_string(),
374 LiteralValue::String(s) => {
375 let bare_ok = !s.is_empty() && s.chars().all(|c| c.is_alphanumeric() || c == '_');
376 if bare_ok {
377 s.clone()
378 } else {
379 format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\""))
380 }
381 }
382 }
383}
384
385#[cfg(test)]
386mod tests {
387 use super::*;
388 use crate::comprehension::source::{LiteralValue, Source};
389
390 fn lit_int_clause(name: &str, values: &[i64]) -> Comprehension {
391 Comprehension::clause(
392 name,
393 Source::Literal {
394 values: values.iter().map(|n| LiteralValue::Int(*n)).collect(),
395 },
396 )
397 }
398
399 #[test]
400 fn clause_coordinates() {
401 let c = lit_int_clause("k", &[1, 2, 3]);
402 assert_eq!(c.coordinate_names(), vec!["k"]);
403 assert!(c.is_clause());
404 assert!(!c.is_combinator());
405 assert!(!c.is_modifier());
406 }
407
408 #[test]
409 fn continuous_interval_spec_text_round_trips_as_float() {
410 use crate::comprehension::cardinality::{Interval, ProductMeasure};
417 let c = Comprehension::clause(
418 "ef",
419 Source::ContinuousInterval {
420 interval: Interval {
421 lo: 1.0,
422 hi: 5.0,
423 lo_open: false,
424 hi_open: true,
425 },
426 measure: ProductMeasure::Uniform,
427 },
428 );
429 let (var, spec_text) = c.coordinate_specs().into_iter().next().unwrap();
430 assert_eq!(var, "ef");
431 let reparsed = crate::comprehension::spec::parse_source(&spec_text).unwrap();
434 assert!(
435 matches!(reparsed, Source::ContinuousInterval { .. }),
436 "reconstructed '{spec_text}' re-parsed to {reparsed:?}, expected ContinuousInterval"
437 );
438 }
439
440 #[test]
441 fn referenced_source_names_grammar_based() {
442 let bare = Comprehension::clause(
445 "eh",
446 Source::Generator {
447 expr: "eh_values".into(),
448 cardinality_hint: None,
449 },
450 );
451 let got: Vec<String> = bare.referenced_source_names().into_iter().collect();
452 assert_eq!(got, vec!["eh_values"]);
453
454 let call = Comprehension::clause(
458 "nbo",
459 Source::Generator {
460 expr: "concat(nbo_v_values)".into(),
461 cardinality_hint: None,
462 },
463 );
464 let got: Vec<String> = call.referenced_source_names().into_iter().collect();
465 assert_eq!(got, vec!["nbo_v_values"]);
466
467 let wpl = Comprehension::clause(
470 "p",
471 Source::WorkloadParamList {
472 name: "profiles".into(),
473 len_hint: None,
474 },
475 );
476 let got: Vec<String> = wpl.referenced_source_names().into_iter().collect();
477 assert_eq!(got, vec!["profiles"]);
478
479 let lit = lit_int_clause("k", &[1, 2, 3]);
481 assert!(lit.referenced_source_names().is_empty());
482
483 let cart = Comprehension::cartesian(vec![bare, call]);
485 let got: Vec<String> = cart.referenced_source_names().into_iter().collect();
486 assert_eq!(got, vec!["eh_values", "nbo_v_values"]);
487 }
488
489 #[test]
490 fn cartesian_coordinates_in_declaration_order() {
491 let c = Comprehension::cartesian(vec![
492 lit_int_clause("k", &[1, 2]),
493 lit_int_clause("limit", &[10, 20, 30]),
494 ]);
495 assert_eq!(c.coordinate_names(), vec!["k", "limit"]);
496 assert!(c.is_combinator());
497 }
498
499 #[test]
500 fn zip_coordinates() {
501 let c = Comprehension::zip(
502 vec![
503 lit_int_clause("x", &[1, 2, 3]),
504 lit_int_clause("y", &[10, 20, 30]),
505 ],
506 ZipMode::Strict,
507 );
508 assert_eq!(c.coordinate_names(), vec!["x", "y"]);
509 }
510
511 #[test]
512 fn union_takes_first_childs_shape() {
513 let a = Comprehension::cartesian(vec![
514 lit_int_clause("k", &[10]),
515 lit_int_clause("limit", &[10, 20]),
516 ]);
517 let b = Comprehension::cartesian(vec![
518 lit_int_clause("k", &[100]),
519 lit_int_clause("limit", &[100, 200]),
520 ]);
521 let u = Comprehension::union(vec![a, b]);
522 assert_eq!(u.coordinate_names(), vec!["k", "limit"]);
523 }
524
525 #[test]
526 fn filter_and_order_pass_through_coordinates() {
527 let inner = Comprehension::cartesian(vec![
528 lit_int_clause("k", &[1, 2]),
529 lit_int_clause("limit", &[10]),
530 ]);
531 let filtered = Comprehension::filter(inner.clone(), "{k} > 0");
532 assert_eq!(filtered.coordinate_names(), vec!["k", "limit"]);
533 assert!(filtered.is_modifier());
534
535 let ordered = Comprehension::order(inner, StrategyName::Lex, Some(5));
536 assert_eq!(ordered.coordinate_names(), vec!["k", "limit"]);
537 assert!(ordered.is_modifier());
538 }
539
540 #[test]
541 fn node_count_and_depth() {
542 let inner = Comprehension::cartesian(vec![
543 lit_int_clause("k", &[1, 2]),
544 lit_int_clause("limit", &[10]),
545 ]);
546 assert_eq!(inner.node_count(), 3);
548 assert_eq!(inner.depth(), 2);
549
550 let filtered = Comprehension::filter(inner, "{k} > 0");
551 assert_eq!(filtered.node_count(), 4);
553 assert_eq!(filtered.depth(), 3);
554 }
555
556 #[test]
557 fn round_trip_serde() {
558 let c = Comprehension::order(
559 Comprehension::filter(
560 Comprehension::cartesian(vec![
561 lit_int_clause("k", &[1, 2, 3]),
562 lit_int_clause("limit", &[10, 20]),
563 ]),
564 "{k} * {limit} > 5",
565 ),
566 StrategyName::Halton,
567 Some(10),
568 );
569 let json = serde_json::to_string(&c).unwrap();
570 let back: Comprehension = serde_json::from_str(&json).unwrap();
571 assert_eq!(c, back);
572 }
573}