Skip to main content

ploidy_core/ir/
graph.rs

1use std::{
2    any::{Any, TypeId},
3    collections::VecDeque,
4    fmt::Debug,
5};
6
7use atomic_refcell::AtomicRefCell;
8use fixedbitset::FixedBitSet;
9use itertools::Itertools;
10use petgraph::{
11    Direction,
12    adj::UnweightedList,
13    algo::{TarjanScc, tred},
14    data::Build,
15    graph::{DiGraph, NodeIndex},
16    stable_graph::StableDiGraph,
17    visit::{
18        DfsPostOrder, EdgeFiltered, EdgeRef, IntoNeighbors, IntoNeighborsDirected,
19        IntoNodeIdentifiers, NodeCount, NodeIndexable,
20    },
21};
22use rustc_hash::{FxBuildHasher, FxHashMap};
23
24use crate::{
25    arena::Arena,
26    ir::{SchemaTypeInfo, UntaggedVariantMeta},
27    parse::Info,
28};
29
30use super::{
31    spec::{ResolvedSpecType, Spec},
32    types::{
33        FieldMeta, GraphContainer, GraphInlineType, GraphOperation, GraphSchemaType, GraphStruct,
34        GraphTagged, GraphType, InlineTypePath, InlineTypePathRoot, InlineTypePathSegment,
35        PrimitiveType, SpecInlineType, SpecSchemaType, SpecType, SpecUntaggedVariant,
36        StructFieldName, TaggedVariantMeta, VariantMeta,
37        shape::{Operation, Parameter, ParameterInfo, Request, Response},
38    },
39    views::{operation::OperationView, primitive::PrimitiveView, schema::SchemaTypeView},
40};
41
42/// The mutable, sparse graph used for transformations.
43type RawDiGraph<'a> = StableDiGraph<GraphType<'a>, GraphEdge<'a>, usize>;
44
45/// The immutable, dense graph used for code generation.
46type CookedDiGraph<'a> = DiGraph<GraphType<'a>, GraphEdge<'a>, usize>;
47
48/// A mutable intermediate dependency graph of all the types in a [`Spec`],
49/// backed by a sparse [`StableDiGraph`].
50///
51/// This graph is constructed directly from a [`Spec`], and represents
52/// type relationships as they exist in the spec. Transformations like
53/// [`inline_tagged_variants`][Self::inline_tagged_variants] rewrite this graph
54/// in place.
55///
56/// After applying all transformations, call [`cook`][Self::cook] to
57/// turn this graph into a [`CookedGraph`] that's ready for code generation.
58#[derive(Debug)]
59pub struct RawGraph<'a> {
60    arena: &'a Arena,
61    spec: &'a Spec<'a>,
62    graph: RawDiGraph<'a>,
63    ops: &'a [&'a GraphOperation<'a>],
64}
65
66impl<'a> RawGraph<'a> {
67    /// Builds a raw type graph from the given spec.
68    pub fn new(arena: &'a Arena, spec: &'a Spec<'a>) -> Self {
69        // All roots (named schemas, parameters, request and response bodies),
70        // and all the types within them (inline schemas and primitives).
71        let tys = SpecTypeVisitor::new(
72            spec.schemas
73                .values()
74                .chain(spec.operations.iter().flat_map(|op| op.types().copied())),
75        );
76
77        // Inflate a graph from the traversal.
78        let mut indices = FxHashMap::default();
79        let mut schemas = FxHashMap::default();
80        let mut graph = RawDiGraph::default();
81        for (parent, child) in tys {
82            use std::collections::hash_map::Entry;
83
84            let source = spec.resolve(child);
85            let &mut to = match indices.entry(source) {
86                Entry::Occupied(entry) => entry.into_mut(),
87                Entry::Vacant(entry) => {
88                    // We might see the same schema multiple times if it's
89                    // referenced multiple times in the spec. Only add
90                    // a new node for the schema if we haven't seen it before.
91                    let index = graph.add_node(match *entry.key() {
92                        ResolvedSpecType::Schema(&ty) => GraphType::Schema(ty.into()),
93                        ResolvedSpecType::Inline(&ty) => GraphType::Inline(ty.into()),
94                    });
95                    if let ResolvedSpecType::Schema(ty) = source {
96                        schemas.entry(ty.name()).or_insert(index);
97                    }
98                    entry.insert(index)
99                }
100            };
101
102            if let Some((parent, edge)) = parent {
103                let destination = spec.resolve(parent);
104                let &mut from = match indices.entry(destination) {
105                    Entry::Occupied(entry) => entry.into_mut(),
106                    Entry::Vacant(entry) => {
107                        let index = graph.add_node(match *entry.key() {
108                            ResolvedSpecType::Schema(&ty) => GraphType::Schema(ty.into()),
109                            ResolvedSpecType::Inline(&ty) => GraphType::Inline(ty.into()),
110                        });
111                        if let ResolvedSpecType::Schema(ty) = destination {
112                            schemas.entry(ty.name()).or_insert(index);
113                        }
114                        entry.insert(index)
115                    }
116                };
117                graph.add_edge(from, to, edge);
118            }
119        }
120
121        // Map type references in operations to graph indices.
122        let ops = arena.alloc_slice_exact(spec.operations.iter().map(|op| {
123            let params = arena.alloc_slice_exact(op.params.iter().map(|param| match param {
124                Parameter::Path(info) => Parameter::Path(ParameterInfo {
125                    name: info.name,
126                    ty: match info.ty {
127                        SpecType::Schema(s) => indices[&ResolvedSpecType::Schema(s)],
128                        SpecType::Inline(i) => indices[&ResolvedSpecType::Inline(i)],
129                        SpecType::Ref(r) => schemas[&*r.name()],
130                    },
131                    required: info.required,
132                    description: info.description,
133                    style: info.style,
134                }),
135                Parameter::Query(info) => Parameter::Query(ParameterInfo {
136                    name: info.name,
137                    ty: match info.ty {
138                        SpecType::Schema(s) => indices[&ResolvedSpecType::Schema(s)],
139                        SpecType::Inline(i) => indices[&ResolvedSpecType::Inline(i)],
140                        SpecType::Ref(r) => schemas[&*r.name()],
141                    },
142                    required: info.required,
143                    description: info.description,
144                    style: info.style,
145                }),
146            }));
147
148            let request = op.request.as_ref().map(|r| match r {
149                Request::Json(ty) => Request::Json(match ty {
150                    SpecType::Schema(s) => indices[&ResolvedSpecType::Schema(s)],
151                    SpecType::Inline(i) => indices[&ResolvedSpecType::Inline(i)],
152                    SpecType::Ref(r) => schemas[&*r.name()],
153                }),
154                Request::Multipart => Request::Multipart,
155            });
156
157            let response = op.response.as_ref().map(|r| match r {
158                Response::Json(ty) => Response::Json(match ty {
159                    SpecType::Schema(s) => indices[&ResolvedSpecType::Schema(s)],
160                    SpecType::Inline(i) => indices[&ResolvedSpecType::Inline(i)],
161                    SpecType::Ref(r) => schemas[&*r.name()],
162                }),
163            });
164
165            &*arena.alloc(Operation {
166                id: op.id,
167                method: op.method,
168                path: op.path,
169                resource: op.resource,
170                description: op.description,
171                params,
172                request,
173                response,
174            })
175        }));
176
177        Self {
178            arena,
179            spec,
180            graph,
181            ops,
182        }
183    }
184
185    /// Inlines schema types used as variants of multiple tagged unions
186    /// with different tags.
187    ///
188    /// In OpenAPI's model of tagged unions, the tag always references a field
189    /// that's defined on each variant struct. This model works well for Python
190    /// and TypeScript, but not Rust; Serde doesn't allow variant structs to
191    /// declare fields with the same name as the tag. The Rust generator
192    /// excludes tag fields when generating structs, but this introduces a
193    /// new problem: a struct can't appear as a variant of multiple unions
194    /// with different tags [^1].
195    ///
196    /// This transformation finds and inlines these structs, so that
197    /// the Rust generator can safely omit their tag fields.
198    ///
199    /// [^1]: If struct A has fields `foo` and `bar`, A is a variant of
200    /// tagged unions C and D, C's tag is `foo`, and D's tag is `bar`...
201    /// only `foo` should be excluded when A is used in C, and only `bar`
202    /// should be excluded when A is used in D; but this can't be modeled
203    /// in Serde without splitting A into two distinct types.
204    pub fn inline_tagged_variants(&mut self) -> &mut Self {
205        // Collect all inlining decisions before mutating the graph,
206        // so that we can check inlinability per variant.
207        let inlinables = self.inlinable_tagged_variants().collect_vec();
208
209        let mut retargets = FxHashMap::default();
210        retargets.reserve(inlinables.len());
211
212        // Add nodes for the inlined variant structs,
213        // and their outgoing edges.
214        for InlinableVariant { tagged, variant } in inlinables {
215            // Duplicate the variant struct as an inline type,
216            // with its original metadata.
217            let index = self
218                .graph
219                .add_node(GraphType::Inline(GraphInlineType::Struct(
220                    InlineTypePath {
221                        root: InlineTypePathRoot::Type(tagged.info.name),
222                        segments: self.arena.alloc_slice_copy(&[
223                            InlineTypePathSegment::TaggedVariant(variant.info.name),
224                        ]),
225                    },
226                    variant.ty,
227                )));
228
229            // Create shadow edges to the original variant struct's fields.
230            // These serve two purposes:
231            //
232            // 1. If a field is recursive, the duplicate joins the field's SCC,
233            //    not the original's SCC, so field edges to the original type
234            //    won't be treated as cyclic.
235            // 2. Hiding the originals' inlines from the duplicate's inlines.
236            //
237            // `fields()` yields edges in reverse order of addition;
238            // we collect and reverse to add them in their original order.
239            let original_field_edges = self.fields(variant.index).collect_vec();
240            for edge in original_field_edges.into_iter().rev() {
241                self.graph.add_edge(
242                    index,
243                    edge.target,
244                    GraphEdge::Field {
245                        meta: edge.meta,
246                        shadow: true,
247                    },
248                );
249            }
250
251            // Inherit from the tagged union (to pick up its own fields)
252            // and the original variant struct (to pick up its ancestors).
253            // The union is added first so that its fields appear first _and_
254            // can be overridden by the variant's fields.
255            self.graph
256                .add_edge(index, tagged.index, GraphEdge::Inherits { shadow: true });
257            self.graph
258                .add_edge(index, variant.index, GraphEdge::Inherits { shadow: true });
259
260            retargets.insert((tagged.index, variant.index), index);
261        }
262
263        // Retarget every tagged union's variant edges to the new structs.
264        let taggeds: FixedBitSet = retargets
265            .keys()
266            .map(|&(tagged, _)| tagged.index())
267            .collect();
268        for index in taggeds.ones().map(NodeIndex::new) {
269            let old_edges = self
270                .graph
271                .edges_directed(index, Direction::Outgoing)
272                .filter(|e| matches!(e.weight(), GraphEdge::Variant(_)))
273                .map(|e| (e.id(), *e.weight(), e.target()))
274                .collect_vec();
275            for &(id, _, _) in &old_edges {
276                self.graph.remove_edge(id);
277            }
278            // Re-add edges. `edges_directed` yields edges in reverse order
279            // of addition; reversing them adds edges in their original order.
280            for (_, weight, target) in old_edges.into_iter().rev() {
281                let new_target = retargets.get(&(index, target)).copied().unwrap_or(target);
282                self.graph.add_edge(index, new_target, weight);
283            }
284        }
285
286        self
287    }
288
289    /// Builds an immutable [`CookedGraph`] from this mutable raw graph.
290    #[inline]
291    pub fn cook(&self) -> CookedGraph<'a> {
292        CookedGraph::new(self)
293    }
294
295    /// Returns an iterator over all the fields of a struct or union type,
296    /// in reverse insertion order.
297    fn fields(&self, node: NodeIndex<usize>) -> impl Iterator<Item = OutgoingEdge<FieldMeta<'a>>> {
298        self.graph
299            .edges_directed(node, Direction::Outgoing)
300            .filter_map(|e| match e.weight() {
301                &GraphEdge::Field { meta, .. } => {
302                    let target = e.target();
303                    Some(OutgoingEdge { meta, target })
304                }
305                _ => None,
306            })
307    }
308
309    /// Returns an iterator over all the tagged union variant structs
310    /// that should be inlined.
311    fn inlinable_tagged_variants(&self) -> impl Iterator<Item = InlinableVariant<'a>> {
312        // Compute the set of types used by all operations.
313        // Operations don't participate in the graph, but
314        // still need to be considered when deciding
315        // whether to inline a variant struct.
316        //
317        // Otherwise, a struct that's used by same-tag unions
318        // _and_ an operation wouldn't be inlined, incorrectly
319        // removing its tag field.
320        let used_by_ops: FixedBitSet = self
321            .ops
322            .iter()
323            .flat_map(|op| op.types())
324            .map(|index| index.index())
325            .collect();
326
327        self.graph
328            .node_indices()
329            .filter_map(|index| match self.graph[index] {
330                GraphType::Schema(GraphSchemaType::Tagged(info, ty)) => {
331                    Some(Node { index, info, ty })
332                }
333                _ => None,
334            })
335            .flat_map(move |tagged| {
336                self.graph
337                    .edges_directed(tagged.index, Direction::Outgoing)
338                    .filter(|e| matches!(e.weight(), GraphEdge::Variant(_)))
339                    .filter_map(move |e| match self.graph[e.target()] {
340                        GraphType::Schema(GraphSchemaType::Struct(info, ty)) => {
341                            let index = e.target();
342                            Some((tagged, Node { index, info, ty }))
343                        }
344                        _ => None,
345                    })
346            })
347            .filter_map(move |(tagged, variant)| {
348                // A variant struct only needs inlining if it has multiple
349                // distinct uses. Skip if (1) no operation uses the struct,
350                // _and_ (2) every incoming edge is from a tagged union with
351                // the same tag and fields. If both hold, all uses agree, so
352                // the struct can be used directly without inlining.
353                if used_by_ops[variant.index.index()] {
354                    return Some((tagged, variant));
355                }
356
357                // Check that all the variant's inbound edges are from
358                // tagged unions, and that all their tags and field
359                // edges match the first union we found.
360                let first_tagged = self
361                    .graph
362                    .neighbors_directed(variant.index, Direction::Incoming)
363                    .find_map(|index| match self.graph[index] {
364                        GraphType::Schema(GraphSchemaType::Tagged(info, ty)) => {
365                            Some(Node { index, info, ty })
366                        }
367                        _ => None,
368                    })?;
369                let all_agree = self
370                    .graph
371                    .neighbors_directed(variant.index, Direction::Incoming)
372                    .all(|index| match self.graph[index] {
373                        GraphType::Schema(GraphSchemaType::Tagged(_, ty)) => {
374                            ty.tag == first_tagged.ty.tag
375                                && self.fields(index).eq(self.fields(first_tagged.index))
376                        }
377                        _ => false,
378                    });
379                if all_agree {
380                    return None;
381                }
382                Some((tagged, variant))
383            })
384            .filter_map(|(tagged, variant)| {
385                // Skip inlining when the inline copy would be identical
386                // to the original. This happens when the variant
387                // doesn't declare the tag as a field _and_ either
388                // (a) the union has no own fields, or (b) the variant
389                // inherits from the union.
390                let ancestors = EdgeFiltered::from_fn(&self.graph, |e| {
391                    matches!(e.weight(), GraphEdge::Inherits { .. })
392                });
393                let mut dfs = DfsPostOrder::new(&ancestors, variant.index);
394                let has_tag_field = std::iter::from_fn(|| dfs.next(&ancestors))
395                    .filter(|&n| {
396                        matches!(
397                            self.graph[n],
398                            GraphType::Schema(GraphSchemaType::Struct(..))
399                                | GraphType::Inline(GraphInlineType::Struct(..))
400                        )
401                    })
402                    .any(|n| {
403                        self.fields(n).any(|f| {
404                            matches!(f.meta.name, StructFieldName::Name(name)
405                                if name == tagged.ty.tag)
406                        })
407                    });
408
409                // If the variant declares or inherits the tag field,
410                // we must inline, so that the inline copy can safely
411                // omit the tag.
412                if has_tag_field {
413                    return Some(InlinableVariant { tagged, variant });
414                }
415
416                // If the DFS visited the union, the variant already inherits
417                // its fields; the inline copy would be identical.
418                if dfs.discovered[tagged.index.index()] {
419                    return None;
420                }
421
422                // If the variant doesn't inherit from the union, but the union
423                // has no fields of its own, the inline copy would be identical.
424                self.fields(tagged.index).next()?;
425
426                Some(InlinableVariant { tagged, variant })
427            })
428    }
429}
430
431/// The final dependency graph of all the types in a [`Spec`],
432/// backed by a dense [`DiGraph`].
433///
434/// This graph has all transformations applied, and is ready for
435/// code generation.
436#[derive(Debug)]
437pub struct CookedGraph<'a> {
438    pub(super) graph: CookedDiGraph<'a>,
439    info: &'a Info,
440    ops: &'a [&'a GraphOperation<'a>],
441    /// Additional metadata for each node.
442    pub(super) metadata: CookedGraphMetadata<'a>,
443}
444
445impl<'a> CookedGraph<'a> {
446    fn new(raw: &RawGraph<'a>) -> Self {
447        // Build a dense graph, mapping sparse raw node indices to
448        // dense cooked node indices.
449        let mut graph =
450            CookedDiGraph::with_capacity(raw.graph.node_count(), raw.graph.edge_count());
451        let mut indices =
452            FxHashMap::with_capacity_and_hasher(raw.graph.node_count(), FxBuildHasher);
453        for raw_index in raw.graph.node_indices() {
454            let cooked_index = graph.add_node(raw.graph[raw_index]);
455            indices.insert(raw_index, cooked_index);
456        }
457
458        // Copy edges.
459        //
460        // `raw.graph.edges()` yields edges in reverse order of addition.
461        // The raw graph adds edges in declaration order, so `edges()`
462        // yields them reversed. Re-adding them to the cooked graph in that
463        // reversed order means they're now stored in reverse-declaration order,
464        // letting the cooked graph's accessors yield edges in declaration order
465        // without any extra work.
466        for index in raw.graph.node_indices() {
467            let from = indices[&index];
468            let edges = raw
469                .graph
470                .edges(index)
471                .map(|e| (indices[&e.target()], *e.weight()));
472            for (to, kind) in edges {
473                graph.add_edge(from, to, kind);
474            }
475        }
476
477        // Remap schema type references in operations.
478        let ops: &_ = raw.arena.alloc_slice_exact(raw.ops.iter().map(|&op| {
479            &*raw.arena.alloc(Operation {
480                id: op.id,
481                method: op.method,
482                path: op.path,
483                resource: op.resource,
484                description: op.description,
485                params: raw
486                    .arena
487                    .alloc_slice_exact(op.params.iter().map(|p| match p {
488                        Parameter::Path(info) => Parameter::Path(ParameterInfo {
489                            name: info.name,
490                            ty: indices[&info.ty],
491                            required: info.required,
492                            description: info.description,
493                            style: info.style,
494                        }),
495                        Parameter::Query(info) => Parameter::Query(ParameterInfo {
496                            name: info.name,
497                            ty: indices[&info.ty],
498                            required: info.required,
499                            description: info.description,
500                            style: info.style,
501                        }),
502                    })),
503                request: op.request.as_ref().map(|r| match r {
504                    Request::Json(ty) => Request::Json(indices[ty]),
505                    Request::Multipart => Request::Multipart,
506                }),
507                response: op.response.as_ref().map(|r| match r {
508                    Response::Json(ty) => Response::Json(indices[ty]),
509                }),
510            })
511        }));
512
513        let metadata = MetadataBuilder::new(&graph, ops).build();
514
515        Self {
516            graph,
517            info: raw.spec.info,
518            ops,
519            metadata,
520        }
521    }
522
523    /// Returns [`Info`] from the [`Document`][crate::parse::Document]
524    /// used to build this graph.
525    #[inline]
526    pub fn info(&self) -> &'a Info {
527        self.info
528    }
529
530    /// Returns an iterator over all the named schemas in this graph.
531    #[inline]
532    pub fn schemas(&self) -> impl Iterator<Item = SchemaTypeView<'_>> {
533        self.graph
534            .node_indices()
535            .filter_map(|index| match self.graph[index] {
536                GraphType::Schema(ty) => Some(SchemaTypeView::new(self, index, ty)),
537                _ => None,
538            })
539    }
540
541    /// Returns an iterator over all primitive type nodes in this graph.
542    #[inline]
543    pub fn primitives(&self) -> impl Iterator<Item = PrimitiveView<'_>> {
544        self.graph
545            .node_indices()
546            .filter_map(|index| match self.graph[index] {
547                GraphType::Schema(GraphSchemaType::Primitive(_, p))
548                | GraphType::Inline(GraphInlineType::Primitive(_, p)) => {
549                    Some(PrimitiveView::new(self, index, p))
550                }
551                _ => None,
552            })
553    }
554
555    /// Returns an iterator over all the operations in this graph.
556    #[inline]
557    pub fn operations(&self) -> impl Iterator<Item = OperationView<'_>> {
558        self.ops.iter().map(move |&op| OperationView::new(self, op))
559    }
560
561    #[inline]
562    pub(super) fn inherits(
563        &self,
564        node: NodeIndex<usize>,
565    ) -> impl Iterator<Item = OutgoingEdge<()>> {
566        self.graph
567            .edges_directed(node, Direction::Outgoing)
568            .filter(|e| matches!(e.weight(), GraphEdge::Inherits { .. }))
569            .map(|e| OutgoingEdge {
570                meta: (),
571                target: e.target(),
572            })
573    }
574
575    #[inline]
576    pub(super) fn fields(
577        &self,
578        node: NodeIndex<usize>,
579    ) -> impl Iterator<Item = OutgoingEdge<FieldMeta<'a>>> {
580        self.graph
581            .edges_directed(node, Direction::Outgoing)
582            .filter_map(|e| match e.weight() {
583                &GraphEdge::Field { meta, .. } => {
584                    let target = e.target();
585                    Some(OutgoingEdge { meta, target })
586                }
587                _ => None,
588            })
589    }
590
591    #[inline]
592    pub(super) fn variants(
593        &self,
594        node: NodeIndex<usize>,
595    ) -> impl Iterator<Item = OutgoingEdge<VariantMeta<'a>>> {
596        self.graph
597            .edges_directed(node, Direction::Outgoing)
598            .filter_map(|e| match e.weight() {
599                &GraphEdge::Variant(meta) => {
600                    let target = e.target();
601                    Some(OutgoingEdge { meta, target })
602                }
603                _ => None,
604            })
605    }
606}
607
608/// A variant that should be inlined into its tagged union.
609struct InlinableVariant<'a> {
610    /// The tagged union that owns this variant.
611    tagged: Node<'a, GraphTagged<'a>>,
612    /// The original variant struct node.
613    variant: Node<'a, GraphStruct<'a>>,
614}
615
616/// An edge between two types in the type graph.
617///
618/// Edges describe the relationship between their source and target types.
619#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
620pub enum GraphEdge<'a> {
621    /// The source type inherits from the target type.
622    Inherits { shadow: bool },
623    /// The source struct, tagged union, or untagged union
624    /// has the target type as a field.
625    Field { shadow: bool, meta: FieldMeta<'a> },
626    /// The source union has the target type as a variant.
627    Variant(VariantMeta<'a>),
628    /// The source type is an array, map, or optional that contains
629    /// the target type.
630    Contains,
631}
632
633impl GraphEdge<'_> {
634    /// Returns `true` if the target type should be excluded from
635    /// the source type's [inlines], but still considered a dependency.
636    ///
637    /// Shadow edges prevent inlined variant structs from claiming
638    /// their originals' inlines.
639    ///
640    /// [inlines]: crate::ir::views::View::inlines
641    #[inline]
642    pub fn shadow(&self) -> bool {
643        matches!(
644            self,
645            GraphEdge::Field { shadow: true, .. } | GraphEdge::Inherits { shadow: true }
646        )
647    }
648}
649
650/// Metadata describing an edge from a source to a target type.
651#[derive(Clone, Copy, Debug, Eq, PartialEq)]
652pub struct OutgoingEdge<T> {
653    pub meta: T,
654    pub target: NodeIndex<usize>,
655}
656
657#[derive(Clone, Copy)]
658struct Node<'a, Ty> {
659    index: NodeIndex<usize>,
660    info: SchemaTypeInfo<'a>,
661    ty: Ty,
662}
663
664/// Precomputed metadata for schema types and operations in the graph.
665pub(super) struct CookedGraphMetadata<'a> {
666    /// Transitive closure over the type graph.
667    pub closure: Closure,
668    /// Maps each type to its SCC equivalence class for boxing decisions.
669    /// Two types in the same class form a cycle that requires `Box<T>`.
670    pub box_sccs: Vec<usize>,
671    /// Whether each type can implement `Eq` and `Hash`.
672    pub hashable: FixedBitSet,
673    /// Whether each type can implement `Default`.
674    pub defaultable: FixedBitSet,
675    /// Maps each type to the operations that use it.
676    pub used_by: Vec<Vec<GraphOperation<'a>>>,
677    /// Maps each operation to the types that it uses.
678    pub uses: FxHashMap<GraphOperation<'a>, FixedBitSet>,
679    /// Opaque extended data for each type.
680    pub extensions: Vec<AtomicRefCell<ExtensionMap>>,
681}
682
683impl Debug for CookedGraphMetadata<'_> {
684    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
685        f.debug_struct("CookedGraphMetadata")
686            .field("closure", &self.closure)
687            .field("box_sccs", &self.box_sccs)
688            .field("hashable", &self.hashable)
689            .field("defaultable", &self.defaultable)
690            .field("used_by", &self.used_by)
691            .field("uses", &self.uses)
692            .finish_non_exhaustive()
693    }
694}
695
696/// Precomputed bitsets indicating which types can derive
697/// `Eq` / `Hash` and `Default`.
698struct HashDefault {
699    hashable: FixedBitSet,
700    defaultable: FixedBitSet,
701}
702
703/// Precomputed metadata for an operation that references
704/// types in the graph.
705struct Operations<'a> {
706    /// All the types that each operation depends on, directly and transitively.
707    pub uses: FxHashMap<GraphOperation<'a>, FixedBitSet>,
708    /// All the operations that use each type, directly and transitively.
709    pub used_by: Vec<Vec<GraphOperation<'a>>>,
710}
711
712struct MetadataBuilder<'graph, 'a> {
713    graph: &'graph CookedDiGraph<'a>,
714    ops: &'graph [&'graph GraphOperation<'a>],
715    /// The full transitive closure of each type's dependencies.
716    closure: Closure,
717}
718
719impl<'graph, 'a> MetadataBuilder<'graph, 'a> {
720    fn new(graph: &'graph CookedDiGraph<'a>, ops: &'graph [&'graph GraphOperation<'a>]) -> Self {
721        Self {
722            graph,
723            ops,
724            closure: Closure::new(graph),
725        }
726    }
727
728    fn build(self) -> CookedGraphMetadata<'a> {
729        let operations = self.operations();
730        let HashDefault {
731            hashable,
732            defaultable,
733        } = self.hash_default();
734        let box_sccs = self.box_sccs();
735        CookedGraphMetadata {
736            closure: self.closure,
737            box_sccs,
738            hashable,
739            defaultable,
740            used_by: operations.used_by,
741            uses: operations.uses,
742            // `AtomicRefCell` doesn't implement `Clone`,
743            // so we use this idiom instead of `vec!`.
744            extensions: std::iter::repeat_with(AtomicRefCell::default)
745                .take(self.graph.node_count())
746                .collect(),
747        }
748    }
749
750    fn operations(&self) -> Operations<'a> {
751        let mut operations = Operations {
752            uses: FxHashMap::default(),
753            used_by: vec![vec![]; self.graph.node_count()],
754        };
755
756        for &&op in self.ops {
757            // Forward propagation: start from the direct types, then
758            // expand to the full transitive dependency set.
759            let mut dependencies = FixedBitSet::with_capacity(self.graph.node_count());
760            for &node in op.types() {
761                dependencies.extend(self.closure.dependencies_of(node).map(|n| n.index()));
762            }
763            operations.uses.entry(op).insert_entry(dependencies);
764        }
765
766        // Backward propagation: mark types as used by their operations.
767        for (op, deps) in &operations.uses {
768            for node in deps.ones() {
769                operations.used_by[node].push(*op);
770            }
771        }
772
773        operations
774    }
775
776    fn box_sccs(&self) -> Vec<usize> {
777        let box_edges = EdgeFiltered::from_fn(self.graph, |e| match e.weight() {
778            // Inheritance edges don't contribute to cycles;
779            // a type can't inherit from itself.
780            GraphEdge::Inherits { .. } => false,
781            GraphEdge::Contains => match self.graph[e.source()] {
782                GraphType::Schema(GraphSchemaType::Container(_, c))
783                | GraphType::Inline(GraphInlineType::Container(_, c)) => {
784                    // Array and map containers are heap-allocated,
785                    // cycles through these edges don't need `Box`.
786                    !matches!(c, GraphContainer::Array { .. } | GraphContainer::Map { .. })
787                }
788                _ => true,
789            },
790            _ => true,
791        });
792        let mut scc = TarjanScc::new();
793        scc.run(&box_edges, |_| ());
794        self.graph
795            .node_indices()
796            .map(move |node| scc.node_component_index(&box_edges, node))
797            .collect()
798    }
799
800    fn hash_default(&self) -> HashDefault {
801        // Mark all leaf types that can't derive `Eq` / `Hash` or `Default`.
802        let n = self.graph.node_count();
803        let mut unhashable = FixedBitSet::with_capacity(n);
804        let mut undefaultable = FixedBitSet::with_capacity(n);
805        for node in self.graph.node_indices() {
806            use {GraphType::*, PrimitiveType::*};
807            match &self.graph[node] {
808                Schema(GraphSchemaType::Primitive(_, F32 | F64))
809                | Inline(GraphInlineType::Primitive(_, F32 | F64)) => {
810                    unhashable.insert(node.index());
811                }
812                Schema(
813                    GraphSchemaType::Primitive(_, Url)
814                    | GraphSchemaType::Tagged(_, _)
815                    | GraphSchemaType::Untagged(_, _),
816                )
817                | Inline(
818                    GraphInlineType::Primitive(_, Url)
819                    | GraphInlineType::Tagged(_, _)
820                    | GraphInlineType::Untagged(_, _),
821                ) => {
822                    undefaultable.insert(node.index());
823                }
824                _ => (),
825            }
826        }
827
828        // Compute the transitive closure over the inheritance subgraph.
829        let inherits = Closure::new(&EdgeFiltered::from_fn(self.graph, |e| {
830            matches!(e.weight(), GraphEdge::Inherits { .. })
831        }));
832
833        // Propagate unhashability backward, from leaves to roots.
834        //
835        // This is conservative: if a descendant overrides an inherited
836        // unhashable or undefaultable field with a different hashable or
837        // defaultable type, that descendant is still marked.
838        let mut queue: VecDeque<_> = unhashable.ones().map(NodeIndex::new).collect();
839        while let Some(node) = queue.pop_front() {
840            for edge in self.graph.edges_directed(node, Direction::Incoming) {
841                let source = edge.source();
842                match edge.weight() {
843                    GraphEdge::Contains | GraphEdge::Variant(_) => {
844                        if !unhashable.put(source.index()) {
845                            queue.push_back(source);
846                        }
847                    }
848                    GraphEdge::Field { .. } => {
849                        if !unhashable.put(source.index()) {
850                            queue.push_back(source);
851                        }
852                        // Every type that inherits from `source` also
853                        // inherits this unhashable field, so mark all
854                        // descendants of `source` as unhashable.
855                        for desc in inherits.dependents_of(source).filter(|&d| d != source) {
856                            if !unhashable.put(desc.index()) {
857                                queue.push_back(desc);
858                            }
859                        }
860                    }
861                    // Don't follow inheritance edges: a parent's intrinsic
862                    // unhashability (e.g., being a tagged union) doesn't
863                    // make its children unhashable, because children only
864                    // inherit the parent's fields, not its shape.
865                    GraphEdge::Inherits { .. } => {}
866                }
867            }
868        }
869
870        // Propagate undefaultability backward.
871        let mut queue: VecDeque<_> = undefaultable.ones().map(NodeIndex::new).collect();
872        while let Some(node) = queue.pop_front() {
873            for edge in self.graph.edges_directed(node, Direction::Incoming) {
874                if !matches!(
875                    edge.weight(),
876                    GraphEdge::Field { meta, .. } if meta.required
877                ) {
878                    // Optional fields become `AbsentOr<T>`,
879                    // which is always `Default`.
880                    continue;
881                }
882                let source = edge.source();
883                if !undefaultable.put(source.index()) {
884                    queue.push_back(source);
885                }
886                // Every type that inherits from `source` also
887                // inherits this undefaultable field, so mark all
888                // descendants of `source` as undefaultable.
889                for desc in inherits.dependents_of(source).filter(|&d| d != source) {
890                    if !undefaultable.put(desc.index()) {
891                        queue.push_back(desc);
892                    }
893                }
894            }
895        }
896
897        HashDefault {
898            hashable: invert(unhashable),
899            defaultable: invert(undefaultable),
900        }
901    }
902}
903
904/// Inverts every bit in the bitset.
905fn invert(mut bits: FixedBitSet) -> FixedBitSet {
906    bits.toggle_range(..);
907    bits
908}
909
910/// Visits all the types and references contained within a [`SpecType`].
911#[derive(Debug)]
912struct SpecTypeVisitor<'a> {
913    stack: Vec<(Option<(&'a SpecType<'a>, GraphEdge<'a>)>, &'a SpecType<'a>)>,
914}
915
916impl<'a> SpecTypeVisitor<'a> {
917    /// Creates a visitor with `roots` on the stack of types to visit.
918    #[inline]
919    fn new(roots: impl Iterator<Item = &'a SpecType<'a>>) -> Self {
920        let mut stack = roots.map(|root| (None, root)).collect_vec();
921        stack.reverse();
922        Self { stack }
923    }
924}
925
926impl<'a> Iterator for SpecTypeVisitor<'a> {
927    type Item = (Option<(&'a SpecType<'a>, GraphEdge<'a>)>, &'a SpecType<'a>);
928
929    fn next(&mut self) -> Option<Self::Item> {
930        let (parent, top) = self.stack.pop()?;
931        if matches!(
932            parent,
933            Some((
934                _,
935                GraphEdge::Variant(VariantMeta::Untagged(UntaggedVariantMeta::Null))
936            ))
937        ) {
938            // Unit variants form self-edges; skip them
939            // to avoid an infinite loop.
940            return Some((parent, top));
941        }
942        match top {
943            SpecType::Schema(SpecSchemaType::Struct(_, ty))
944            | SpecType::Inline(SpecInlineType::Struct(_, ty)) => {
945                self.stack.extend(
946                    itertools::chain!(
947                        ty.fields.iter().map(|field| (
948                            GraphEdge::Field {
949                                shadow: false,
950                                meta: FieldMeta {
951                                    name: field.name,
952                                    required: field.required,
953                                    description: field.description,
954                                    flattened: field.flattened,
955                                },
956                            },
957                            field.ty
958                        )),
959                        ty.parents
960                            .iter()
961                            .map(|parent| (GraphEdge::Inherits { shadow: false }, *parent)),
962                    )
963                    .map(|(edge, ty)| (Some((top, edge)), ty))
964                    .rev(),
965                );
966            }
967            SpecType::Schema(SpecSchemaType::Untagged(_, ty))
968            | SpecType::Inline(SpecInlineType::Untagged(_, ty)) => {
969                self.stack.extend(
970                    itertools::chain!(
971                        ty.fields.iter().map(|field| (
972                            GraphEdge::Field {
973                                shadow: false,
974                                meta: FieldMeta {
975                                    name: field.name,
976                                    required: field.required,
977                                    description: field.description,
978                                    flattened: field.flattened,
979                                },
980                            },
981                            field.ty
982                        )),
983                        ty.variants.iter().map(|variant| match variant {
984                            &SpecUntaggedVariant::Some(hint, ty) => {
985                                let meta = UntaggedVariantMeta::Type { hint };
986                                (GraphEdge::Variant(meta.into()), ty)
987                            }
988                            // `null` variants have no target type;
989                            // we represent these variants as self-edges.
990                            SpecUntaggedVariant::Null => {
991                                (GraphEdge::Variant(UntaggedVariantMeta::Null.into()), top)
992                            }
993                        }),
994                    )
995                    .map(|(edge, ty)| (Some((top, edge)), ty))
996                    .rev(),
997                );
998            }
999            SpecType::Schema(SpecSchemaType::Tagged(_, ty))
1000            | SpecType::Inline(SpecInlineType::Tagged(_, ty)) => {
1001                self.stack.extend(
1002                    itertools::chain!(
1003                        ty.fields.iter().map(|field| (
1004                            GraphEdge::Field {
1005                                shadow: false,
1006                                meta: FieldMeta {
1007                                    name: field.name,
1008                                    required: field.required,
1009                                    description: field.description,
1010                                    flattened: field.flattened,
1011                                },
1012                            },
1013                            field.ty
1014                        )),
1015                        ty.variants.iter().map(|variant| (
1016                            GraphEdge::Variant(
1017                                TaggedVariantMeta {
1018                                    name: variant.name,
1019                                    aliases: variant.aliases,
1020                                }
1021                                .into()
1022                            ),
1023                            variant.ty
1024                        )),
1025                    )
1026                    .map(|(edge, ty)| (Some((top, edge)), ty))
1027                    .rev(),
1028                );
1029            }
1030            SpecType::Schema(SpecSchemaType::Container(_, container))
1031            | SpecType::Inline(SpecInlineType::Container(_, container)) => {
1032                self.stack
1033                    .push((Some((top, GraphEdge::Contains)), container.inner().ty));
1034            }
1035            SpecType::Schema(
1036                SpecSchemaType::Enum(..) | SpecSchemaType::Primitive(..) | SpecSchemaType::Any(_),
1037            )
1038            | SpecType::Inline(
1039                SpecInlineType::Enum(..) | SpecInlineType::Primitive(..) | SpecInlineType::Any(_),
1040            ) => (),
1041            SpecType::Ref(_) => (),
1042        }
1043        Some((parent, top))
1044    }
1045}
1046
1047/// A map that can store one value for each type.
1048pub(super) type ExtensionMap = FxHashMap<TypeId, Box<dyn Extension>>;
1049
1050pub trait Extension: Any + Send + Sync {
1051    fn into_inner(self: Box<Self>) -> Box<dyn Any>;
1052}
1053
1054impl dyn Extension {
1055    #[inline]
1056    pub fn downcast_ref<T: 'static>(&self) -> Option<&T> {
1057        (self as &dyn Any).downcast_ref::<T>()
1058    }
1059}
1060
1061impl<T: Send + Sync + 'static> Extension for T {
1062    #[inline]
1063    fn into_inner(self: Box<Self>) -> Box<dyn Any> {
1064        self
1065    }
1066}
1067
1068/// Strongly connected components (SCCs) in topological order.
1069///
1070/// [`TopoSccs`] uses Tarjan's single-pass algorithm to find all SCCs,
1071/// and provides topological ordering, efficient membership testing, and
1072/// condensation for computing the transitive closure. These are
1073/// building blocks for cycle detection and dependency propagation.
1074struct TopoSccs<G> {
1075    graph: G,
1076    tarjan: TarjanScc<NodeIndex<usize>>,
1077    sccs: Vec<Vec<usize>>,
1078}
1079
1080impl<G> TopoSccs<G>
1081where
1082    G: Closable<NodeIndex<usize>> + Copy,
1083{
1084    fn new(graph: G) -> Self {
1085        let mut sccs = Vec::new();
1086        let mut tarjan = TarjanScc::new();
1087        tarjan.run(graph, |scc_nodes| {
1088            sccs.push(scc_nodes.iter().map(|node| node.index()).collect());
1089        });
1090        // Tarjan's algorithm returns SCCs in reverse topological order;
1091        // reverse them to get the topological order.
1092        sccs.reverse();
1093        Self {
1094            graph,
1095            tarjan,
1096            sccs,
1097        }
1098    }
1099
1100    #[inline]
1101    fn scc_count(&self) -> usize {
1102        self.sccs.len()
1103    }
1104
1105    /// Returns the topological index of the SCC that contains the given node.
1106    #[inline]
1107    fn topo_index(&self, node: NodeIndex<usize>) -> usize {
1108        // Tarjan's algorithm returns indices in reverse topological order;
1109        // inverting the component index gets us the topological index.
1110        self.sccs.len() - 1 - self.tarjan.node_component_index(self.graph, node)
1111    }
1112
1113    /// Builds a condensed DAG of SCCs.
1114    ///
1115    /// The condensed graph is represented as an adjacency list, where both
1116    /// the node indices and the neighbors of each node are stored in
1117    /// topological order. This specific ordering is required by
1118    /// [`tred::dag_transitive_reduction_closure`].
1119    fn condensation(&self) -> UnweightedList<usize> {
1120        let mut dag = UnweightedList::with_capacity(self.scc_count());
1121        for to in 0..self.scc_count() {
1122            dag.add_node();
1123            for neighbor in self.sccs[to].iter().flat_map(|&index| {
1124                self.graph
1125                    .neighbors_directed(NodeIndex::new(index), Direction::Incoming)
1126            }) {
1127                let from = self.topo_index(neighbor);
1128                if from != to {
1129                    dag.update_edge(from, to, ());
1130                }
1131            }
1132        }
1133        dag
1134    }
1135}
1136
1137/// The transitive closure of a graph.
1138#[derive(Debug)]
1139pub(super) struct Closure {
1140    /// Maps each node index to its SCC's topological index.
1141    scc_indices: Vec<usize>,
1142    /// Members of each SCC, indexed by topological SCC index.
1143    scc_members: Vec<Vec<usize>>,
1144    /// Maps each SCC to a list of all the SCCs that it transitively depends on,
1145    /// excluding itself.
1146    scc_deps: Vec<Vec<usize>>,
1147    /// Maps each SCC to a list of all the SCCs that transitively depend on it,
1148    /// excluding itself.
1149    scc_rdeps: Vec<Vec<usize>>,
1150}
1151
1152impl Closure {
1153    /// Computes the transitive closure of a graph.
1154    fn new<G>(graph: G) -> Self
1155    where
1156        G: Closable<NodeIndex<usize>> + Copy,
1157    {
1158        let sccs = TopoSccs::new(graph);
1159        let condensation = sccs.condensation();
1160        let (_, closure) = tred::dag_transitive_reduction_closure(&condensation);
1161
1162        // Build the forward and reverse adjacency lists
1163        // from the transitive closure graph.
1164        let scc_deps = (0..sccs.scc_count())
1165            .map(|scc| closure.neighbors(scc).collect_vec())
1166            .collect_vec();
1167        let mut scc_rdeps = vec![vec![]; sccs.scc_count()];
1168        for (scc, deps) in scc_deps.iter().enumerate() {
1169            for &dep in deps {
1170                scc_rdeps[dep].push(scc);
1171            }
1172        }
1173
1174        let mut scc_indices = vec![0; graph.node_count()];
1175        for node in graph.node_identifiers() {
1176            scc_indices[node.index()] = sccs.topo_index(node);
1177        }
1178
1179        Closure {
1180            scc_indices,
1181            scc_members: sccs.sccs.iter().cloned().collect_vec(),
1182            scc_deps,
1183            scc_rdeps,
1184        }
1185    }
1186
1187    /// Returns the topological SCC index for the given node.
1188    #[inline]
1189    pub fn scc_index_of(&self, node: NodeIndex<usize>) -> usize {
1190        self.scc_indices[node.index()]
1191    }
1192
1193    /// Iterates over all nodes that `node` transitively depends on,
1194    /// including `node` and all members of its SCC.
1195    pub fn dependencies_of(
1196        &self,
1197        node: NodeIndex<usize>,
1198    ) -> impl Iterator<Item = NodeIndex<usize>> {
1199        let scc = self.scc_index_of(node);
1200        std::iter::once(scc)
1201            .chain(self.scc_deps[scc].iter().copied())
1202            .flat_map(|s| self.scc_members[s].iter().copied()) // Expand SCCs to nodes.
1203            .map(NodeIndex::new)
1204    }
1205
1206    /// Iterates over all nodes that transitively depend on `node`,
1207    /// including `node` and all members of its SCC.
1208    pub fn dependents_of(&self, node: NodeIndex<usize>) -> impl Iterator<Item = NodeIndex<usize>> {
1209        let scc = self.scc_index_of(node);
1210        std::iter::once(scc)
1211            .chain(self.scc_rdeps[scc].iter().copied())
1212            .flat_map(|s| self.scc_members[s].iter().copied())
1213            .map(NodeIndex::new)
1214    }
1215
1216    /// Returns whether `node` transitively depends on `other`,
1217    /// or `false` when `node == other`.
1218    #[inline]
1219    pub fn depends_on(&self, node: NodeIndex<usize>, other: NodeIndex<usize>) -> bool {
1220        if node == other {
1221            return false;
1222        }
1223        let scc = self.scc_index_of(node);
1224        let other_scc = self.scc_index_of(other);
1225        scc == other_scc || self.scc_deps[scc].contains(&other_scc)
1226    }
1227}
1228
1229/// Trait requirements for computing a transitive closure.
1230trait Closable<N>:
1231    NodeCount
1232    + IntoNodeIdentifiers<NodeId = N>
1233    + IntoNeighbors<NodeId = N>
1234    + IntoNeighborsDirected<NodeId = N>
1235    + NodeIndexable<NodeId = N>
1236{
1237}
1238
1239impl<N, G> Closable<N> for G where
1240    G: NodeCount
1241        + IntoNodeIdentifiers<NodeId = N>
1242        + IntoNeighbors<NodeId = N>
1243        + IntoNeighborsDirected<NodeId = N>
1244        + NodeIndexable<NodeId = N>
1245{
1246}
1247
1248#[cfg(test)]
1249mod tests {
1250    use super::*;
1251
1252    use crate::tests::assert_matches;
1253
1254    /// Creates a simple graph: `A -> B -> C`.
1255    fn linear_graph() -> DiGraph<(), (), usize> {
1256        let mut g = DiGraph::default();
1257        let a = g.add_node(());
1258        let b = g.add_node(());
1259        let c = g.add_node(());
1260        g.extend_with_edges([(a, b), (b, c)]);
1261        g
1262    }
1263
1264    /// Creates a cyclic graph: `A -> B -> C -> A`, with `D -> A`.
1265    fn cyclic_graph() -> DiGraph<(), (), usize> {
1266        let mut g = DiGraph::default();
1267        let a = g.add_node(());
1268        let b = g.add_node(());
1269        let c = g.add_node(());
1270        let d = g.add_node(());
1271        g.extend_with_edges([(a, b), (b, c), (c, a), (d, a)]);
1272        g
1273    }
1274
1275    // MARK: SCC detection
1276
1277    #[test]
1278    fn test_linear_graph_has_singleton_sccs() {
1279        let g = linear_graph();
1280        let sccs = TopoSccs::new(&g);
1281        let sizes = sccs.sccs.iter().map(|scc| scc.len()).collect_vec();
1282        assert_matches!(&*sizes, [1, 1, 1]);
1283    }
1284
1285    #[test]
1286    fn test_cyclic_graph_has_one_multi_node_scc() {
1287        let g = cyclic_graph();
1288        let sccs = TopoSccs::new(&g);
1289
1290        // A-B-C form one SCC; D is its own SCC. Since D has an edge to
1291        // the cycle, D must precede the cycle in topological order.
1292        let sizes = sccs.sccs.iter().map(|scc| scc.len()).collect_vec();
1293        assert_matches!(&*sizes, [1, 3]);
1294    }
1295
1296    // MARK: Topological ordering
1297
1298    #[test]
1299    fn test_sccs_are_in_topological_order() {
1300        let g = cyclic_graph();
1301        let sccs = TopoSccs::new(&g);
1302
1303        let d_topo = sccs.topo_index(3.into());
1304        let a_topo = sccs.topo_index(0.into());
1305        assert!(
1306            d_topo < a_topo,
1307            "D should precede A-B-C in topological order"
1308        );
1309    }
1310
1311    #[test]
1312    fn test_topo_index_consistent_within_scc() {
1313        let g = cyclic_graph();
1314        let sccs = TopoSccs::new(&g);
1315
1316        // A, B, C are in the same SCC, so they should have
1317        // the same topological index.
1318        let a_topo = sccs.topo_index(0.into());
1319        let b_topo = sccs.topo_index(1.into());
1320        let c_topo = sccs.topo_index(2.into());
1321
1322        assert_eq!(a_topo, b_topo);
1323        assert_eq!(b_topo, c_topo);
1324    }
1325
1326    // MARK: Condensation
1327
1328    #[test]
1329    fn test_condensation_has_correct_node_count() {
1330        let g = cyclic_graph();
1331        let sccs = TopoSccs::new(&g);
1332        let dag = sccs.condensation();
1333
1334        assert_eq!(dag.node_count(), 2);
1335    }
1336
1337    #[test]
1338    fn test_condensation_has_correct_edges() {
1339        let g = cyclic_graph();
1340        let sccs = TopoSccs::new(&g);
1341        let dag = sccs.condensation();
1342
1343        // D should have an edge to the A-B-C SCC, and
1344        // A-B-C shouldn't create a self-loop.
1345        let d_topo = sccs.topo_index(3.into());
1346        let abc_topo = sccs.topo_index(0.into());
1347
1348        let d_neighbors = dag.neighbors(d_topo).collect_vec();
1349        assert_eq!(&*d_neighbors, [abc_topo]);
1350
1351        let abc_neighbors = dag.neighbors(abc_topo).collect_vec();
1352        assert!(abc_neighbors.is_empty());
1353    }
1354
1355    #[test]
1356    fn test_condensation_neighbors_in_topological_order() {
1357        // Matches Petgraph's `dag_to_toposorted_adjacency_list` example:
1358        // edges added as `(top, second), (top, first)`, but neighbors should be
1359        // `[first, second]` in topological order.
1360        let mut g = DiGraph::<(), (), usize>::default();
1361        let second = g.add_node(());
1362        let top = g.add_node(());
1363        let first = g.add_node(());
1364        g.extend_with_edges([(top, second), (top, first), (first, second)]);
1365
1366        let sccs = TopoSccs::new(&g);
1367        let dag = sccs.condensation();
1368
1369        let top_topo = sccs.topo_index(top);
1370        assert_eq!(top_topo, 0);
1371
1372        let first_topo = sccs.topo_index(first);
1373        assert_eq!(first_topo, 1);
1374
1375        let second_topo = sccs.topo_index(second);
1376        assert_eq!(second_topo, 2);
1377
1378        let neighbors = dag.neighbors(top_topo).collect_vec();
1379        assert_eq!(&*neighbors, [first_topo, second_topo]);
1380    }
1381}