use super::ir::VarId;
use core::fmt;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum OrderError {
Cycle {
vars: Vec<VarId>,
},
UnknownVar {
var: VarId,
},
}
impl fmt::Display for OrderError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
OrderError::Cycle { vars } => write!(
f,
"the `solve ... before` constraints are circular through random variables {vars:?} \
(IEEE 1800-2023 18.5.9)"
),
OrderError::UnknownVar { var } => write!(
f,
"a `solve ... before` edge names random variable #{var} which does not exist"
),
}
}
}
impl std::error::Error for OrderError {}
pub fn levels(n: usize, edges: &[(VarId, VarId)]) -> Result<Vec<usize>, OrderError> {
let mut succ: Vec<Vec<usize>> = vec![Vec::new(); n];
for &(a, b) in edges {
for v in [a, b] {
if v as usize >= n {
return Err(OrderError::UnknownVar { var: v });
}
}
if !succ[a as usize].contains(&(b as usize)) {
succ[a as usize].push(b as usize);
}
}
let mut pred: Vec<Vec<usize>> = vec![Vec::new(); n];
for (a, ss) in succ.iter().enumerate() {
for &b in ss {
pred[b].push(a);
}
}
let mut pending: Vec<usize> = succ.iter().map(Vec::len).collect();
let mut height = vec![0usize; n];
let mut ready: Vec<usize> = (0..n).filter(|&v| pending[v] == 0).collect();
let mut done = 0usize;
while let Some(v) = ready.pop() {
done += 1;
for &p in &pred[v] {
height[p] = height[p].max(height[v] + 1);
pending[p] -= 1;
if pending[p] == 0 {
ready.push(p);
}
}
}
if done < n {
let unfinished: Vec<bool> = pending.iter().map(|&p| p > 0).collect();
let start = (0..n).find(|&v| unfinished[v]).expect("done < n");
let mut seen_at = vec![usize::MAX; n];
let mut path: Vec<usize> = Vec::new();
let mut cur = start;
loop {
if seen_at[cur] != usize::MAX {
let vars = path[seen_at[cur]..].iter().map(|&v| v as VarId).collect();
return Err(OrderError::Cycle { vars });
}
seen_at[cur] = path.len();
path.push(cur);
cur = *succ[cur]
.iter()
.find(|&&s| unfinished[s])
.expect("an unfinished variable has an unfinished successor");
}
}
let top = height.iter().copied().max().unwrap_or(0);
Ok(height.into_iter().map(|h| top - h).collect())
}
pub fn groups(n: usize, edges: &[(VarId, VarId)]) -> Result<Vec<Vec<VarId>>, OrderError> {
let lv = levels(n, edges)?;
let count = lv.iter().copied().max().map_or(0, |m| m + 1);
let mut out: Vec<Vec<VarId>> = vec![Vec::new(); count];
for (v, l) in lv.into_iter().enumerate() {
out[l].push(v as VarId);
}
debug_assert!(out.iter().all(|g| !g.is_empty()));
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn no_edges_is_one_set() {
assert_eq!(levels(3, &[]).unwrap(), vec![0, 0, 0]);
assert_eq!(groups(3, &[]).unwrap(), vec![vec![0, 1, 2]]);
assert_eq!(groups(0, &[]).unwrap(), Vec::<Vec<VarId>>::new());
}
#[test]
fn a_before_b_leaves_unordered_variables_with_the_last_set() {
assert_eq!(levels(3, &[(0, 1)]).unwrap(), vec![0, 1, 1]);
assert_eq!(groups(3, &[(0, 1)]).unwrap(), vec![vec![0], vec![1, 2]]);
}
#[test]
fn a_partially_ordered_variable_is_solved_as_late_as_the_edges_allow() {
let e = [(0, 1), (1, 2), (3, 2)];
assert_eq!(levels(4, &e).unwrap(), vec![0, 1, 2, 1]);
assert_eq!(groups(4, &e).unwrap(), vec![vec![0], vec![1, 3], vec![2]]);
}
#[test]
fn several_names_on_one_side_fan_in_and_out() {
let e = [(0, 2), (0, 3), (1, 2), (1, 3)];
assert_eq!(groups(4, &e).unwrap(), vec![vec![0, 1], vec![2, 3]]);
}
#[test]
fn duplicate_edges_are_harmless() {
assert_eq!(levels(2, &[(0, 1), (0, 1), (0, 1)]).unwrap(), vec![0, 1]);
}
#[test]
fn cycles_are_rejected_and_named() {
assert_eq!(
levels(2, &[(0, 0)]).unwrap_err(),
OrderError::Cycle { vars: vec![0] }
);
let e = levels(3, &[(0, 1), (1, 2), (2, 1)]).unwrap_err();
let OrderError::Cycle { vars } = e else {
panic!("{e:?}")
};
let mut sorted = vars.clone();
sorted.sort_unstable();
assert_eq!(
sorted,
vec![1, 2],
"the cycle, not variable 0 upstream of it"
);
assert!(vars == vec![1, 2] || vars == vec![2, 1]);
}
#[test]
fn an_unknown_variable_is_an_error() {
assert_eq!(
levels(2, &[(0, 5)]).unwrap_err(),
OrderError::UnknownVar { var: 5 }
);
}
#[test]
fn errors_have_messages() {
assert!(
OrderError::Cycle { vars: vec![1, 2] }
.to_string()
.contains("circular")
);
assert!(OrderError::UnknownVar { var: 9 }.to_string().contains("#9"));
}
}