use std::{
collections::{BTreeMap, HashMap, HashSet},
sync::Arc,
};
use crate::{
eval::{
error::EvalError,
reduce::{Scope, reduce_expression},
},
ir::{Address, Expression, Local, SymbolKind},
};
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct CycleParticipant {
pub address: Address,
}
impl CycleParticipant {
#[must_use]
pub const fn new(address: Address) -> Self {
Self { address }
}
}
pub fn solve_locals(locals: &[Local], scope: &mut Scope) -> Result<Vec<Local>, EvalError> {
check_cycle(locals)?;
let mut out: Vec<Local> = locals.to_vec();
let mut converged: HashSet<Arc<str>> = HashSet::new();
let max_iters = locals.len().saturating_add(1);
for _ in 0..max_iters {
let mut made_progress = false;
for local in &mut out {
if converged.contains(&local.name) {
continue;
}
let reduced = reduce_expression(&local.value, scope);
let progressed = reduced != local.value;
local.value = reduced;
if let Expression::Literal(v) = &local.value {
scope.locals.push((Arc::clone(&local.name), v.clone()));
converged.insert(Arc::clone(&local.name));
made_progress = true;
} else if progressed {
made_progress = true;
}
}
if !made_progress {
break;
}
}
Ok(out)
}
pub(super) fn check_cycle(locals: &[Local]) -> Result<(), EvalError> {
let name_to_idx: HashMap<Arc<str>, usize> = locals
.iter()
.enumerate()
.map(|(i, l)| (Arc::clone(&l.name), i))
.collect();
let mut edges: Vec<Vec<usize>> = (0..locals.len()).map(|_| Vec::new()).collect();
for (i, local) in locals.iter().enumerate() {
let Some(edge_list) = edges.get_mut(i) else {
continue;
};
let mut deps: HashSet<Arc<str>> = HashSet::new();
collect_local_refs(&local.value, &mut deps);
for dep in deps {
if let Some(j) = name_to_idx.get(&dep) {
edge_list.push(*j);
}
}
}
if let Some(scc) = tarjan_first_cycle(&edges) {
let mut addrs: BTreeMap<String, Address> = BTreeMap::new();
for idx in scc {
if let Some(local) = locals.get(idx) {
let addr_str = format!("local.{}", local.name);
if let Ok(addr) = Address::new(&addr_str) {
addrs.insert(addr_str, addr);
}
}
}
return Err(EvalError::Cycle {
participants: addrs.into_values().collect(),
});
}
Ok(())
}
fn tarjan_first_cycle(edges: &[Vec<usize>]) -> Option<Vec<usize>> {
struct State {
index: u32,
stack: Vec<usize>,
on_stack: Vec<bool>,
indices: Vec<Option<u32>>,
lowlinks: Vec<u32>,
found: Option<Vec<usize>>,
}
fn strongconnect(v: usize, edges: &[Vec<usize>], st: &mut State) {
if st.found.is_some() {
return;
}
if let (Some(slot), Some(low)) = (st.indices.get_mut(v), st.lowlinks.get_mut(v)) {
*slot = Some(st.index);
*low = st.index;
}
st.index += 1;
st.stack.push(v);
if let Some(flag) = st.on_stack.get_mut(v) {
*flag = true;
}
let outgoing = edges.get(v).map_or(&[][..], Vec::as_slice);
for &w in outgoing {
let visited = st.indices.get(w).copied().flatten();
if visited.is_none() {
strongconnect(w, edges, st);
let merged = st.lowlinks.get(v).copied().unwrap_or(u32::MAX);
let from_w = st.lowlinks.get(w).copied().unwrap_or(u32::MAX);
if let Some(slot) = st.lowlinks.get_mut(v) {
*slot = merged.min(from_w);
}
} else if st.on_stack.get(w).copied().unwrap_or(false) {
let merged = st.lowlinks.get(v).copied().unwrap_or(u32::MAX);
let from_w = visited.unwrap_or(u32::MAX);
if let Some(slot) = st.lowlinks.get_mut(v) {
*slot = merged.min(from_w);
}
}
}
let low_v = st.lowlinks.get(v).copied();
let idx_v = st.indices.get(v).copied().flatten();
if low_v == idx_v {
let mut scc: Vec<usize> = Vec::new();
while let Some(w) = st.stack.pop() {
if let Some(flag) = st.on_stack.get_mut(w) {
*flag = false;
}
scc.push(w);
if w == v {
break;
}
}
let is_self_cycle = scc.len() == 1 && edges.get(v).is_some_and(|out| out.contains(&v));
if scc.len() > 1 || is_self_cycle {
st.found = Some(scc);
}
}
}
let n = edges.len();
let mut st = State {
index: 0,
stack: Vec::new(),
on_stack: vec![false; n],
indices: vec![None; n],
lowlinks: vec![0; n],
found: None,
};
for v in 0..n {
let already = st.indices.get(v).copied().flatten();
if already.is_none() {
strongconnect(v, edges, &mut st);
if st.found.is_some() {
break;
}
}
}
st.found
}
fn collect_local_refs(expr: &Expression, out: &mut HashSet<Arc<str>>) {
match expr {
Expression::Literal(_) => {}
Expression::Unresolved(s) => {
if matches!(s.kind, SymbolKind::Local) {
let rest = s.source.strip_prefix("local.").unwrap_or(&s.source);
let name = rest.split('.').next().unwrap_or(rest);
if !name.is_empty() {
out.insert(Arc::from(name));
}
}
}
Expression::BinaryOp { lhs, rhs, .. } => {
collect_local_refs(lhs, out);
collect_local_refs(rhs, out);
}
Expression::UnaryOp { operand, .. } => collect_local_refs(operand, out),
Expression::TemplateConcat(parts) | Expression::Array(parts) => {
for p in parts {
collect_local_refs(p, out);
}
}
Expression::Object(entries) => {
for (k, v) in entries {
collect_local_refs(k, out);
collect_local_refs(v, out);
}
}
Expression::FuncCall(call) => {
for a in &call.args {
collect_local_refs(a, out);
}
}
Expression::Conditional(c) => {
collect_local_refs(&c.cond, out);
collect_local_refs(&c.then_branch, out);
collect_local_refs(&c.else_branch, out);
}
Expression::For(f) => {
collect_local_refs(&f.collection, out);
collect_local_refs(&f.value, out);
if let Some(k) = &f.key {
collect_local_refs(k, out);
}
if let Some(c) = &f.cond {
collect_local_refs(c, out);
}
}
}
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::indexing_slicing
)]
mod tests {
use std::sync::Arc;
use super::*;
use crate::ir::{Expression, Local, Span, SymbolKind, Symbolic, Value};
fn local(name: &str, expr: Expression) -> Local {
Local::builder()
.name(Arc::<str>::from(name))
.value(expr)
.span(Span::synthetic())
.build()
}
fn local_ref(name: &str) -> Expression {
Expression::Unresolved(
Symbolic::builder()
.kind(SymbolKind::Local)
.source(Arc::<str>::from(format!("local.{name}")))
.span(Span::synthetic())
.build(),
)
}
#[test]
fn test_cycle_check_accepts_acyclic_locals() {
let locals = vec![
local("a", Expression::Literal(Value::Int(1))),
local("b", local_ref("a")),
];
check_cycle(&locals).expect("acyclic");
}
#[test]
fn test_cycle_check_rejects_self_cycle() {
let locals = vec![local("a", local_ref("a"))];
let err = check_cycle(&locals).unwrap_err();
let EvalError::Cycle { participants } = err else {
panic!("expected cycle");
};
assert_eq!(participants.len(), 1);
assert_eq!(participants[0].as_str(), "local.a");
}
#[test]
fn test_cycle_check_rejects_two_node_cycle() {
let locals = vec![local("a", local_ref("b")), local("b", local_ref("a"))];
let err = check_cycle(&locals).unwrap_err();
let EvalError::Cycle { participants } = err else {
panic!("expected cycle");
};
assert_eq!(participants.len(), 2);
assert_eq!(participants[0].as_str(), "local.a");
assert_eq!(participants[1].as_str(), "local.b");
}
#[test]
fn test_cycle_check_rejects_three_node_cycle() {
let locals = vec![
local("a", local_ref("b")),
local("b", local_ref("c")),
local("c", local_ref("a")),
];
let err = check_cycle(&locals).unwrap_err();
let EvalError::Cycle { participants } = err else {
panic!("expected cycle");
};
let names: Vec<&str> = participants.iter().map(Address::as_str).collect();
assert_eq!(names, vec!["local.a", "local.b", "local.c"]);
}
#[test]
fn test_cycle_check_finds_cycle_among_acyclic_neighbours() {
let locals = vec![
local("x", Expression::Literal(Value::Int(0))),
local("a", local_ref("b")),
local("b", local_ref("a")),
];
let err = check_cycle(&locals).unwrap_err();
assert!(matches!(err, EvalError::Cycle { .. }));
}
#[test]
fn test_cycle_participants_are_deterministic_order() {
let locals_a = vec![local("a", local_ref("b")), local("b", local_ref("a"))];
let locals_b = vec![local("b", local_ref("a")), local("a", local_ref("b"))];
let EvalError::Cycle { participants: pa } = check_cycle(&locals_a).unwrap_err() else {
panic!()
};
let EvalError::Cycle { participants: pb } = check_cycle(&locals_b).unwrap_err() else {
panic!()
};
assert_eq!(pa, pb);
}
}