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(_)
116 | LiteralValue::UInt(_)
117 | LiteralValue::Float(_)
118 | LiteralValue::Bool(_) => true,
119 LiteralValue::String(s) => {
120 let trimmed = s.trim();
121 !trimmed.is_empty()
122 && trimmed == s
123 && !s.contains([
124 '(', ')', '[', ']', '{', '}', '\'', '"', '+', '*', '/', '%', '=', '<', '>',
125 '!', '&', '|', '~', '^', '?', ',', '\n',
126 ])
127 && s.parse::<i64>().is_err()
128 && s.parse::<f64>().is_err()
129 && !s.eq_ignore_ascii_case("true")
130 && !s.eq_ignore_ascii_case("false")
131 }
132 LiteralValue::Json(_) => false,
133 }
134}
135
136fn bare_value_text(v: &LiteralValue) -> Option<String> {
138 Some(match v {
139 LiteralValue::Int(i) => i.to_string(),
140 LiteralValue::UInt(u) => u.to_string(),
141 LiteralValue::Float(f) => format!("{f:?}"),
142 LiteralValue::Bool(b) => b.to_string(),
143 LiteralValue::String(s) => s.clone(),
144 LiteralValue::Json(_) => return None,
145 })
146}
147
148fn quoted_value_text(v: &LiteralValue) -> Option<String> {
153 Some(match v {
154 LiteralValue::String(s) => {
155 if s.contains(['"', '\'', ',', '[', ']', '{', '}', '(', ')', '\n']) {
156 return None;
157 }
158 format!("\"{s}\"")
159 }
160 other => bare_value_text(other)?,
161 })
162}
163
164impl Comprehension {
165 pub fn to_text(&self) -> Option<String> {
172 match self {
173 Comprehension::Order {
174 child,
175 strategy,
176 truncation,
177 seed,
178 } => {
179 let head = match &**child {
180 Comprehension::Filter { child, predicate } => {
181 format!("{} where {predicate}", child.body_text()?)
182 }
183 other => other.body_text()?,
184 };
185 Some(format!(
186 "{head} order {}",
187 order_text(*strategy, *truncation, *seed)
188 ))
189 }
190 Comprehension::Filter { child, predicate } => {
191 Some(format!("{} where {predicate}", child.body_text()?))
192 }
193 other => other.body_text(),
194 }
195 }
196
197 fn body_text(&self) -> Option<String> {
201 match self {
202 Comprehension::Clause { .. } | Comprehension::Zip { .. } => self.clause_text(),
203 Comprehension::Cartesian { children } => children
204 .iter()
205 .map(Comprehension::clause_text)
206 .collect::<Option<Vec<_>>>()
207 .map(|clauses| clauses.join(", ")),
208 Comprehension::Union { children } => {
209 let members = children
210 .iter()
211 .map(|c| c.to_text().map(|text| format!("for {text}")))
212 .collect::<Option<Vec<_>>>()?;
213 Some(format!("[ {} ]", members.join(", ")))
214 }
215 Comprehension::Filter { .. } | Comprehension::Order { .. } => None,
218 }
219 }
220
221 fn clause_text(&self) -> Option<String> {
224 match self {
225 Comprehension::Clause { name, source } => {
226 Some(format!("{name} in {}", source.to_text()?))
227 }
228 Comprehension::Zip { children, mode } => {
229 if children.len() < 2 {
230 return None;
231 }
232 let mut names = Vec::with_capacity(children.len());
233 let mut sources = Vec::with_capacity(children.len());
234 for child in children {
235 let Comprehension::Clause { name, source } = child else {
236 return None;
237 };
238 names.push(name.clone());
239 sources.push(source.to_text()?);
240 }
241 let inner = sources.join(", ");
242 let rhs = match mode {
243 ZipMode::Strict => format!("({inner})"),
244 ZipMode::Truncate => format!("zip_truncate({inner})"),
245 ZipMode::Cycle => format!("zip_cycle({inner})"),
246 };
247 Some(format!("({}) in {rhs}", names.join(", ")))
248 }
249 _ => None,
250 }
251 }
252}
253
254fn order_text(strategy: StrategyName, truncation: Option<u64>, seed: Option<u64>) -> String {
258 let name = strategy.as_str();
259 match (truncation, seed) {
260 (Some(n), Some(s)) => format!("{name}(count={n}, seed={s})"),
261 (None, Some(s)) => format!("{name}(seed={s})"),
262 (Some(n), None) => format!("{name}/{n}"),
263 (None, None) => name.to_string(),
264 }
265}
266
267#[cfg(test)]
268mod tests {
269 use super::*;
270 use crate::comprehension::cardinality::{Interval, MeasureName, ProductMeasure};
271 use crate::comprehension::spec::{parse_comprehension_algebra, parse_source};
272
273 #[test]
275 fn every_source_round_trips_through_its_text() {
276 let sources = [
277 Source::IntRange {
278 lo: 1,
279 hi: 10,
280 step: 1,
281 },
282 Source::IntRange {
283 lo: 0,
284 hi: 20,
285 step: 5,
286 },
287 Source::Literal {
288 values: vec![LiteralValue::Int(1), LiteralValue::Int(2)],
289 },
290 Source::Literal {
291 values: vec![
292 LiteralValue::Int(i64::MAX),
293 LiteralValue::UInt(1 << 63),
294 LiteralValue::UInt(u64::MAX),
295 ],
296 },
297 Source::Literal {
298 values: vec![LiteralValue::String("load".into())],
299 },
300 Source::Literal {
301 values: vec![
302 LiteralValue::String("load".into()),
303 LiteralValue::String("verify".into()),
304 ],
305 },
306 Source::Literal {
307 values: vec![LiteralValue::Float(1.5), LiteralValue::Float(2.0)],
308 },
309 Source::Literal {
310 values: vec![LiteralValue::Bool(true), LiteralValue::Bool(false)],
311 },
312 Source::Generator {
313 expr: "partitions(\"*/4\", 100)".into(),
314 cardinality_hint: None,
315 },
316 Source::WorkloadParamList {
317 name: "total".into(),
318 len_hint: None,
319 },
320 Source::ContinuousInterval {
321 interval: Interval::half_open(0.0, 1.0),
322 measure: ProductMeasure::Uniform,
323 },
324 Source::Distribution {
325 distribution: MeasureName::Normal,
326 support: MeasureName::Normal.support(&[0.0, 1.0]),
327 params: vec![0.0, 1.0],
328 },
329 Source::Distribution {
330 distribution: MeasureName::Exponential,
331 support: Interval::half_open(0.0, 1.0),
332 params: vec![1.0],
333 },
334 ];
335 for source in sources {
336 let text = source.to_text().expect("the text writes this source");
337 let back = parse_source(&text).unwrap_or_else(|e| panic!("`{text}`: {e:?}"));
338 assert_eq!(back, source, "`{text}`");
339 assert_eq!(back.to_text(), Some(text));
340 }
341 }
342
343 #[test]
346 fn a_source_the_text_cannot_write_has_no_text() {
347 for value in [
348 LiteralValue::Json(serde_json::json!({"a": 1})),
349 LiteralValue::String("a,b".into()),
350 LiteralValue::String("say \"hi\"".into()),
351 LiteralValue::String("[bracketed]".into()),
352 ] {
353 let source = Source::Literal {
354 values: vec![value.clone()],
355 };
356 assert_eq!(source.to_text(), None, "{value:?}");
357 }
358 }
359
360 #[test]
364 fn canonical_text_round_trips_through_the_grammar() {
365 let texts = [
366 "k in 1..4",
367 "k in 1..10..2",
368 "k in 1, 2, 4",
369 "k in load, verify",
370 "x in 0.0..1.0",
371 "x in normal(0.0, 1.0)",
372 "x in exponential(1.0) on 0.0..1.0",
373 "k in 1..4, limit in 10, 20",
374 "(a, b) in (1..4, 10..13)",
375 "(a, b) in zip_truncate(1..4, 10..20)",
376 "(a, b) in zip_cycle(1..4, 10..20)",
377 "k in 1..9 where {k} > 2",
378 "k in 1..9 order lex",
379 "k in 1..9 order halton/3",
380 "k in 1..9 order shuffle(count=3, seed=42)",
381 "k in 1..9 order lhs(seed=7)",
382 "k in 1..9 where {k} > 2 order halton/2",
383 "[ for k in 1..4, for k in 10..13 ]",
384 "[ for k in 1..4 where {k} > 1, for k in 10..13 order lex/2 ]",
385 "[ for k in 1..4, for k in 10..13 ] where {k} > 2 order halton/2",
386 ];
387 for text in texts {
388 let tree =
389 parse_comprehension_algebra(text).unwrap_or_else(|e| panic!("`{text}`: {e}"));
390 let rendered = tree
391 .to_text()
392 .unwrap_or_else(|| panic!("`{text}` has no canonical text"));
393 assert_eq!(rendered, text, "canonical text is not the written text");
394 let back = parse_comprehension_algebra(&rendered)
395 .unwrap_or_else(|e| panic!("`{rendered}`: {e}"));
396 assert_eq!(back, tree, "`{rendered}` parses to a different tree");
397 assert_eq!(back.to_text().as_deref(), Some(rendered.as_str()));
398 }
399 }
400
401 #[test]
403 fn a_tree_outside_the_text_grammar_has_no_text() {
404 let clause = |name: &str| {
405 Comprehension::clause(
406 name,
407 Source::IntRange {
408 lo: 1,
409 hi: 4,
410 step: 1,
411 },
412 )
413 };
414 let inner_filter = Comprehension::cartesian(vec![
417 Comprehension::filter(clause("k"), "{k} > 1"),
418 clause("j"),
419 ]);
420 assert_eq!(inner_filter.to_text(), None);
421 let ordered_zip = Comprehension::zip(
423 vec![
424 Comprehension::order(clause("k"), StrategyName::Lex, Some(2)),
425 clause("j"),
426 ],
427 ZipMode::Strict,
428 );
429 assert_eq!(ordered_zip.to_text(), None);
430 let nested = Comprehension::cartesian(vec![
431 Comprehension::cartesian(vec![clause("k"), clause("j")]),
432 clause("m"),
433 ]);
434 assert_eq!(nested.to_text(), None);
435 assert_eq!(
436 Comprehension::zip(vec![clause("k")], ZipMode::Strict).to_text(),
437 None
438 );
439 }
440}