Skip to main content

tfparser_core/eval/
locals.rs

1//! `locals` fixpoint solver and cycle detection.
2//!
3//! Per [13-evaluator.md § 3] step 2: topologically order the component's
4//! `locals` by their inter-references and evaluate them in dependency
5//! order. The worklist algorithm reduces one `Local` at a time, taking the
6//! reduced expression and binding it into the scope so later locals can
7//! reach it.
8//!
9//! Cycle detection runs as a Tarjan-style SCC pass over the **declared**
10//! dependency graph (every `Local` is a node; every `local.X` reference
11//! adds a `X → name` edge). Any SCC of size > 1, or a self-edge, surfaces
12//! as [`EvalError::Cycle`]. Participants are sorted by address so the
13//! diagnostic is deterministic across runs.
14//!
15//! [13-evaluator.md § 3]: ../../../specs/13-evaluator.md
16
17use 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/// A single participant in a cycle diagnostic.
31///
32/// Wrapped instead of `Address` directly to keep the public API additive
33/// — future variants can carry the file / span of the offending
34/// `local.X` reference. Phase 4 ships the address only.
35#[derive(Clone, Debug, PartialEq, Eq, Hash)]
36#[non_exhaustive]
37pub struct CycleParticipant {
38    /// `local.<name>` address.
39    pub address: Address,
40}
41
42impl CycleParticipant {
43    /// Construct a participant from an address.
44    #[must_use]
45    pub const fn new(address: Address) -> Self {
46        Self { address }
47    }
48}
49
50/// Resolve the component's locals against `scope`, returning the post-
51/// reduction `Local`s plus any local-shadowable bindings.
52///
53/// `scope.locals` is updated as each local resolves so the next iteration
54/// can reach it. Locals that cannot resolve in any iteration keep their
55/// expressions (possibly partially-reduced); the binding is left absent
56/// from `scope.locals` so downstream `local.X` references stay
57/// [`Expression::Unresolved`].
58///
59/// Cycle detection runs **before** any reduction so we never even start
60/// evaluating a tree we know will diverge.
61///
62/// # Errors
63///
64/// Returns [`EvalError::Cycle`] if the locals' declared reference graph
65/// contains a cycle. All other failures are recorded as diagnostics by the
66/// caller (the walker treats unresolved subtrees as data, not errors).
67pub 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    // Bounded by `locals.len()` because each fully-resolved local kicks
73    // off at most one new wave of resolutions. Cap at len()+1 anyway as a
74    // belt-and-braces defence against pathological inputs.
75    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
100/// Tarjan-style cycle check over the locals' declared `local.X` references.
101///
102/// We do not consult `address_hint` on the contained expressions because
103/// the walk wants direct child→parent edges; the lowering pass already
104/// classified every `local.X` traversal as
105/// [`SymbolKind::Local`](crate::ir::SymbolKind::Local), so a simple
106/// `collect_local_refs` pass is enough.
107pub(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
145/// Surface the first non-trivial SCC encountered (Tarjan's algorithm).
146///
147/// Returns `Some(scc)` for any SCC of size > 1 or a single node with a
148/// self-edge, else `None`. The visit order is deterministic across runs
149/// (node ids 0..n).
150fn 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                // Source is `local.<name>[.<rest>]`. We only care about
241                // the `<name>` part.
242                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        // Sorted alphabetically.
347        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        // Two disjoint groups: one acyclic, one cyclic. The cycle still
370        // surfaces.
371        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        // Same cycle declared in a different source order: participants
383        // sort lexicographically so the diagnostic is stable.
384        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}