1use std::{
18 collections::{BTreeMap, HashMap, HashSet},
19 sync::Arc,
20};
21
22use crate::{
23 eval::{
24 error::EvalError,
25 reduce::{Scope, reduce_expression},
26 },
27 ir::{Address, Expression, Local, SymbolKind},
28};
29
30#[derive(Clone, Debug, PartialEq, Eq, Hash)]
36#[non_exhaustive]
37pub struct CycleParticipant {
38 pub address: Address,
40}
41
42impl CycleParticipant {
43 #[must_use]
45 pub const fn new(address: Address) -> Self {
46 Self { address }
47 }
48}
49
50pub fn solve_locals(locals: &[Local], scope: &mut Scope) -> Result<Vec<Local>, EvalError> {
68 check_cycle(locals)?;
69
70 let mut out: Vec<Local> = locals.to_vec();
71 let mut converged: HashSet<Arc<str>> = HashSet::new();
72 let max_iters = locals.len().saturating_add(1);
76 for _ in 0..max_iters {
77 let mut made_progress = false;
78 for local in &mut out {
79 if converged.contains(&local.name) {
80 continue;
81 }
82 let reduced = reduce_expression(&local.value, scope);
83 let progressed = reduced != local.value;
84 local.value = reduced;
85 if let Expression::Literal(v) = &local.value {
86 scope.locals.push((Arc::clone(&local.name), v.clone()));
87 converged.insert(Arc::clone(&local.name));
88 made_progress = true;
89 } else if progressed {
90 made_progress = true;
91 }
92 }
93 if !made_progress {
94 break;
95 }
96 }
97 Ok(out)
98}
99
100pub(super) fn check_cycle(locals: &[Local]) -> Result<(), EvalError> {
108 let name_to_idx: HashMap<Arc<str>, usize> = locals
109 .iter()
110 .enumerate()
111 .map(|(i, l)| (Arc::clone(&l.name), i))
112 .collect();
113
114 let mut edges: Vec<Vec<usize>> = (0..locals.len()).map(|_| Vec::new()).collect();
115 for (i, local) in locals.iter().enumerate() {
116 let Some(edge_list) = edges.get_mut(i) else {
117 continue;
118 };
119 let mut deps: HashSet<Arc<str>> = HashSet::new();
120 collect_local_refs(&local.value, &mut deps);
121 for dep in deps {
122 if let Some(j) = name_to_idx.get(&dep) {
123 edge_list.push(*j);
124 }
125 }
126 }
127
128 if let Some(scc) = tarjan_first_cycle(&edges) {
129 let mut addrs: BTreeMap<String, Address> = BTreeMap::new();
130 for idx in scc {
131 if let Some(local) = locals.get(idx) {
132 let addr_str = format!("local.{}", local.name);
133 if let Ok(addr) = Address::new(&addr_str) {
134 addrs.insert(addr_str, addr);
135 }
136 }
137 }
138 return Err(EvalError::Cycle {
139 participants: addrs.into_values().collect(),
140 });
141 }
142 Ok(())
143}
144
145fn tarjan_first_cycle(edges: &[Vec<usize>]) -> Option<Vec<usize>> {
151 struct State {
152 index: u32,
153 stack: Vec<usize>,
154 on_stack: Vec<bool>,
155 indices: Vec<Option<u32>>,
156 lowlinks: Vec<u32>,
157 found: Option<Vec<usize>>,
158 }
159
160 fn strongconnect(v: usize, edges: &[Vec<usize>], st: &mut State) {
161 if st.found.is_some() {
162 return;
163 }
164 if let (Some(slot), Some(low)) = (st.indices.get_mut(v), st.lowlinks.get_mut(v)) {
165 *slot = Some(st.index);
166 *low = st.index;
167 }
168 st.index += 1;
169 st.stack.push(v);
170 if let Some(flag) = st.on_stack.get_mut(v) {
171 *flag = true;
172 }
173
174 let outgoing = edges.get(v).map_or(&[][..], Vec::as_slice);
175 for &w in outgoing {
176 let visited = st.indices.get(w).copied().flatten();
177 if visited.is_none() {
178 strongconnect(w, edges, st);
179 let merged = st.lowlinks.get(v).copied().unwrap_or(u32::MAX);
180 let from_w = st.lowlinks.get(w).copied().unwrap_or(u32::MAX);
181 if let Some(slot) = st.lowlinks.get_mut(v) {
182 *slot = merged.min(from_w);
183 }
184 } else if st.on_stack.get(w).copied().unwrap_or(false) {
185 let merged = st.lowlinks.get(v).copied().unwrap_or(u32::MAX);
186 let from_w = visited.unwrap_or(u32::MAX);
187 if let Some(slot) = st.lowlinks.get_mut(v) {
188 *slot = merged.min(from_w);
189 }
190 }
191 }
192
193 let low_v = st.lowlinks.get(v).copied();
194 let idx_v = st.indices.get(v).copied().flatten();
195 if low_v == idx_v {
196 let mut scc: Vec<usize> = Vec::new();
197 while let Some(w) = st.stack.pop() {
198 if let Some(flag) = st.on_stack.get_mut(w) {
199 *flag = false;
200 }
201 scc.push(w);
202 if w == v {
203 break;
204 }
205 }
206 let is_self_cycle = scc.len() == 1 && edges.get(v).is_some_and(|out| out.contains(&v));
207 if scc.len() > 1 || is_self_cycle {
208 st.found = Some(scc);
209 }
210 }
211 }
212
213 let n = edges.len();
214 let mut st = State {
215 index: 0,
216 stack: Vec::new(),
217 on_stack: vec![false; n],
218 indices: vec![None; n],
219 lowlinks: vec![0; n],
220 found: None,
221 };
222
223 for v in 0..n {
224 let already = st.indices.get(v).copied().flatten();
225 if already.is_none() {
226 strongconnect(v, edges, &mut st);
227 if st.found.is_some() {
228 break;
229 }
230 }
231 }
232 st.found
233}
234
235fn collect_local_refs(expr: &Expression, out: &mut HashSet<Arc<str>>) {
236 match expr {
237 Expression::Literal(_) => {}
238 Expression::Unresolved(s) => {
239 if matches!(s.kind, SymbolKind::Local) {
240 let rest = s.source.strip_prefix("local.").unwrap_or(&s.source);
243 let name = rest.split('.').next().unwrap_or(rest);
244 if !name.is_empty() {
245 out.insert(Arc::from(name));
246 }
247 }
248 }
249 Expression::BinaryOp { lhs, rhs, .. } => {
250 collect_local_refs(lhs, out);
251 collect_local_refs(rhs, out);
252 }
253 Expression::UnaryOp { operand, .. } => collect_local_refs(operand, out),
254 Expression::TemplateConcat(parts) | Expression::Array(parts) => {
255 for p in parts {
256 collect_local_refs(p, out);
257 }
258 }
259 Expression::Object(entries) => {
260 for (k, v) in entries {
261 collect_local_refs(k, out);
262 collect_local_refs(v, out);
263 }
264 }
265 Expression::FuncCall(call) => {
266 for a in &call.args {
267 collect_local_refs(a, out);
268 }
269 }
270 Expression::Conditional(c) => {
271 collect_local_refs(&c.cond, out);
272 collect_local_refs(&c.then_branch, out);
273 collect_local_refs(&c.else_branch, out);
274 }
275 Expression::For(f) => {
276 collect_local_refs(&f.collection, out);
277 collect_local_refs(&f.value, out);
278 if let Some(k) = &f.key {
279 collect_local_refs(k, out);
280 }
281 if let Some(c) = &f.cond {
282 collect_local_refs(c, out);
283 }
284 }
285 }
286}
287
288#[cfg(test)]
289#[allow(
290 clippy::unwrap_used,
291 clippy::expect_used,
292 clippy::panic,
293 clippy::indexing_slicing
294)]
295mod tests {
296 use std::sync::Arc;
297
298 use super::*;
299 use crate::ir::{Expression, Local, Span, SymbolKind, Symbolic, Value};
300
301 fn local(name: &str, expr: Expression) -> Local {
302 Local::builder()
303 .name(Arc::<str>::from(name))
304 .value(expr)
305 .span(Span::synthetic())
306 .build()
307 }
308
309 fn local_ref(name: &str) -> Expression {
310 Expression::Unresolved(
311 Symbolic::builder()
312 .kind(SymbolKind::Local)
313 .source(Arc::<str>::from(format!("local.{name}")))
314 .span(Span::synthetic())
315 .build(),
316 )
317 }
318
319 #[test]
320 fn test_cycle_check_accepts_acyclic_locals() {
321 let locals = vec![
322 local("a", Expression::Literal(Value::Int(1))),
323 local("b", local_ref("a")),
324 ];
325 check_cycle(&locals).expect("acyclic");
326 }
327
328 #[test]
329 fn test_cycle_check_rejects_self_cycle() {
330 let locals = vec![local("a", local_ref("a"))];
331 let err = check_cycle(&locals).unwrap_err();
332 let EvalError::Cycle { participants } = err else {
333 panic!("expected cycle");
334 };
335 assert_eq!(participants.len(), 1);
336 assert_eq!(participants[0].as_str(), "local.a");
337 }
338
339 #[test]
340 fn test_cycle_check_rejects_two_node_cycle() {
341 let locals = vec![local("a", local_ref("b")), local("b", local_ref("a"))];
342 let err = check_cycle(&locals).unwrap_err();
343 let EvalError::Cycle { participants } = err else {
344 panic!("expected cycle");
345 };
346 assert_eq!(participants.len(), 2);
348 assert_eq!(participants[0].as_str(), "local.a");
349 assert_eq!(participants[1].as_str(), "local.b");
350 }
351
352 #[test]
353 fn test_cycle_check_rejects_three_node_cycle() {
354 let locals = vec![
355 local("a", local_ref("b")),
356 local("b", local_ref("c")),
357 local("c", local_ref("a")),
358 ];
359 let err = check_cycle(&locals).unwrap_err();
360 let EvalError::Cycle { participants } = err else {
361 panic!("expected cycle");
362 };
363 let names: Vec<&str> = participants.iter().map(Address::as_str).collect();
364 assert_eq!(names, vec!["local.a", "local.b", "local.c"]);
365 }
366
367 #[test]
368 fn test_cycle_check_finds_cycle_among_acyclic_neighbours() {
369 let locals = vec![
372 local("x", Expression::Literal(Value::Int(0))),
373 local("a", local_ref("b")),
374 local("b", local_ref("a")),
375 ];
376 let err = check_cycle(&locals).unwrap_err();
377 assert!(matches!(err, EvalError::Cycle { .. }));
378 }
379
380 #[test]
381 fn test_cycle_participants_are_deterministic_order() {
382 let locals_a = vec![local("a", local_ref("b")), local("b", local_ref("a"))];
385 let locals_b = vec![local("b", local_ref("a")), local("a", local_ref("b"))];
386 let EvalError::Cycle { participants: pa } = check_cycle(&locals_a).unwrap_err() else {
387 panic!()
388 };
389 let EvalError::Cycle { participants: pb } = check_cycle(&locals_b).unwrap_err() else {
390 panic!()
391 };
392 assert_eq!(pa, pb);
393 }
394}