use std::fmt;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Clause {
pub vars: Vec<String>,
pub source: ClauseSource,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum ClauseSource {
Single(String),
Parallel { mode: ZipMode, exprs: Vec<String> },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum ZipMode {
#[default]
Strict,
Truncate,
Cycle,
}
impl Clause {
pub fn new(var: impl Into<String>, expr: impl Into<String>) -> Self {
Self {
vars: vec![var.into()],
source: ClauseSource::Single(expr.into()),
}
}
pub fn parallel(
vars: impl IntoIterator<Item = impl Into<String>>,
exprs: impl IntoIterator<Item = impl Into<String>>,
) -> Self {
Self::parallel_with_mode(ZipMode::Strict, vars, exprs)
}
pub fn parallel_with_mode(
mode: ZipMode,
vars: impl IntoIterator<Item = impl Into<String>>,
exprs: impl IntoIterator<Item = impl Into<String>>,
) -> Self {
let vars: Vec<String> = vars.into_iter().map(Into::into).collect();
let exprs: Vec<String> = exprs.into_iter().map(Into::into).collect();
assert_eq!(vars.len(), exprs.len(),
"Clause::parallel: vars and exprs must have equal length");
assert!(vars.len() >= 2,
"Clause::parallel: parallel form requires ≥ 2 variables (use Clause::new for single-var)");
Self { vars, source: ClauseSource::Parallel { mode, exprs } }
}
pub fn single_var(&self) -> Option<&str> {
if self.vars.len() == 1 { Some(&self.vars[0]) } else { None }
}
pub fn single_expr(&self) -> Option<&str> {
match &self.source {
ClauseSource::Single(s) => Some(s),
ClauseSource::Parallel { .. } => None,
}
}
pub fn is_parallel(&self) -> bool {
matches!(self.source, ClauseSource::Parallel { .. })
}
pub fn first_var(&self) -> &str {
&self.vars[0]
}
pub fn var(&self) -> &str {
&self.vars[0]
}
pub fn expr(&self) -> &str {
match &self.source {
ClauseSource::Single(s) => s,
ClauseSource::Parallel { exprs, .. } => &exprs[0],
}
}
pub fn scalar_bindings(&self) -> Vec<(&str, &str)> {
match &self.source {
ClauseSource::Single(s) => {
self.vars.iter().map(|v| (v.as_str(), s.as_str())).collect()
}
ClauseSource::Parallel { exprs, .. } => {
self.vars.iter().zip(exprs.iter())
.map(|(v, e)| (v.as_str(), e.as_str()))
.collect()
}
}
}
}
impl fmt::Display for Clause {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.source {
ClauseSource::Single(s) => write!(f, "{} in {}", self.vars[0], s),
ClauseSource::Parallel { mode, exprs } => {
write!(f, "({}) in ", self.vars.join(", "))?;
let inner = exprs.join(", ");
match mode {
ZipMode::Strict => write!(f, "({inner})"),
ZipMode::Truncate => write!(f, "zip_truncate({inner})"),
ZipMode::Cycle => write!(f, "zip_cycle({inner})"),
}
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ComprehensionMode {
Cartesian(Vec<Clause>),
Union(Vec<Subspace>),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Subspace {
pub clauses: Vec<Clause>,
}
impl Subspace {
pub fn new(clauses: Vec<Clause>) -> Self { Self { clauses } }
pub fn is_empty(&self) -> bool { self.clauses.is_empty() }
pub fn len(&self) -> usize { self.clauses.len() }
pub fn iter(&self) -> std::slice::Iter<'_, Clause> { self.clauses.iter() }
}
impl<'a> IntoIterator for &'a Subspace {
type Item = &'a Clause;
type IntoIter = std::slice::Iter<'a, Clause>;
fn into_iter(self) -> Self::IntoIter { self.clauses.iter() }
}
impl From<Vec<Clause>> for Subspace {
fn from(clauses: Vec<Clause>) -> Self { Self { clauses } }
}
impl std::ops::Index<usize> for Subspace {
type Output = Clause;
fn index(&self, i: usize) -> &Clause { &self.clauses[i] }
}
impl std::ops::Deref for Subspace {
type Target = [Clause];
fn deref(&self) -> &[Clause] { &self.clauses }
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum TraversalOrder {
Lex { #[serde(default, skip_serializing_if = "Option::is_none")] count: Option<usize> },
ReverseLex { #[serde(default, skip_serializing_if = "Option::is_none")] count: Option<usize> },
Diagonal { #[serde(default, skip_serializing_if = "Option::is_none")] count: Option<usize> },
Antidiagonal { #[serde(default, skip_serializing_if = "Option::is_none")] count: Option<usize> },
Extrema { #[serde(default, skip_serializing_if = "Option::is_none")] strata: Option<usize> },
Shells {
#[serde(default)]
origin: ShellOrigin,
#[serde(default, skip_serializing_if = "Option::is_none")]
depth: Option<usize>,
},
Halton { #[serde(default, skip_serializing_if = "Option::is_none")] count: Option<usize> },
Sobol { #[serde(default, skip_serializing_if = "Option::is_none")] count: Option<usize> },
Lhs {
#[serde(default, skip_serializing_if = "Option::is_none")]
count: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
seed: Option<u64>,
},
Custom { function: String },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum ShellOrigin {
#[default]
Outer,
Center,
Corner,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Comprehension {
pub mode: ComprehensionMode,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub filter: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub order: Option<TraversalOrder>,
}
impl Comprehension {
pub fn cartesian(clauses: Vec<Clause>) -> Self {
Self {
mode: ComprehensionMode::Cartesian(clauses),
filter: None,
order: None,
}
}
pub fn union(subspaces: Vec<Vec<Clause>>) -> Self {
Self {
mode: ComprehensionMode::Union(
subspaces.into_iter().map(Subspace::new).collect()
),
filter: None,
order: None,
}
}
pub fn union_from(subspaces: Vec<Subspace>) -> Self {
Self {
mode: ComprehensionMode::Union(subspaces),
filter: None,
order: None,
}
}
pub fn with_filter(mut self, predicate: impl Into<String>) -> Self {
self.filter = Some(predicate.into());
self
}
pub fn with_order(mut self, order: TraversalOrder) -> Self {
self.order = Some(order);
self
}
pub fn coordinate_names(&self) -> Vec<&str> {
let mut out: Vec<&str> = Vec::new();
for clause in self.flat_clauses() {
for v in &clause.vars {
if !out.iter().any(|n| *n == v.as_str()) {
out.push(v);
}
}
}
out
}
pub fn flat_clauses(&self) -> Vec<&Clause> {
match &self.mode {
ComprehensionMode::Cartesian(clauses) => clauses.iter().collect(),
ComprehensionMode::Union(subspaces) => {
let mut out = Vec::new();
for sub in subspaces {
for clause in &sub.clauses {
out.push(clause);
}
}
out
}
}
}
pub fn clause_count(&self) -> usize {
self.flat_clauses().len()
}
pub fn is_cartesian(&self) -> bool {
matches!(self.mode, ComprehensionMode::Cartesian(_))
}
pub fn is_union(&self) -> bool {
matches!(self.mode, ComprehensionMode::Union(_))
}
pub fn validate(&self) -> Result<(), Vec<String>> {
let mut errors: Vec<String> = Vec::new();
match &self.mode {
ComprehensionMode::Cartesian(clauses) => {
if clauses.is_empty() {
errors.push("Cartesian comprehension has no clauses".to_string());
}
let mut seen: Vec<&str> = Vec::new();
for clause in clauses {
for v in &clause.vars {
if seen.iter().any(|n| *n == v.as_str()) {
errors.push(format!(
"Cartesian comprehension repeats variable name '{v}' \
— name collision across clauses (use Union mode for \
alternative sub-spaces with shared coordinate names)"
));
} else {
seen.push(v.as_str());
}
}
}
}
ComprehensionMode::Union(subspaces) => {
if subspaces.is_empty() {
errors.push("Union comprehension has no sub-spaces".to_string());
}
for (i, sub) in subspaces.iter().enumerate() {
if sub.is_empty() {
errors.push(format!(
"Union sub-space #{i} has no clauses"
));
}
}
}
}
if let Err(e) = check_order_for_mode(&self.mode, &self.order) {
errors.push(e);
}
if errors.is_empty() { Ok(()) } else { Err(errors) }
}
}
pub(crate) fn check_order_for_mode(
mode: &ComprehensionMode,
order: &Option<TraversalOrder>,
) -> Result<(), String> {
let ComprehensionMode::Union(_) = mode else { return Ok(()); };
let Some(order) = order else { return Ok(()); };
let strategy_name = match order {
TraversalOrder::Lex { .. } => return Ok(()),
TraversalOrder::Custom { .. } => return Ok(()),
TraversalOrder::ReverseLex { .. } => "reverse_lex",
TraversalOrder::Diagonal { .. } => "diagonal",
TraversalOrder::Antidiagonal { .. } => "antidiagonal",
TraversalOrder::Extrema { .. } => "extrema",
TraversalOrder::Shells { .. } => "shells",
TraversalOrder::Halton { .. } => "halton",
TraversalOrder::Sobol { .. } => "sobol",
TraversalOrder::Lhs { .. } => "lhs",
};
Err(format!(
"ordering '{strategy_name}' has no defined behavior on Union mode \
(no single Cartesian lattice). Use Cartesian mode, or pick \
'lex' / 'custom' which are well-defined on Union."
))
}
impl fmt::Display for Comprehension {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.mode {
ComprehensionMode::Cartesian(clauses) => {
let parts: Vec<String> = clauses.iter().map(|c| c.to_string()).collect();
write!(f, "{}", parts.join(", "))?;
}
ComprehensionMode::Union(subspaces) => {
let parts: Vec<String> = subspaces.iter().map(|sub| {
let inner: Vec<String> = sub.clauses.iter()
.map(|c| c.to_string()).collect();
inner.join(", ")
}).collect();
write!(f, "{}", parts.join(" | "))?;
}
}
if let Some(predicate) = &self.filter {
write!(f, " where {predicate}")?;
}
if let Some(order) = &self.order {
write!(f, " order {}", format_order(order))?;
}
Ok(())
}
}
fn format_order(order: &TraversalOrder) -> String {
fn count_suffix(n: Option<usize>) -> String {
n.map(|n| format!("/{n}")).unwrap_or_default()
}
match order {
TraversalOrder::Lex { count } => format!("lex{}", count_suffix(*count)),
TraversalOrder::ReverseLex { count } => format!("reverse_lex{}", count_suffix(*count)),
TraversalOrder::Diagonal { count } => format!("diagonal{}", count_suffix(*count)),
TraversalOrder::Antidiagonal { count } => format!("antidiagonal{}", count_suffix(*count)),
TraversalOrder::Extrema { strata } => format!("extrema{}", count_suffix(*strata)),
TraversalOrder::Shells { origin, depth } => {
let origin_part = match origin {
ShellOrigin::Outer => "",
ShellOrigin::Center => "/center",
ShellOrigin::Corner => "/corner",
};
format!("shells{}{}", origin_part, count_suffix(*depth))
}
TraversalOrder::Halton { count } => format!("halton{}", count_suffix(*count)),
TraversalOrder::Sobol { count } => format!("sobol{}", count_suffix(*count)),
TraversalOrder::Lhs { count, seed } => {
let mut s = format!("lhs{}", count_suffix(*count));
if let Some(k) = seed { s.push_str(&format!(" seed={k}")); }
s
}
TraversalOrder::Custom { function } => format!("custom({function})"),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cartesian_coordinate_names_in_declaration_order() {
let c = Comprehension::cartesian(vec![
Clause::new("k", "{k_values}"),
Clause::new("limit", "{k_{k}_limits}"),
]);
assert_eq!(c.coordinate_names(), vec!["k", "limit"]);
assert!(c.is_cartesian());
assert_eq!(c.clause_count(), 2);
}
#[test]
fn union_dedupes_repeated_names_first_occurrence_wins() {
let c = Comprehension::union(vec![
vec![Clause::new("k", "10"), Clause::new("limit", "10,20,30")],
vec![Clause::new("k", "100"), Clause::new("limit", "100,200,300")],
]);
assert_eq!(c.coordinate_names(), vec!["k", "limit"]);
assert!(c.is_union());
assert_eq!(c.flat_clauses().len(), 4);
assert_eq!(c.clause_count(), 4);
}
#[test]
fn single_clause_cartesian_is_the_simple_form() {
let c = Comprehension::cartesian(vec![
Clause::new("profile", "matching_profiles('{dataset}', '{prefix}')"),
]);
assert_eq!(c.coordinate_names(), vec!["profile"]);
assert_eq!(c.clause_count(), 1);
}
#[test]
fn union_with_distinct_names_per_subspace_keeps_all_in_order() {
let c = Comprehension::union(vec![
vec![Clause::new("a", "1")],
vec![Clause::new("b", "2")],
]);
assert_eq!(c.coordinate_names(), vec!["a", "b"]);
}
#[test]
fn display_single_var_clause() {
let c = Clause::new("k", "1..10");
assert_eq!(c.to_string(), "k in 1..10");
}
#[test]
fn display_parallel_clause_strict() {
let c = Clause::parallel(["x", "y"], ["fib(8)", "pow2(8)"]);
assert_eq!(c.to_string(), "(x, y) in (fib(8), pow2(8))");
}
#[test]
fn display_parallel_clause_truncate() {
let c = Clause::parallel_with_mode(
ZipMode::Truncate, ["x", "y"], ["fib(8)", "pow2(4)"]
);
assert_eq!(c.to_string(), "(x, y) in zip_truncate(fib(8), pow2(4))");
}
#[test]
fn display_parallel_clause_cycle() {
let c = Clause::parallel_with_mode(
ZipMode::Cycle, ["x", "y"], ["1..4", "10..20..10"]
);
assert_eq!(c.to_string(), "(x, y) in zip_cycle(1..4, 10..20..10)");
}
#[test]
fn display_cartesian_comprehension() {
let c = Comprehension::cartesian(vec![
Clause::new("k", "1..10"),
Clause::new("limit", "10,20,30"),
]);
assert_eq!(c.to_string(), "k in 1..10, limit in 10,20,30");
}
#[test]
fn display_comprehension_with_filter_and_order() {
let c = Comprehension::cartesian(vec![Clause::new("k", "1..10")])
.with_filter("{k} > 3")
.with_order(TraversalOrder::Extrema { strata: Some(2) });
assert_eq!(c.to_string(), "k in 1..10 where {k} > 3 order extrema/2");
}
#[test]
fn validate_accepts_valid_cartesian() {
let c = Comprehension::cartesian(vec![
Clause::new("k", "1..10"),
Clause::new("limit", "10,20,30"),
]);
assert!(c.validate().is_ok());
}
#[test]
fn validate_accepts_valid_union() {
let c = Comprehension::union(vec![
vec![Clause::new("k", "10"), Clause::new("limit", "10,20")],
vec![Clause::new("k", "100"), Clause::new("limit", "100,200")],
]);
assert!(c.validate().is_ok());
}
#[test]
fn validate_rejects_empty_cartesian() {
let c = Comprehension::cartesian(vec![]);
let errs = c.validate().unwrap_err();
assert!(errs.iter().any(|e| e.contains("no clauses")), "got: {errs:?}");
}
#[test]
fn validate_rejects_empty_union() {
let c = Comprehension::union(vec![]);
let errs = c.validate().unwrap_err();
assert!(errs.iter().any(|e| e.contains("no sub-spaces")), "got: {errs:?}");
}
#[test]
fn validate_rejects_empty_subspace_inside_union() {
let c = Comprehension::union(vec![
vec![Clause::new("k", "10")],
vec![], ]);
let errs = c.validate().unwrap_err();
assert!(errs.iter().any(|e| e.contains("Union sub-space #1 has no clauses")),
"got: {errs:?}");
}
#[test]
fn validate_rejects_cartesian_name_collision() {
let c = Comprehension::cartesian(vec![
Clause::new("k", "10"),
Clause::new("k", "20"), ]);
let errs = c.validate().unwrap_err();
assert!(errs.iter().any(|e| e.contains("repeats variable name 'k'")),
"got: {errs:?}");
}
#[test]
fn validate_rejects_cartesian_collision_with_parallel_clause() {
let c = Comprehension::cartesian(vec![
Clause::parallel(["x", "y"], ["1..10", "10..100..10"]),
Clause::new("y", "100"), ]);
let errs = c.validate().unwrap_err();
assert!(errs.iter().any(|e| e.contains("repeats variable name 'y'")),
"got: {errs:?}");
}
#[test]
fn validate_permits_repeated_names_across_union_subspaces() {
let c = Comprehension::union(vec![
vec![Clause::new("k", "10")],
vec![Clause::new("k", "100")],
]);
assert!(c.validate().is_ok());
}
#[test]
fn display_union_uses_pipe_separator_per_subspace() {
let c = Comprehension::union(vec![
vec![Clause::new("k", "10"), Clause::new("limit", "10,20")],
vec![Clause::new("k", "100"), Clause::new("limit", "100,200")],
]);
assert_eq!(c.to_string(),
"k in 10, limit in 10,20 | k in 100, limit in 100,200");
}
}