1use super::ast::Comprehension;
21use super::source::{LiteralValue, Source};
22use super::strategy::{StrategyName, ZipMode};
23
24impl Source {
25 pub fn to_text(&self) -> Option<String> {
28 Some(match self {
29 Source::IntRange { lo, hi, step } => {
30 if *step == 1 {
31 format!("{lo}..{hi}")
32 } else {
33 format!("{lo}..{hi}..{step}")
34 }
35 }
36 Source::Literal { values } => literal_list_text(values)?,
41 Source::Generator { expr, .. } => expr.clone(),
42 Source::WorkloadParamList { name, .. } => format!("{{{name}}}"),
43 Source::ContinuousInterval { interval, .. } => {
44 if interval.hi_open {
50 format!("{:?}..{:?}", interval.lo, interval.hi)
51 } else {
52 format!("{:?}..={:?}", interval.lo, interval.hi)
53 }
54 }
55 Source::Distribution {
56 distribution,
57 support,
58 params,
59 } => {
60 let args = params
64 .iter()
65 .map(|p| format!("{p:?}"))
66 .collect::<Vec<_>>()
67 .join(", ");
68 let call = format!("{}({args})", distribution.text());
69 if *support == distribution.support(params) {
70 call
71 } else if support.hi_open {
72 format!("{call} on {:?}..{:?}", support.lo, support.hi)
73 } else {
74 format!("{call} on {:?}..={:?}", support.lo, support.hi)
75 }
76 }
77 })
78 }
79}
80
81fn literal_list_text(values: &[LiteralValue]) -> Option<String> {
87 if values.is_empty() {
88 return None;
89 }
90 let bare_ok = values.iter().all(bare_value_is_unambiguous)
94 && (values.len() > 1 || !matches!(values[0], LiteralValue::String(_)));
95 if bare_ok {
96 let items = values
97 .iter()
98 .map(bare_value_text)
99 .collect::<Option<Vec<_>>>()?;
100 return Some(items.join(", "));
101 }
102 let items = values
103 .iter()
104 .map(quoted_value_text)
105 .collect::<Option<Vec<_>>>()?;
106 Some(format!("[{}]", items.join(", ")))
107}
108
109fn bare_value_is_unambiguous(v: &LiteralValue) -> bool {
114 match v {
115 LiteralValue::Int(_) | LiteralValue::Float(_) | LiteralValue::Bool(_) => true,
116 LiteralValue::String(s) => {
117 let trimmed = s.trim();
118 !trimmed.is_empty()
119 && trimmed == s
120 && !s.contains([
121 '(', ')', '[', ']', '{', '}', '\'', '"', '+', '*', '/', '%', '=', '<', '>',
122 '!', '&', '|', '~', '^', '?', ',', '\n',
123 ])
124 && s.parse::<i64>().is_err()
125 && s.parse::<f64>().is_err()
126 && !s.eq_ignore_ascii_case("true")
127 && !s.eq_ignore_ascii_case("false")
128 }
129 LiteralValue::Json(_) => false,
130 }
131}
132
133fn bare_value_text(v: &LiteralValue) -> Option<String> {
135 Some(match v {
136 LiteralValue::Int(i) => i.to_string(),
137 LiteralValue::Float(f) => format!("{f:?}"),
138 LiteralValue::Bool(b) => b.to_string(),
139 LiteralValue::String(s) => s.clone(),
140 LiteralValue::Json(_) => return None,
141 })
142}
143
144fn quoted_value_text(v: &LiteralValue) -> Option<String> {
149 Some(match v {
150 LiteralValue::String(s) => {
151 if s.contains(['"', '\'', ',', '[', ']', '{', '}', '(', ')', '\n']) {
152 return None;
153 }
154 format!("\"{s}\"")
155 }
156 other => bare_value_text(other)?,
157 })
158}
159
160impl Comprehension {
161 pub fn to_text(&self) -> Option<String> {
168 match self {
169 Comprehension::Order {
170 child,
171 strategy,
172 truncation,
173 seed,
174 } => {
175 let head = match &**child {
176 Comprehension::Filter { child, predicate } => {
177 format!("{} where {predicate}", child.body_text()?)
178 }
179 other => other.body_text()?,
180 };
181 Some(format!(
182 "{head} order {}",
183 order_text(*strategy, *truncation, *seed)
184 ))
185 }
186 Comprehension::Filter { child, predicate } => {
187 Some(format!("{} where {predicate}", child.body_text()?))
188 }
189 other => other.body_text(),
190 }
191 }
192
193 fn body_text(&self) -> Option<String> {
197 match self {
198 Comprehension::Clause { .. } | Comprehension::Zip { .. } => self.clause_text(),
199 Comprehension::Cartesian { children } => children
200 .iter()
201 .map(Comprehension::clause_text)
202 .collect::<Option<Vec<_>>>()
203 .map(|clauses| clauses.join(", ")),
204 Comprehension::Union { children } => {
205 let members = children
206 .iter()
207 .map(|c| c.to_text().map(|text| format!("for {text}")))
208 .collect::<Option<Vec<_>>>()?;
209 Some(format!("[ {} ]", members.join(", ")))
210 }
211 Comprehension::Filter { .. } | Comprehension::Order { .. } => None,
214 }
215 }
216
217 fn clause_text(&self) -> Option<String> {
220 match self {
221 Comprehension::Clause { name, source } => {
222 Some(format!("{name} in {}", source.to_text()?))
223 }
224 Comprehension::Zip { children, mode } => {
225 if children.len() < 2 {
226 return None;
227 }
228 let mut names = Vec::with_capacity(children.len());
229 let mut sources = Vec::with_capacity(children.len());
230 for child in children {
231 let Comprehension::Clause { name, source } = child else {
232 return None;
233 };
234 names.push(name.clone());
235 sources.push(source.to_text()?);
236 }
237 let inner = sources.join(", ");
238 let rhs = match mode {
239 ZipMode::Strict => format!("({inner})"),
240 ZipMode::Truncate => format!("zip_truncate({inner})"),
241 ZipMode::Cycle => format!("zip_cycle({inner})"),
242 };
243 Some(format!("({}) in {rhs}", names.join(", ")))
244 }
245 _ => None,
246 }
247 }
248}
249
250fn order_text(strategy: StrategyName, truncation: Option<u64>, seed: Option<u64>) -> String {
254 let name = strategy.as_str();
255 match (truncation, seed) {
256 (Some(n), Some(s)) => format!("{name}(count={n}, seed={s})"),
257 (None, Some(s)) => format!("{name}(seed={s})"),
258 (Some(n), None) => format!("{name}/{n}"),
259 (None, None) => name.to_string(),
260 }
261}
262
263#[cfg(test)]
264mod tests {
265 use super::*;
266 use crate::comprehension::cardinality::{Interval, MeasureName, ProductMeasure};
267 use crate::comprehension::spec::{parse_comprehension_algebra, parse_source};
268
269 #[test]
271 fn every_source_round_trips_through_its_text() {
272 let sources = [
273 Source::IntRange {
274 lo: 1,
275 hi: 10,
276 step: 1,
277 },
278 Source::IntRange {
279 lo: 0,
280 hi: 20,
281 step: 5,
282 },
283 Source::Literal {
284 values: vec![LiteralValue::Int(1), LiteralValue::Int(2)],
285 },
286 Source::Literal {
287 values: vec![LiteralValue::String("load".into())],
288 },
289 Source::Literal {
290 values: vec![
291 LiteralValue::String("load".into()),
292 LiteralValue::String("verify".into()),
293 ],
294 },
295 Source::Literal {
296 values: vec![LiteralValue::Float(1.5), LiteralValue::Float(2.0)],
297 },
298 Source::Literal {
299 values: vec![LiteralValue::Bool(true), LiteralValue::Bool(false)],
300 },
301 Source::Generator {
302 expr: "partitions(\"*/4\", 100)".into(),
303 cardinality_hint: None,
304 },
305 Source::WorkloadParamList {
306 name: "total".into(),
307 len_hint: None,
308 },
309 Source::ContinuousInterval {
310 interval: Interval::half_open(0.0, 1.0),
311 measure: ProductMeasure::Uniform,
312 },
313 Source::Distribution {
314 distribution: MeasureName::Normal,
315 support: MeasureName::Normal.support(&[0.0, 1.0]),
316 params: vec![0.0, 1.0],
317 },
318 Source::Distribution {
319 distribution: MeasureName::Exponential,
320 support: Interval::half_open(0.0, 1.0),
321 params: vec![1.0],
322 },
323 ];
324 for source in sources {
325 let text = source.to_text().expect("the text writes this source");
326 let back = parse_source(&text).unwrap_or_else(|e| panic!("`{text}`: {e:?}"));
327 assert_eq!(back, source, "`{text}`");
328 assert_eq!(back.to_text(), Some(text));
329 }
330 }
331
332 #[test]
335 fn a_source_the_text_cannot_write_has_no_text() {
336 for value in [
337 LiteralValue::Json(serde_json::json!({"a": 1})),
338 LiteralValue::String("a,b".into()),
339 LiteralValue::String("say \"hi\"".into()),
340 LiteralValue::String("[bracketed]".into()),
341 ] {
342 let source = Source::Literal {
343 values: vec![value.clone()],
344 };
345 assert_eq!(source.to_text(), None, "{value:?}");
346 }
347 }
348
349 #[test]
353 fn canonical_text_round_trips_through_the_grammar() {
354 let texts = [
355 "k in 1..4",
356 "k in 1..10..2",
357 "k in 1, 2, 4",
358 "k in load, verify",
359 "x in 0.0..1.0",
360 "x in normal(0.0, 1.0)",
361 "x in exponential(1.0) on 0.0..1.0",
362 "k in 1..4, limit in 10, 20",
363 "(a, b) in (1..4, 10..13)",
364 "(a, b) in zip_truncate(1..4, 10..20)",
365 "(a, b) in zip_cycle(1..4, 10..20)",
366 "k in 1..9 where {k} > 2",
367 "k in 1..9 order lex",
368 "k in 1..9 order halton/3",
369 "k in 1..9 order shuffle(count=3, seed=42)",
370 "k in 1..9 order lhs(seed=7)",
371 "k in 1..9 where {k} > 2 order halton/2",
372 "[ for k in 1..4, for k in 10..13 ]",
373 "[ for k in 1..4 where {k} > 1, for k in 10..13 order lex/2 ]",
374 "[ for k in 1..4, for k in 10..13 ] where {k} > 2 order halton/2",
375 ];
376 for text in texts {
377 let tree =
378 parse_comprehension_algebra(text).unwrap_or_else(|e| panic!("`{text}`: {e}"));
379 let rendered = tree
380 .to_text()
381 .unwrap_or_else(|| panic!("`{text}` has no canonical text"));
382 assert_eq!(rendered, text, "canonical text is not the written text");
383 let back = parse_comprehension_algebra(&rendered)
384 .unwrap_or_else(|e| panic!("`{rendered}`: {e}"));
385 assert_eq!(back, tree, "`{rendered}` parses to a different tree");
386 assert_eq!(back.to_text().as_deref(), Some(rendered.as_str()));
387 }
388 }
389
390 #[test]
392 fn a_tree_outside_the_text_grammar_has_no_text() {
393 let clause = |name: &str| {
394 Comprehension::clause(
395 name,
396 Source::IntRange {
397 lo: 1,
398 hi: 4,
399 step: 1,
400 },
401 )
402 };
403 let inner_filter = Comprehension::cartesian(vec![
406 Comprehension::filter(clause("k"), "{k} > 1"),
407 clause("j"),
408 ]);
409 assert_eq!(inner_filter.to_text(), None);
410 let ordered_zip = Comprehension::zip(
412 vec![
413 Comprehension::order(clause("k"), StrategyName::Lex, Some(2)),
414 clause("j"),
415 ],
416 ZipMode::Strict,
417 );
418 assert_eq!(ordered_zip.to_text(), None);
419 let nested = Comprehension::cartesian(vec![
420 Comprehension::cartesian(vec![clause("k"), clause("j")]),
421 clause("m"),
422 ]);
423 assert_eq!(nested.to_text(), None);
424 assert_eq!(
425 Comprehension::zip(vec![clause("k")], ZipMode::Strict).to_text(),
426 None
427 );
428 }
429}