beck_core/
graph.rs

1//! The dependency graph — what the program is made of and what depends on what.
2//!
3//! # Why the compiler owns this
4//!
5//! .NET Aspire's dashboard shows a resource list and a dependency graph, and it can, because you
6//! write an *AppHost*: a second program that declares `AddPostgres("db")`, `WithReference(db)`, and
7//! so on. The topology is described twice — once as the application, once as the AppHost — and the
8//! two drift.
9//!
10//! Beck has no AppHost, because the program *is* the AppHost. [`crate::place`] assigns every
11//! definition a tier, [`crate::split`] slices the signal graph, and `beck-infra` derives the
12//! resource set from the effect rows, each resource carrying the effect that implies it. Nothing
13//! about the topology is written down a second time, so nothing about it can disagree. The graph
14//! below is not *collected*; it is *read off* what the compiler already knows.
15//!
16//! # The structure, and why
17//!
18//! A compressed sparse row adjacency: `offsets[v]..offsets[v + 1]` indexes a contiguous run of
19//! `edges`. That is 4 bytes per edge and one cache line per neighbourhood, against the pointer per
20//! edge and one allocation per vertex of a `Vec<Vec<_>>`. Both directions are stored, because the
21//! two questions a dashboard asks are opposite: *what does this need* (forward) and *what breaks if
22//! I change it* (reverse).
23//!
24//! | operation                                  | time                | space      |
25//! |--------------------------------------------|---------------------|------------|
26//! | build, including SCCs                       | `O(V + E)`          | `O(V + E)` |
27//! | `dependencies`, `dependents`                | `O(1)` to the slice | 0          |
28//! | `cycle_of`, `scc_index`, `id`               | `O(1)`, `O(log V)` for `id` | 0 |
29//! | `impacted_by` (transitive dependents)       | `O(V' + E')` reached | `O(V')`   |
30//! | `topological`                               | `O(1)`, precomputed | 0          |
31//!
32//! Building is linear and cannot be better: the program has to be read once. Everything the
33//! dashboard asks afterwards is a slice index or a bounded traversal, so "almost instant" is not a
34//! performance target to chase — it is what the representation makes unavoidable.
35//!
36//! # Cycles are not errors here
37//!
38//! `docs/19-phase-1-report.md` §19.4 item 4: the signal graph is *legitimately* cyclic —
39//! `events` is decided from `todos`, `todos` is folded from `events` — and §3.7 makes the cycle
40//! sound. So this does not topologically sort the vertices, which would be impossible. It computes
41//! strongly connected components with Tarjan's algorithm and topologically sorts the *condensation*,
42//! which always exists. A cycle becomes one box in the dashboard rather than a failure to render.
43//!
44//! Tarjan rather than Kosaraju because it is one pass rather than two, and it emits components in
45//! reverse topological order for free — the layout order the dashboard wants. It is written
46//! iteratively: recursion depth would be the longest path in the program, and a compiler should not
47//! have a program size at which it overflows the stack.
48
49use std::collections::BTreeMap;
50use std::sync::Arc;
51
52use beck_diag::Span;
53
54use crate::check::Program;
55use crate::core::{Core, CoreKind};
56use crate::ty::{Effect, Tier, Ty, TyDecl};
57
58/// An index into [`DepGraph::nodes`].
59#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
60pub struct NodeId(pub u32);
61
62#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
63pub enum NodeKind {
64    /// A model, union, newtype or alias.
65    Type,
66    /// A top-level function.
67    Function,
68    /// A top-level signal — a node in the dataflow, not a subroutine.
69    Signal,
70    /// An infrastructure object the effects imply: a workload, a log store, a route, a policy.
71    Resource,
72}
73
74impl NodeKind {
75    pub fn as_str(self) -> &'static str {
76        match self {
77            NodeKind::Type => "type",
78            NodeKind::Function => "function",
79            NodeKind::Signal => "signal",
80            NodeKind::Resource => "resource",
81        }
82    }
83}
84
85#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
86pub enum EdgeKind {
87    /// A function body mentions another definition.
88    Calls,
89    /// A signal's expression reads another signal. The dataflow edges, and the ones that may form
90    /// a cycle.
91    Reads,
92    /// A definition constructs, matches on, or is typed by a declared type.
93    Uses,
94    /// A resource exists *because* of this definition's effects. The edge `beck-infra` already
95    /// records as prose in `because`, as structure.
96    Implies,
97    /// A resource cannot start without another: a route needs a service, a stateful set needs the
98    /// headless service its `serviceName` names.
99    Needs,
100}
101
102impl EdgeKind {
103    pub fn as_str(self) -> &'static str {
104        match self {
105            EdgeKind::Calls => "calls",
106            EdgeKind::Reads => "reads",
107            EdgeKind::Uses => "uses",
108            EdgeKind::Implies => "implies",
109            EdgeKind::Needs => "needs",
110        }
111    }
112}
113
114#[derive(Clone, Copy, Debug, PartialEq, Eq)]
115pub struct Edge {
116    pub to: NodeId,
117    pub kind: EdgeKind,
118}
119
120#[derive(Clone, Debug, PartialEq, Eq)]
121pub struct GraphNode {
122    pub name: Arc<str>,
123    pub kind: NodeKind,
124    /// Where each definition runs. `Tier::Any` for a resource, which is not placed but derived.
125    pub tier: Tier,
126    pub effects: Vec<Effect>,
127    /// For a resource, the sentence `beck-infra` wrote saying which effect implied it. Empty for
128    /// program definitions, whose reason for existing is that someone wrote them.
129    pub because: String,
130    /// The declaration site, so the dashboard can link a resource to the line that caused it.
131    pub span: Span,
132}
133
134/// A dependency graph over one program and the infrastructure its effects imply.
135#[derive(Clone, Debug)]
136pub struct DepGraph {
137    nodes: Vec<GraphNode>,
138    by_name: BTreeMap<Arc<str>, NodeId>,
139    out_offsets: Vec<u32>,
140    out_edges: Vec<Edge>,
141    in_offsets: Vec<u32>,
142    in_edges: Vec<Edge>,
143    /// Which strongly connected component each node belongs to. Components are numbered in
144    /// topological order of the condensation.
145    scc_of: Vec<u32>,
146    /// Members of each component, grouped: `scc_members[scc_offsets[c]..scc_offsets[c + 1]]`.
147    scc_members: Vec<NodeId>,
148    scc_offsets: Vec<u32>,
149    /// Every node, in an order where a node comes after everything it depends on — except within a
150    /// cycle, where the members are adjacent and in no meaningful order.
151    order: Vec<NodeId>,
152}
153
154impl DepGraph {
155    pub fn len(&self) -> usize {
156        self.nodes.len()
157    }
158
159    pub fn is_empty(&self) -> bool {
160        self.nodes.is_empty()
161    }
162
163    pub fn edge_count(&self) -> usize {
164        self.out_edges.len()
165    }
166
167    pub fn nodes(&self) -> impl Iterator<Item = (NodeId, &GraphNode)> {
168        self.nodes
169            .iter()
170            .enumerate()
171            .map(|(i, n)| (NodeId(i as u32), n))
172    }
173
174    pub fn node(&self, id: NodeId) -> &GraphNode {
175        &self.nodes[id.0 as usize]
176    }
177
178    pub fn id(&self, name: &str) -> Option<NodeId> {
179        self.by_name.get(name).copied()
180    }
181
182    /// What this node depends on.
183    pub fn dependencies(&self, id: NodeId) -> &[Edge] {
184        let i = id.0 as usize;
185        &self.out_edges[self.out_offsets[i] as usize..self.out_offsets[i + 1] as usize]
186    }
187
188    /// What depends on this node — the direction that answers "what breaks if I change this".
189    pub fn dependents(&self, id: NodeId) -> &[Edge] {
190        let i = id.0 as usize;
191        &self.in_edges[self.in_offsets[i] as usize..self.in_offsets[i + 1] as usize]
192    }
193
194    /// The strongly connected component this node is in. A single-element slice when the node is
195    /// not in a cycle, which is the common case.
196    pub fn cycle_of(&self, id: NodeId) -> &[NodeId] {
197        self.members_of(self.scc_of[id.0 as usize])
198    }
199
200    fn members_of(&self, scc: u32) -> &[NodeId] {
201        let c = scc as usize;
202        &self.scc_members[self.scc_offsets[c] as usize..self.scc_offsets[c + 1] as usize]
203    }
204
205    /// Component index, in topological order of the condensation.
206    pub fn scc_index(&self, id: NodeId) -> u32 {
207        self.scc_of[id.0 as usize]
208    }
209
210    /// The cycles: components with more than one member. Empty for an acyclic program; the todo
211    /// example has exactly one, and it is the point of the architecture rather than a mistake.
212    pub fn cycles(&self) -> impl Iterator<Item = &[NodeId]> {
213        (0..self.scc_offsets.len() as u32 - 1)
214            .map(|c| self.members_of(c))
215            .filter(|m| m.len() > 1)
216    }
217
218    /// Dependencies before dependents, with cycle members adjacent.
219    pub fn topological(&self) -> &[NodeId] {
220        &self.order
221    }
222
223    /// A layer per node: 0 for something that depends on nothing, otherwise one more than the
224    /// deepest thing it depends on. Cycle members share a layer, because within a cycle there is no
225    /// "deeper".
226    ///
227    /// This is the x-coordinate of a layered drawing — the shape Aspire's graph view has, and the
228    /// reason it is computed here rather than in the browser: the condensation is already in
229    /// topological order, so one pass over it in that order gives every layer. `O(V + E)`, against
230    /// the iterative force-directed relaxation a client-side layout would need.
231    pub fn layers(&self) -> Vec<u32> {
232        let mut scc_layer = vec![0u32; self.scc_offsets.len() - 1];
233        // Components are numbered so that a dependency's component comes first; visiting them in
234        // order means every dependency's layer is final before it is read.
235        for c in 0..scc_layer.len() as u32 {
236            let mut deepest = 0;
237            for &m in self.members_of(c) {
238                for e in self.dependencies(m) {
239                    let d = self.scc_of[e.to.0 as usize];
240                    if d != c {
241                        deepest = deepest.max(scc_layer[d as usize] + 1);
242                    }
243                }
244            }
245            scc_layer[c as usize] = deepest;
246        }
247        self.scc_of.iter().map(|c| scc_layer[*c as usize]).collect()
248    }
249
250    /// Everything that transitively depends on `id`, including `id`. Breadth-first over the reverse
251    /// edges, so it costs the size of the affected region rather than the size of the program.
252    pub fn impacted_by(&self, id: NodeId) -> Vec<NodeId> {
253        self.impact(id).into_iter().map(|(n, _)| n).collect()
254    }
255
256    /// The same, with each node's distance in hops from `id`, and nearest first.
257    ///
258    /// The distance is what makes the answer usable rather than merely correct: "37 things depend
259    /// on this" is a number, and "4 things depend on it directly, and the rest through them" is an
260    /// answer. Breadth-first, so the first time a node is reached is by a shortest path.
261    pub fn impact(&self, id: NodeId) -> Vec<(NodeId, u32)> {
262        let mut seen = vec![false; self.nodes.len()];
263        let mut queue = std::collections::VecDeque::from([(id, 0)]);
264        let mut out = Vec::new();
265        seen[id.0 as usize] = true;
266        while let Some((v, d)) = queue.pop_front() {
267            out.push((v, d));
268            for e in self.dependents(v) {
269                if !seen[e.to.0 as usize] {
270                    seen[e.to.0 as usize] = true;
271                    queue.push_back((e.to, d + 1));
272                }
273            }
274        }
275        out
276    }
277}
278
279// -------------------------------------------------------------------------------------------
280// Building
281// -------------------------------------------------------------------------------------------
282
283/// Accumulates vertices and edges before they are frozen into CSR form.
284///
285/// Split from [`DepGraph`] so `beck-infra` can add the resource vertices — it depends on
286/// `beck-core`, not the other way round — without either crate knowing the other's node kinds.
287#[derive(Default)]
288pub struct GraphBuilder {
289    nodes: Vec<GraphNode>,
290    by_name: BTreeMap<Arc<str>, NodeId>,
291    edges: Vec<(NodeId, Edge)>,
292}
293
294impl GraphBuilder {
295    pub fn new() -> GraphBuilder {
296        GraphBuilder::default()
297    }
298
299    /// Add a vertex, or return the existing one if the name is already known.
300    pub fn node(&mut self, node: GraphNode) -> NodeId {
301        if let Some(id) = self.by_name.get(&node.name) {
302            return *id;
303        }
304        let id = NodeId(self.nodes.len() as u32);
305        self.by_name.insert(node.name.clone(), id);
306        self.nodes.push(node);
307        id
308    }
309
310    pub fn id(&self, name: &str) -> Option<NodeId> {
311        self.by_name.get(name).copied()
312    }
313
314    pub fn edge(&mut self, from: NodeId, to: NodeId, kind: EdgeKind) {
315        self.edges.push((from, Edge { to, kind }));
316    }
317
318    /// Add an edge to a name, if that name is a vertex. Silently ignores unknown names: a body
319    /// mentions prims and locals as well as globals, and those are not vertices.
320    pub fn edge_to_name(&mut self, from: NodeId, to: &str, kind: EdgeKind) {
321        if let Some(to) = self.id(to) {
322            if to != from {
323                self.edge(from, to, kind);
324            }
325        }
326    }
327
328    /// Freeze into CSR form and compute the components. `O(V + E)`.
329    pub fn finish(mut self) -> DepGraph {
330        let v = self.nodes.len();
331
332        // Deduplicate: a body that calls `mine` three times is one edge, not three. Sorting by
333        // (from, to, kind) also groups the edges by source, which is what CSR construction wants.
334        self.edges
335            .sort_unstable_by_key(|(from, e)| (from.0, e.to.0, e.kind));
336        self.edges
337            .dedup_by_key(|(from, e)| (from.0, e.to.0, e.kind));
338
339        let (out_offsets, out_edges) = csr(v, self.edges.iter().map(|(f, e)| (*f, *e)));
340        let (in_offsets, in_edges) = csr(
341            v,
342            self.edges.iter().map(|(f, e)| {
343                (
344                    e.to,
345                    Edge {
346                        to: *f,
347                        kind: e.kind,
348                    },
349                )
350            }),
351        );
352
353        let (scc_of, scc_members, scc_offsets) = tarjan(v, &out_offsets, &out_edges);
354        let order = scc_members.clone();
355
356        DepGraph {
357            nodes: self.nodes,
358            by_name: self.by_name,
359            out_offsets,
360            out_edges,
361            in_offsets,
362            in_edges,
363            scc_of,
364            scc_members,
365            scc_offsets,
366            order,
367        }
368    }
369}
370
371/// Counting sort into compressed sparse rows: one pass to count degrees, a prefix sum, one pass to
372/// place. `O(V + E)`, no per-vertex allocation.
373fn csr(v: usize, edges: impl Iterator<Item = (NodeId, Edge)> + Clone) -> (Vec<u32>, Vec<Edge>) {
374    let mut offsets = vec![0u32; v + 1];
375    for (from, _) in edges.clone() {
376        offsets[from.0 as usize + 1] += 1;
377    }
378    for i in 0..v {
379        offsets[i + 1] += offsets[i];
380    }
381    let mut out = vec![
382        Edge {
383            to: NodeId(0),
384            kind: EdgeKind::Calls
385        };
386        offsets[v] as usize
387    ];
388    let mut cursor = offsets.clone();
389    for (from, e) in edges {
390        let slot = &mut cursor[from.0 as usize];
391        out[*slot as usize] = e;
392        *slot += 1;
393    }
394    (offsets, out)
395}
396
397/// Tarjan's strongly-connected-components, iteratively.
398///
399/// Returns each node's component, the members grouped by component, and the group offsets.
400///
401/// A component is closed only once every component reachable from it has been closed. An edge here
402/// means "depends on", so a dependency's component is always closed first, and Tarjan's natural
403/// output order *is* the order the dashboard wants: everything a node depends on comes before it,
404/// except inside a cycle, where there is no such order and the members are adjacent instead.
405fn tarjan(v: usize, offsets: &[u32], edges: &[Edge]) -> (Vec<u32>, Vec<NodeId>, Vec<u32>) {
406    const UNVISITED: u32 = u32::MAX;
407
408    let mut index = vec![UNVISITED; v]; // discovery time
409    let mut low = vec![0u32; v];
410    let mut on_stack = vec![false; v];
411    let mut stack: Vec<NodeId> = Vec::new();
412    let mut next_index = 0u32;
413    // Components in discovery order, which is reverse topological order.
414    let mut comps: Vec<Vec<NodeId>> = Vec::new();
415
416    // The explicit call stack: (vertex, how far through its edges we are).
417    let mut work: Vec<(u32, u32)> = Vec::new();
418
419    for root in 0..v as u32 {
420        if index[root as usize] != UNVISITED {
421            continue;
422        }
423        work.push((root, offsets[root as usize]));
424        index[root as usize] = next_index;
425        low[root as usize] = next_index;
426        next_index += 1;
427        stack.push(NodeId(root));
428        on_stack[root as usize] = true;
429
430        while let Some((node, edge_cursor)) = work.last_mut() {
431            let n = *node as usize;
432            if *edge_cursor < offsets[n + 1] {
433                let e = edges[*edge_cursor as usize];
434                *edge_cursor += 1;
435                let w = e.to.0 as usize;
436                if index[w] == UNVISITED {
437                    index[w] = next_index;
438                    low[w] = next_index;
439                    next_index += 1;
440                    stack.push(e.to);
441                    on_stack[w] = true;
442                    work.push((e.to.0, offsets[w]));
443                } else if on_stack[w] {
444                    low[n] = low[n].min(index[w]);
445                }
446            } else {
447                // Done with this vertex: close a component if it is a root, then fold into parent.
448                if low[n] == index[n] {
449                    let mut comp = Vec::new();
450                    while let Some(w) = stack.pop() {
451                        on_stack[w.0 as usize] = false;
452                        comp.push(w);
453                        if w.0 as usize == n {
454                            break;
455                        }
456                    }
457                    comps.push(comp);
458                }
459                work.pop();
460                if let Some((parent, _)) = work.last() {
461                    let p = *parent as usize;
462                    low[p] = low[p].min(low[n]);
463                }
464            }
465        }
466    }
467
468    let mut scc_of = vec![0u32; v];
469    let mut members = Vec::with_capacity(v);
470    let mut scc_offsets = Vec::with_capacity(comps.len() + 1);
471    scc_offsets.push(0);
472    for (c, comp) in comps.iter().enumerate() {
473        for &m in comp {
474            scc_of[m.0 as usize] = c as u32;
475            members.push(m);
476        }
477        scc_offsets.push(members.len() as u32);
478    }
479    (scc_of, members, scc_offsets)
480}
481
482/// Read the vertices and edges of a checked program.
483///
484/// One walk of every definition body, so `O(program size)`.
485pub fn from_program(program: &Program) -> GraphBuilder {
486    let mut b = GraphBuilder::new();
487
488    // Vertices first, all of them, so a body walked in any order can find what it references. The
489    // signal graph is cyclic, so there is no order in which this would not be needed.
490    for name in program.types.keys() {
491        b.node(GraphNode {
492            name: name.clone(),
493            kind: NodeKind::Type,
494            tier: Tier::Any,
495            effects: Vec::new(),
496            because: String::new(),
497            // `TyDecl` carries no span — nothing has needed one yet. A type is the one vertex the
498            // dashboard cannot link back to its line.
499            span: Span::NONE,
500        });
501    }
502    for name in &program.def_order {
503        let Some(def) = program.defs.get(name) else {
504            continue;
505        };
506        b.node(GraphNode {
507            name: name.clone(),
508            kind: NodeKind::Function,
509            tier: def.tier,
510            effects: def.effects.clone(),
511            because: String::new(),
512            span: def.span,
513        });
514    }
515    for sig in &program.signals {
516        b.node(GraphNode {
517            name: sig.name.clone(),
518            kind: NodeKind::Signal,
519            tier: sig.tier,
520            effects: sig.effects.clone(),
521            because: String::new(),
522            span: sig.span,
523        });
524    }
525
526    // A model's field types and a union's variant payloads are dependencies too: changing `Id`
527    // changes `Todo`, and the dashboard should say so.
528    for (name, decl) in &program.types {
529        let from = b.id(name).expect("just added");
530        match decl {
531            TyDecl::Model { fields, .. } => {
532                for (_, ty) in fields {
533                    add_type_edges(&mut b, from, ty);
534                }
535            }
536            TyDecl::Union { variants, .. } => {
537                for v in variants {
538                    for (_, ty) in &v.fields {
539                        add_type_edges(&mut b, from, ty);
540                    }
541                }
542            }
543            TyDecl::Newtype { inner: ty, .. } | TyDecl::Alias { ty, .. } => {
544                add_type_edges(&mut b, from, ty)
545            }
546        }
547    }
548
549    for name in &program.def_order {
550        let Some(def) = program.defs.get(name) else {
551            continue;
552        };
553        let from = b.id(name).expect("just added");
554        for (_, _, ty) in &def.params {
555            add_type_edges(&mut b, from, ty);
556        }
557        add_type_edges(&mut b, from, &def.ret);
558        add_body_edges(&mut b, from, &def.body, EdgeKind::Calls);
559    }
560    for sig in &program.signals {
561        let from = b.id(&sig.name).expect("just added");
562        add_type_edges(&mut b, from, &sig.ty);
563        add_body_edges(&mut b, from, &sig.expr, EdgeKind::Reads);
564    }
565    b
566}
567
568/// Edges from a definition to every declared type its signature or body mentions.
569fn add_type_edges(b: &mut GraphBuilder, from: NodeId, ty: &Ty) {
570    match ty {
571        Ty::Con(name, args) => {
572            if b.id(name)
573                .is_some_and(|t| b.nodes[t.0 as usize].kind == NodeKind::Type)
574            {
575                b.edge_to_name(from, name, EdgeKind::Uses);
576            }
577            for a in args {
578                add_type_edges(b, from, a);
579            }
580        }
581        Ty::Fun(args, ret, _) => {
582            for a in args {
583                add_type_edges(b, from, a);
584            }
585            add_type_edges(b, from, ret);
586        }
587        Ty::Var(_) => {}
588    }
589}
590
591/// Edges from a definition to everything its body names.
592///
593/// `default_kind` distinguishes a function calling a function from a signal reading a signal; a
594/// reference to a *signal* is always `Reads`, whoever makes it, because that is a dataflow edge.
595fn add_body_edges(b: &mut GraphBuilder, from: NodeId, core: &Core, default_kind: EdgeKind) {
596    walk(core, &mut |c| match &c.kind {
597        CoreKind::Global(name) => {
598            let kind = match b.id(name).map(|id| b.nodes[id.0 as usize].kind) {
599                Some(NodeKind::Signal) => EdgeKind::Reads,
600                Some(NodeKind::Type) => EdgeKind::Uses,
601                _ => default_kind,
602            };
603            b.edge_to_name(from, name, kind);
604        }
605        CoreKind::Make { ty, .. } => b.edge_to_name(from, ty, EdgeKind::Uses),
606        _ => {}
607    });
608}
609
610/// Pre-order walk of a `Core` tree.
611fn walk(core: &Core, f: &mut impl FnMut(&Core)) {
612    f(core);
613    match &core.kind {
614        CoreKind::Const(_) | CoreKind::Var(_) | CoreKind::Global(_) => {}
615        CoreKind::Lam { body, .. } => walk(body, f),
616        CoreKind::App { func, args } => {
617            walk(func, f);
618            args.iter().for_each(|a| walk(a, f));
619        }
620        CoreKind::Prim { args, .. } => args.iter().for_each(|a| walk(a, f)),
621        CoreKind::Let { value, body, .. } => {
622            walk(value, f);
623            walk(body, f);
624        }
625        CoreKind::If { cond, then, alt } => {
626            walk(cond, f);
627            walk(then, f);
628            walk(alt, f);
629        }
630        CoreKind::Match { scrutinee, arms } => {
631            walk(scrutinee, f);
632            arms.iter().flat_map(|a| a.exprs()).for_each(|e| walk(e, f));
633        }
634        CoreKind::Make { fields, .. } => {
635            fields.iter().for_each(|(_, v)| walk(v, f));
636        }
637        // Not merged with `Make`: a `Make | With` or-pattern binding `fields` would drop `base`.
638        CoreKind::With { base, fields } => {
639            walk(base, f);
640            fields.iter().for_each(|(_, v)| walk(v, f));
641        }
642        CoreKind::Field { base, .. } => walk(base, f),
643        CoreKind::ListLit(xs) => xs.iter().for_each(|x| walk(x, f)),
644        CoreKind::MapLit(kvs) => kvs.iter().for_each(|(k, v)| {
645            walk(k, f);
646            walk(v, f);
647        }),
648    }
649}
650
651#[cfg(test)]
652mod tests {
653    use super::*;
654
655    /// Build a graph from adjacency written as `(from, to)` name pairs, so the algorithms can be
656    /// tested without a program in front of them.
657    fn graph_of(names: &[&str], edges: &[(&str, &str)]) -> DepGraph {
658        let mut b = GraphBuilder::new();
659        for n in names {
660            b.node(GraphNode {
661                name: Arc::from(*n),
662                kind: NodeKind::Function,
663                tier: Tier::Any,
664                effects: Vec::new(),
665                because: String::new(),
666                span: Span::NONE,
667            });
668        }
669        for (f, t) in edges {
670            let (f, t) = (b.id(f).unwrap(), b.id(t).unwrap());
671            b.edge(f, t, EdgeKind::Calls);
672        }
673        b.finish()
674    }
675
676    #[test]
677    fn edges_go_both_ways_and_duplicates_collapse() {
678        let g = graph_of(&["a", "b", "c"], &[("a", "b"), ("a", "b"), ("c", "b")]);
679        let b = g.id("b").unwrap();
680        assert_eq!(
681            g.dependencies(g.id("a").unwrap()).len(),
682            1,
683            "duplicate not collapsed"
684        );
685        assert_eq!(g.dependencies(b).len(), 0);
686        assert_eq!(g.dependents(b).len(), 2);
687        assert_eq!(g.edge_count(), 2);
688    }
689
690    #[test]
691    fn a_cycle_becomes_one_component_rather_than_a_failure() {
692        // The shape of the todo program: proposals → events → todos → proposals is not a mistake.
693        let g = graph_of(
694            &["merge", "events", "todos", "page", "unrelated"],
695            &[
696                ("events", "merge"),
697                ("events", "todos"),
698                ("todos", "events"),
699                ("page", "todos"),
700            ],
701        );
702        let cycles: Vec<Vec<&str>> = g
703            .cycles()
704            .map(|c| {
705                let mut names: Vec<&str> = c.iter().map(|n| &*g.node(*n).name).collect();
706                names.sort_unstable();
707                names
708            })
709            .collect();
710        assert_eq!(cycles, vec![vec!["events", "todos"]]);
711        assert_eq!(g.cycle_of(g.id("page").unwrap()).len(), 1, "not in a cycle");
712        assert_eq!(g.cycle_of(g.id("unrelated").unwrap()).len(), 1);
713    }
714
715    #[test]
716    fn the_condensation_is_topologically_ordered() {
717        let g = graph_of(
718            &["merge", "events", "todos", "page"],
719            &[
720                ("events", "merge"),
721                ("events", "todos"),
722                ("todos", "events"),
723                ("page", "todos"),
724            ],
725        );
726        let scc = |n: &str| g.scc_index(g.id(n).unwrap());
727        // A dependency's component comes first; the cycle members share one.
728        assert!(
729            scc("merge") < scc("events"),
730            "dependency must precede dependent"
731        );
732        assert_eq!(
733            scc("events"),
734            scc("todos"),
735            "cycle members share a component"
736        );
737        assert!(scc("todos") < scc("page"));
738
739        // …and `topological` lists nodes in that order, with cycle members adjacent.
740        let order: Vec<&str> = g.topological().iter().map(|n| &*g.node(*n).name).collect();
741        let at = |n: &str| order.iter().position(|x| *x == n).unwrap();
742        assert!(at("merge") < at("events"));
743        assert!(at("todos") < at("page"));
744        assert_eq!(order.len(), 4);
745    }
746
747    #[test]
748    fn impact_is_the_transitive_dependents_and_stops_there() {
749        let g = graph_of(
750            &["util", "a", "b", "unrelated", "other"],
751            &[("a", "util"), ("b", "a"), ("other", "unrelated")],
752        );
753        let mut impacted: Vec<&str> = g
754            .impacted_by(g.id("util").unwrap())
755            .iter()
756            .map(|n| &*g.node(*n).name)
757            .collect();
758        impacted.sort_unstable();
759        assert_eq!(impacted, vec!["a", "b", "util"]);
760
761        // The other half of the claim: it costs the affected region, not the program.
762        assert_eq!(g.impacted_by(g.id("other").unwrap()).len(), 1);
763
764        // Distances separate "depends on this directly" from "depends on it through something".
765        let hops: Vec<(&str, u32)> = g
766            .impact(g.id("util").unwrap())
767            .iter()
768            .map(|(n, d)| (&*g.node(*n).name, *d))
769            .collect();
770        assert_eq!(hops, vec![("util", 0), ("a", 1), ("b", 2)]);
771    }
772
773    #[test]
774    fn a_cycle_reached_from_outside_pulls_in_the_whole_component() {
775        let g = graph_of(&["x", "y", "z"], &[("x", "y"), ("y", "x"), ("z", "x")]);
776        assert_eq!(g.impacted_by(g.id("y").unwrap()).len(), 3);
777    }
778
779    #[test]
780    fn layers_put_dependencies_to_the_left_and_cycles_in_one_column() {
781        let g = graph_of(
782            &["merge", "events", "todos", "page", "loner"],
783            &[
784                ("events", "merge"),
785                ("events", "todos"),
786                ("todos", "events"),
787                ("page", "todos"),
788            ],
789        );
790        let layers = g.layers();
791        let l = |n: &str| layers[g.id(n).unwrap().0 as usize];
792        assert_eq!(l("merge"), 0, "depends on nothing");
793        assert_eq!(l("loner"), 0);
794        assert_eq!(l("events"), 1);
795        assert_eq!(l("todos"), 1, "a cycle is one column, not two");
796        assert_eq!(l("page"), 2);
797    }
798
799    #[test]
800    fn deep_chains_do_not_overflow_the_stack() {
801        // Tarjan is iterative precisely so this holds. Recursive Tarjan overflows here in debug.
802        let names: Vec<String> = (0..100_000).map(|i| format!("n{i}")).collect();
803        let mut b = GraphBuilder::new();
804        for n in &names {
805            b.node(GraphNode {
806                name: Arc::from(n.as_str()),
807                kind: NodeKind::Function,
808                tier: Tier::Any,
809                effects: Vec::new(),
810                because: String::new(),
811                span: Span::NONE,
812            });
813        }
814        for i in 0..names.len() - 1 {
815            b.edge(NodeId(i as u32), NodeId(i as u32 + 1), EdgeKind::Calls);
816        }
817        let g = b.finish();
818        assert_eq!(g.len(), 100_000);
819        assert_eq!(g.cycles().count(), 0);
820        // A 100,000-long chain is one long topological order, and every node is impacted by the last.
821        assert_eq!(g.impacted_by(NodeId(99_999)).len(), 100_000);
822    }
823
824    #[test]
825    fn a_walk_reaches_the_base_of_a_with() {
826        // A global referenced only through a `with`'s base must still be reached.
827        let global =
828            |name: &str| Core::new(CoreKind::Global(Arc::from(name)), Ty::unit(), Span::NONE);
829        let with = Core::new(
830            CoreKind::With {
831                base: Box::new(global("through_the_base")),
832                fields: vec![(Arc::from("f"), global("through_a_field"))],
833            },
834            Ty::unit(),
835            Span::NONE,
836        );
837        let mut seen = Vec::new();
838        walk(&with, &mut |c| {
839            if let CoreKind::Global(name) = &c.kind {
840                seen.push(name.to_string());
841            }
842        });
843        assert_eq!(seen, ["through_the_base", "through_a_field"]);
844    }
845}