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)> {
158 let mut acc = Vec::new();
159 let mut seen = std::collections::HashSet::new();
160 self.collect_coordinate_specs(&mut acc, &mut seen);
161 acc
162 }
163
164 pub fn referenced_source_names(&self) -> std::collections::BTreeSet<String> {
177 use super::source::Source;
178 let mut out = std::collections::BTreeSet::new();
179 self.walk_sources(&mut |source| match source {
180 Source::WorkloadParamList { name, .. } => {
181 out.insert(name.clone());
182 }
183 Source::Generator { expr, .. } => {
184 out.extend(crate::refs::referenced_names(expr));
192 crate::refs::collect_string_interpolation_refs(expr, &mut out);
193 }
194 Source::Literal { .. }
195 | Source::IntRange { .. }
196 | Source::ContinuousInterval { .. }
197 | Source::Distribution { .. } => {}
198 });
199 out
200 }
201
202 fn walk_sources(&self, visit: &mut impl FnMut(&super::source::Source)) {
204 match self {
205 Comprehension::Clause { source, .. } => visit(source),
206 Comprehension::Cartesian { children }
207 | Comprehension::Zip { children, .. }
208 | Comprehension::Union { children } => {
209 for c in children {
210 c.walk_sources(visit);
211 }
212 }
213 Comprehension::Filter { child, .. } | Comprehension::Order { child, .. } => {
214 child.walk_sources(visit);
215 }
216 }
217 }
218
219 fn collect_coordinate_specs(
220 &self,
221 acc: &mut Vec<(String, String)>,
222 seen: &mut std::collections::HashSet<String>,
223 ) {
224 use super::source::Source;
225 match self {
226 Comprehension::Clause { name, source } => {
227 if seen.insert(name.clone()) {
228 let spec_text = match source {
229 Source::IntRange { lo, hi, step } => {
230 if *step == 1 {
231 format!("{lo}..{hi}")
232 } else {
233 format!("{lo}..{hi}..{step}")
234 }
235 }
236 Source::Literal { values } if values.len() == 1 => {
237 literal_value_text(&values[0])
238 }
239 Source::Literal { values } => values
240 .iter()
241 .map(literal_value_text)
242 .collect::<Vec<_>>()
243 .join(", "),
244 Source::Generator { expr, .. } => expr.clone(),
245 Source::WorkloadParamList { name, .. } => format!("{{{name}}}"),
246 Source::ContinuousInterval { interval, .. } => {
247 if interval.hi_open {
253 format!("{:?}..{:?}", interval.lo, interval.hi)
254 } else {
255 format!("{:?}..={:?}", interval.lo, interval.hi)
256 }
257 }
258 Source::Distribution { .. } => "<distribution>".to_string(),
259 };
260 acc.push((name.clone(), spec_text));
261 }
262 }
263 Comprehension::Cartesian { children } | Comprehension::Zip { children, .. } => {
264 for c in children {
265 c.collect_coordinate_specs(acc, seen);
266 }
267 }
268 Comprehension::Union { children } => {
269 for c in children {
270 c.collect_coordinate_specs(acc, seen);
271 }
272 }
273 Comprehension::Filter { child, .. } | Comprehension::Order { child, .. } => {
274 child.collect_coordinate_specs(acc, seen);
275 }
276 }
277 }
278
279 fn collect_coordinate_names(&self, acc: &mut Vec<String>) {
280 match self {
281 Comprehension::Clause { name, .. } => {
282 if !acc.contains(name) {
283 acc.push(name.clone());
284 }
285 }
286 Comprehension::Cartesian { children } | Comprehension::Zip { children, .. } => {
287 for c in children {
288 c.collect_coordinate_names(acc);
289 }
290 }
291 Comprehension::Union { children } => {
292 if let Some(first) = children.first() {
295 first.collect_coordinate_names(acc);
296 }
297 }
298 Comprehension::Filter { child, .. } | Comprehension::Order { child, .. } => {
299 child.collect_coordinate_names(acc);
300 }
301 }
302 }
303
304 pub fn is_clause(&self) -> bool {
306 matches!(self, Comprehension::Clause { .. })
307 }
308
309 pub fn is_combinator(&self) -> bool {
311 matches!(
312 self,
313 Comprehension::Cartesian { .. }
314 | Comprehension::Zip { .. }
315 | Comprehension::Union { .. }
316 )
317 }
318
319 pub fn is_modifier(&self) -> bool {
321 matches!(
322 self,
323 Comprehension::Filter { .. } | Comprehension::Order { .. }
324 )
325 }
326
327 pub fn children(&self) -> Box<dyn Iterator<Item = &Comprehension> + '_> {
330 match self {
331 Comprehension::Clause { .. } => Box::new(std::iter::empty()),
332 Comprehension::Cartesian { children }
333 | Comprehension::Zip { children, .. }
334 | Comprehension::Union { children } => Box::new(children.iter()),
335 Comprehension::Filter { child, .. } | Comprehension::Order { child, .. } => {
336 Box::new(std::iter::once(child.as_ref()))
337 }
338 }
339 }
340
341 pub fn node_count(&self) -> usize {
345 1 + self.children().map(|c| c.node_count()).sum::<usize>()
346 }
347
348 pub fn depth(&self) -> usize {
352 1 + self.children().map(|c| c.depth()).max().unwrap_or(0)
353 }
354}
355
356fn literal_value_text(v: &super::source::LiteralValue) -> String {
365 use super::source::LiteralValue;
366 match v {
367 LiteralValue::Int(n) => n.to_string(),
368 LiteralValue::Float(f) => {
369 if f.fract() == 0.0 && f.is_finite() {
370 format!("{f:.1}")
371 } else {
372 format!("{f}")
373 }
374 }
375 LiteralValue::Bool(b) => b.to_string(),
376 LiteralValue::Json(j) => j.to_string(),
377 LiteralValue::String(s) => {
378 let bare_ok = !s.is_empty() && s.chars().all(|c| c.is_alphanumeric() || c == '_');
379 if bare_ok {
380 s.clone()
381 } else {
382 format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\""))
383 }
384 }
385 }
386}
387
388#[cfg(test)]
389mod tests {
390 use super::*;
391 use crate::comprehension::source::{LiteralValue, Source};
392
393 fn lit_int_clause(name: &str, values: &[i64]) -> Comprehension {
394 Comprehension::clause(
395 name,
396 Source::Literal {
397 values: values.iter().map(|n| LiteralValue::Int(*n)).collect(),
398 },
399 )
400 }
401
402 #[test]
403 fn clause_coordinates() {
404 let c = lit_int_clause("k", &[1, 2, 3]);
405 assert_eq!(c.coordinate_names(), vec!["k"]);
406 assert!(c.is_clause());
407 assert!(!c.is_combinator());
408 assert!(!c.is_modifier());
409 }
410
411 #[test]
412 fn continuous_interval_spec_text_round_trips_as_float() {
413 use crate::comprehension::cardinality::{Interval, ProductMeasure};
420 let c = Comprehension::clause(
421 "ef",
422 Source::ContinuousInterval {
423 interval: Interval {
424 lo: 1.0,
425 hi: 5.0,
426 lo_open: false,
427 hi_open: true,
428 },
429 measure: ProductMeasure::Uniform,
430 },
431 );
432 let (var, spec_text) = c.coordinate_specs().into_iter().next().unwrap();
433 assert_eq!(var, "ef");
434 let reparsed = crate::comprehension::spec::parse_source(&spec_text).unwrap();
437 assert!(
438 matches!(reparsed, Source::ContinuousInterval { .. }),
439 "reconstructed '{spec_text}' re-parsed to {reparsed:?}, expected ContinuousInterval"
440 );
441 }
442
443 #[test]
444 fn referenced_source_names_grammar_based() {
445 let bare = Comprehension::clause(
448 "eh",
449 Source::Generator {
450 expr: "eh_values".into(),
451 cardinality_hint: None,
452 },
453 );
454 let got: Vec<String> = bare.referenced_source_names().into_iter().collect();
455 assert_eq!(got, vec!["eh_values"]);
456
457 let call = Comprehension::clause(
461 "nbo",
462 Source::Generator {
463 expr: "concat(nbo_v_values)".into(),
464 cardinality_hint: None,
465 },
466 );
467 let got: Vec<String> = call.referenced_source_names().into_iter().collect();
468 assert_eq!(got, vec!["nbo_v_values"]);
469
470 let wpl = Comprehension::clause(
473 "p",
474 Source::WorkloadParamList {
475 name: "profiles".into(),
476 len_hint: None,
477 },
478 );
479 let got: Vec<String> = wpl.referenced_source_names().into_iter().collect();
480 assert_eq!(got, vec!["profiles"]);
481
482 let lit = lit_int_clause("k", &[1, 2, 3]);
484 assert!(lit.referenced_source_names().is_empty());
485
486 let cart = Comprehension::cartesian(vec![bare, call]);
488 let got: Vec<String> = cart.referenced_source_names().into_iter().collect();
489 assert_eq!(got, vec!["eh_values", "nbo_v_values"]);
490 }
491
492 #[test]
493 fn cartesian_coordinates_in_declaration_order() {
494 let c = Comprehension::cartesian(vec![
495 lit_int_clause("k", &[1, 2]),
496 lit_int_clause("limit", &[10, 20, 30]),
497 ]);
498 assert_eq!(c.coordinate_names(), vec!["k", "limit"]);
499 assert!(c.is_combinator());
500 }
501
502 #[test]
503 fn zip_coordinates() {
504 let c = Comprehension::zip(
505 vec![
506 lit_int_clause("x", &[1, 2, 3]),
507 lit_int_clause("y", &[10, 20, 30]),
508 ],
509 ZipMode::Strict,
510 );
511 assert_eq!(c.coordinate_names(), vec!["x", "y"]);
512 }
513
514 #[test]
515 fn union_takes_first_childs_shape() {
516 let a = Comprehension::cartesian(vec![
517 lit_int_clause("k", &[10]),
518 lit_int_clause("limit", &[10, 20]),
519 ]);
520 let b = Comprehension::cartesian(vec![
521 lit_int_clause("k", &[100]),
522 lit_int_clause("limit", &[100, 200]),
523 ]);
524 let u = Comprehension::union(vec![a, b]);
525 assert_eq!(u.coordinate_names(), vec!["k", "limit"]);
526 }
527
528 #[test]
529 fn filter_and_order_pass_through_coordinates() {
530 let inner = Comprehension::cartesian(vec![
531 lit_int_clause("k", &[1, 2]),
532 lit_int_clause("limit", &[10]),
533 ]);
534 let filtered = Comprehension::filter(inner.clone(), "{k} > 0");
535 assert_eq!(filtered.coordinate_names(), vec!["k", "limit"]);
536 assert!(filtered.is_modifier());
537
538 let ordered = Comprehension::order(inner, StrategyName::Lex, Some(5));
539 assert_eq!(ordered.coordinate_names(), vec!["k", "limit"]);
540 assert!(ordered.is_modifier());
541 }
542
543 #[test]
544 fn node_count_and_depth() {
545 let inner = Comprehension::cartesian(vec![
546 lit_int_clause("k", &[1, 2]),
547 lit_int_clause("limit", &[10]),
548 ]);
549 assert_eq!(inner.node_count(), 3);
551 assert_eq!(inner.depth(), 2);
552
553 let filtered = Comprehension::filter(inner, "{k} > 0");
554 assert_eq!(filtered.node_count(), 4);
556 assert_eq!(filtered.depth(), 3);
557 }
558
559 #[test]
560 fn round_trip_serde() {
561 let c = Comprehension::order(
562 Comprehension::filter(
563 Comprehension::cartesian(vec![
564 lit_int_clause("k", &[1, 2, 3]),
565 lit_int_clause("limit", &[10, 20]),
566 ]),
567 "{k} * {limit} > 5",
568 ),
569 StrategyName::Halton,
570 Some(10),
571 );
572 let json = serde_json::to_string(&c).unwrap();
573 let back: Comprehension = serde_json::from_str(&json).unwrap();
574 assert_eq!(c, back);
575 }
576}