use super::ast::Comprehension;
use super::source::{LiteralValue, Source};
use super::strategy::{StrategyName, ZipMode};
impl Source {
pub fn to_text(&self) -> Option<String> {
Some(match self {
Source::IntRange { lo, hi, step } => {
if *step == 1 {
format!("{lo}..{hi}")
} else {
format!("{lo}..{hi}..{step}")
}
}
Source::Literal { values } => literal_list_text(values)?,
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,
support,
params,
} => {
let args = params
.iter()
.map(|p| format!("{p:?}"))
.collect::<Vec<_>>()
.join(", ");
let call = format!("{}({args})", distribution.text());
if *support == distribution.support(params) {
call
} else if support.hi_open {
format!("{call} on {:?}..{:?}", support.lo, support.hi)
} else {
format!("{call} on {:?}..={:?}", support.lo, support.hi)
}
}
})
}
}
fn literal_list_text(values: &[LiteralValue]) -> Option<String> {
if values.is_empty() {
return None;
}
let bare_ok = values.iter().all(bare_value_is_unambiguous)
&& (values.len() > 1 || !matches!(values[0], LiteralValue::String(_)));
if bare_ok {
let items = values
.iter()
.map(bare_value_text)
.collect::<Option<Vec<_>>>()?;
return Some(items.join(", "));
}
let items = values
.iter()
.map(quoted_value_text)
.collect::<Option<Vec<_>>>()?;
Some(format!("[{}]", items.join(", ")))
}
fn bare_value_is_unambiguous(v: &LiteralValue) -> bool {
match v {
LiteralValue::Int(_)
| LiteralValue::UInt(_)
| LiteralValue::Float(_)
| LiteralValue::Bool(_) => true,
LiteralValue::String(s) => {
let trimmed = s.trim();
!trimmed.is_empty()
&& trimmed == s
&& !s.contains([
'(', ')', '[', ']', '{', '}', '\'', '"', '+', '*', '/', '%', '=', '<', '>',
'!', '&', '|', '~', '^', '?', ',', '\n',
])
&& s.parse::<i64>().is_err()
&& s.parse::<f64>().is_err()
&& !s.eq_ignore_ascii_case("true")
&& !s.eq_ignore_ascii_case("false")
}
LiteralValue::Json(_) => false,
}
}
fn bare_value_text(v: &LiteralValue) -> Option<String> {
Some(match v {
LiteralValue::Int(i) => i.to_string(),
LiteralValue::UInt(u) => u.to_string(),
LiteralValue::Float(f) => format!("{f:?}"),
LiteralValue::Bool(b) => b.to_string(),
LiteralValue::String(s) => s.clone(),
LiteralValue::Json(_) => return None,
})
}
fn quoted_value_text(v: &LiteralValue) -> Option<String> {
Some(match v {
LiteralValue::String(s) => {
if s.contains(['"', '\'', ',', '[', ']', '{', '}', '(', ')', '\n']) {
return None;
}
format!("\"{s}\"")
}
other => bare_value_text(other)?,
})
}
impl Comprehension {
pub fn to_text(&self) -> Option<String> {
match self {
Comprehension::Order {
child,
strategy,
truncation,
seed,
} => {
let head = match &**child {
Comprehension::Filter { child, predicate } => {
format!("{} where {predicate}", child.body_text()?)
}
other => other.body_text()?,
};
Some(format!(
"{head} order {}",
order_text(*strategy, *truncation, *seed)
))
}
Comprehension::Filter { child, predicate } => {
Some(format!("{} where {predicate}", child.body_text()?))
}
other => other.body_text(),
}
}
fn body_text(&self) -> Option<String> {
match self {
Comprehension::Clause { .. } | Comprehension::Zip { .. } => self.clause_text(),
Comprehension::Cartesian { children } => children
.iter()
.map(Comprehension::clause_text)
.collect::<Option<Vec<_>>>()
.map(|clauses| clauses.join(", ")),
Comprehension::Union { children } => {
let members = children
.iter()
.map(|c| c.to_text().map(|text| format!("for {text}")))
.collect::<Option<Vec<_>>>()?;
Some(format!("[ {} ]", members.join(", ")))
}
Comprehension::Filter { .. } | Comprehension::Order { .. } => None,
}
}
fn clause_text(&self) -> Option<String> {
match self {
Comprehension::Clause { name, source } => {
Some(format!("{name} in {}", source.to_text()?))
}
Comprehension::Zip { children, mode } => {
if children.len() < 2 {
return None;
}
let mut names = Vec::with_capacity(children.len());
let mut sources = Vec::with_capacity(children.len());
for child in children {
let Comprehension::Clause { name, source } = child else {
return None;
};
names.push(name.clone());
sources.push(source.to_text()?);
}
let inner = sources.join(", ");
let rhs = match mode {
ZipMode::Strict => format!("({inner})"),
ZipMode::Truncate => format!("zip_truncate({inner})"),
ZipMode::Cycle => format!("zip_cycle({inner})"),
};
Some(format!("({}) in {rhs}", names.join(", ")))
}
_ => None,
}
}
}
fn order_text(strategy: StrategyName, truncation: Option<u64>, seed: Option<u64>) -> String {
let name = strategy.as_str();
match (truncation, seed) {
(Some(n), Some(s)) => format!("{name}(count={n}, seed={s})"),
(None, Some(s)) => format!("{name}(seed={s})"),
(Some(n), None) => format!("{name}/{n}"),
(None, None) => name.to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::comprehension::cardinality::{Interval, MeasureName, ProductMeasure};
use crate::comprehension::spec::{parse_comprehension_algebra, parse_source};
#[test]
fn every_source_round_trips_through_its_text() {
let sources = [
Source::IntRange {
lo: 1,
hi: 10,
step: 1,
},
Source::IntRange {
lo: 0,
hi: 20,
step: 5,
},
Source::Literal {
values: vec![LiteralValue::Int(1), LiteralValue::Int(2)],
},
Source::Literal {
values: vec![
LiteralValue::Int(i64::MAX),
LiteralValue::UInt(1 << 63),
LiteralValue::UInt(u64::MAX),
],
},
Source::Literal {
values: vec![LiteralValue::String("load".into())],
},
Source::Literal {
values: vec![
LiteralValue::String("load".into()),
LiteralValue::String("verify".into()),
],
},
Source::Literal {
values: vec![LiteralValue::Float(1.5), LiteralValue::Float(2.0)],
},
Source::Literal {
values: vec![LiteralValue::Bool(true), LiteralValue::Bool(false)],
},
Source::Generator {
expr: "partitions(\"*/4\", 100)".into(),
cardinality_hint: None,
},
Source::WorkloadParamList {
name: "total".into(),
len_hint: None,
},
Source::ContinuousInterval {
interval: Interval::half_open(0.0, 1.0),
measure: ProductMeasure::Uniform,
},
Source::Distribution {
distribution: MeasureName::Normal,
support: MeasureName::Normal.support(&[0.0, 1.0]),
params: vec![0.0, 1.0],
},
Source::Distribution {
distribution: MeasureName::Exponential,
support: Interval::half_open(0.0, 1.0),
params: vec![1.0],
},
];
for source in sources {
let text = source.to_text().expect("the text writes this source");
let back = parse_source(&text).unwrap_or_else(|e| panic!("`{text}`: {e:?}"));
assert_eq!(back, source, "`{text}`");
assert_eq!(back.to_text(), Some(text));
}
}
#[test]
fn a_source_the_text_cannot_write_has_no_text() {
for value in [
LiteralValue::Json(serde_json::json!({"a": 1})),
LiteralValue::String("a,b".into()),
LiteralValue::String("say \"hi\"".into()),
LiteralValue::String("[bracketed]".into()),
] {
let source = Source::Literal {
values: vec![value.clone()],
};
assert_eq!(source.to_text(), None, "{value:?}");
}
}
#[test]
fn canonical_text_round_trips_through_the_grammar() {
let texts = [
"k in 1..4",
"k in 1..10..2",
"k in 1, 2, 4",
"k in load, verify",
"x in 0.0..1.0",
"x in normal(0.0, 1.0)",
"x in exponential(1.0) on 0.0..1.0",
"k in 1..4, limit in 10, 20",
"(a, b) in (1..4, 10..13)",
"(a, b) in zip_truncate(1..4, 10..20)",
"(a, b) in zip_cycle(1..4, 10..20)",
"k in 1..9 where {k} > 2",
"k in 1..9 order lex",
"k in 1..9 order halton/3",
"k in 1..9 order shuffle(count=3, seed=42)",
"k in 1..9 order lhs(seed=7)",
"k in 1..9 where {k} > 2 order halton/2",
"[ for k in 1..4, for k in 10..13 ]",
"[ for k in 1..4 where {k} > 1, for k in 10..13 order lex/2 ]",
"[ for k in 1..4, for k in 10..13 ] where {k} > 2 order halton/2",
];
for text in texts {
let tree =
parse_comprehension_algebra(text).unwrap_or_else(|e| panic!("`{text}`: {e}"));
let rendered = tree
.to_text()
.unwrap_or_else(|| panic!("`{text}` has no canonical text"));
assert_eq!(rendered, text, "canonical text is not the written text");
let back = parse_comprehension_algebra(&rendered)
.unwrap_or_else(|e| panic!("`{rendered}`: {e}"));
assert_eq!(back, tree, "`{rendered}` parses to a different tree");
assert_eq!(back.to_text().as_deref(), Some(rendered.as_str()));
}
}
#[test]
fn a_tree_outside_the_text_grammar_has_no_text() {
let clause = |name: &str| {
Comprehension::clause(
name,
Source::IntRange {
lo: 1,
hi: 4,
step: 1,
},
)
};
let inner_filter = Comprehension::cartesian(vec![
Comprehension::filter(clause("k"), "{k} > 1"),
clause("j"),
]);
assert_eq!(inner_filter.to_text(), None);
let ordered_zip = Comprehension::zip(
vec![
Comprehension::order(clause("k"), StrategyName::Lex, Some(2)),
clause("j"),
],
ZipMode::Strict,
);
assert_eq!(ordered_zip.to_text(), None);
let nested = Comprehension::cartesian(vec![
Comprehension::cartesian(vec![clause("k"), clause("j")]),
clause("m"),
]);
assert_eq!(nested.to_text(), None);
assert_eq!(
Comprehension::zip(vec![clause("k")], ZipMode::Strict).to_text(),
None
);
}
}