Skip to main content

ploidy_core/ir/
graph.rs

1use std::{
2    any::{Any, TypeId as StdTypeId},
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, EdgeIndex, NodeIndex},
16    stable_graph::StableDiGraph,
17    visit::{
18        DfsPostOrder, EdgeFiltered, EdgeRef, GraphRef, IntoEdgeReferences, IntoEdges,
19        IntoNeighbors, IntoNeighborsDirected, IntoNodeIdentifiers, NodeCount, NodeIndexable,
20        VisitMap, Visitable,
21    },
22};
23use rustc_hash::{FxBuildHasher, FxHashMap};
24
25use crate::{arena::Arena, ir::TypeView, parse::Info};
26
27use super::{
28    spec::{ResolvedSpecType, Spec},
29    types::{
30        FieldMeta, GraphContainer, GraphInlineType, GraphOperation, GraphSchemaType, GraphStruct,
31        GraphTagged, GraphType, InlineTypeId, InlineTypeIds, InlineTypePathRoot, OperationUsage,
32        PrimitiveType, SpecInlineType, SpecSchemaType, SpecType, SpecUntaggedVariant,
33        StructFieldName, TaggedVariantMeta, UntaggedVariantMeta, VariantMeta,
34        shape::{Operation, Parameter, ParameterInfo, Request, Response},
35    },
36    views::{TypeId, operation::OperationView, primitive::PrimitiveView, schema::SchemaTypeView},
37};
38
39/// The mutable, sparse graph used for transformations.
40type RawDiGraph<'a> = StableDiGraph<GraphType<'a>, GraphEdge<'a>, usize>;
41
42/// The immutable, dense graph used for code generation.
43type CookedDiGraph<'a> = DiGraph<GraphType<'a>, GraphEdge<'a>, usize>;
44
45/// A mutable intermediate dependency graph of all the types in a [`Spec`],
46/// backed by a sparse [`StableDiGraph`].
47///
48/// This graph is constructed directly from a [`Spec`], and represents
49/// type relationships as they exist in the spec. Transformations like
50/// [`inline_tagged_variants`][Self::inline_tagged_variants] rewrite this graph
51/// in place.
52///
53/// After applying all transformations, call [`cook`][Self::cook] to
54/// turn this graph into a [`CookedGraph`] that's ready for code generation.
55#[derive(Debug)]
56pub struct RawGraph<'a> {
57    arena: &'a Arena,
58    spec: &'a Spec<'a>,
59    graph: RawDiGraph<'a>,
60    schemas: FxHashMap<&'a str, NodeIndex<usize>>,
61    ops: &'a [&'a GraphOperation<'a>],
62    ids: InlineTypeIds<'a>,
63}
64
65impl<'a> RawGraph<'a> {
66    /// Builds a raw type graph from the given spec.
67    pub fn new(arena: &'a Arena, spec: &'a Spec<'a>) -> Self {
68        // All roots (named schemas, parameters, request and response bodies),
69        // and all the types within them (inline schemas and primitives).
70        let tys = SpecTypeVisitor::new(
71            spec.schemas
72                .values()
73                .chain(spec.operations.iter().flat_map(|op| op.types().copied())),
74        );
75
76        // Inflate a graph from the traversal.
77        let mut indices = FxHashMap::default();
78        let mut schemas = FxHashMap::default();
79        let mut graph = RawDiGraph::default();
80        for (parent, child) in tys {
81            use std::collections::hash_map::Entry;
82
83            let source = spec.resolve(child);
84            let &mut to = match indices.entry(source) {
85                Entry::Occupied(entry) => entry.into_mut(),
86                Entry::Vacant(entry) => {
87                    // We might see the same schema multiple times if it's
88                    // referenced multiple times in the spec. Only add
89                    // a new node for the schema if we haven't seen it before.
90                    let index = graph.add_node(match *entry.key() {
91                        ResolvedSpecType::Schema(&ty) => GraphType::Schema(ty.into()),
92                        ResolvedSpecType::Inline(&ty) => GraphType::Inline(ty.into()),
93                    });
94                    if let ResolvedSpecType::Schema(ty) = source {
95                        schemas.entry(ty.name()).or_insert(index);
96                    }
97                    entry.insert(index)
98                }
99            };
100
101            if let Some((parent, edge)) = parent {
102                let destination = spec.resolve(parent);
103                let &mut from = match indices.entry(destination) {
104                    Entry::Occupied(entry) => entry.into_mut(),
105                    Entry::Vacant(entry) => {
106                        let index = graph.add_node(match *entry.key() {
107                            ResolvedSpecType::Schema(&ty) => GraphType::Schema(ty.into()),
108                            ResolvedSpecType::Inline(&ty) => GraphType::Inline(ty.into()),
109                        });
110                        if let ResolvedSpecType::Schema(ty) = destination {
111                            schemas.entry(ty.name()).or_insert(index);
112                        }
113                        entry.insert(index)
114                    }
115                };
116                graph.add_edge(from, to, edge);
117            }
118        }
119
120        // Map type references in operations to graph indices.
121        let ops = arena.alloc_slice_exact(spec.operations.iter().map(|op| {
122            let params = arena.alloc_slice_exact(op.params.iter().map(|param| match param {
123                Parameter::Path(info) => Parameter::Path(ParameterInfo {
124                    name: info.name,
125                    ty: match info.ty {
126                        SpecType::Schema(s) => indices[&ResolvedSpecType::Schema(s)],
127                        SpecType::Inline(i) => indices[&ResolvedSpecType::Inline(i)],
128                        SpecType::Ref(r) => schemas[&*r.name()],
129                    },
130                    required: info.required,
131                    description: info.description,
132                    style: info.style,
133                }),
134                Parameter::Query(info) => Parameter::Query(ParameterInfo {
135                    name: info.name,
136                    ty: match info.ty {
137                        SpecType::Schema(s) => indices[&ResolvedSpecType::Schema(s)],
138                        SpecType::Inline(i) => indices[&ResolvedSpecType::Inline(i)],
139                        SpecType::Ref(r) => schemas[&*r.name()],
140                    },
141                    required: info.required,
142                    description: info.description,
143                    style: info.style,
144                }),
145            }));
146
147            let request = op.request.as_ref().map(|r| match r {
148                Request::Json(ty) => Request::Json(match ty {
149                    SpecType::Schema(s) => indices[&ResolvedSpecType::Schema(s)],
150                    SpecType::Inline(i) => indices[&ResolvedSpecType::Inline(i)],
151                    SpecType::Ref(r) => schemas[&*r.name()],
152                }),
153                Request::Multipart => Request::Multipart,
154            });
155
156            let response = op.response.as_ref().map(|r| match r {
157                Response::Json(ty) => Response::Json(match ty {
158                    SpecType::Schema(s) => indices[&ResolvedSpecType::Schema(s)],
159                    SpecType::Inline(i) => indices[&ResolvedSpecType::Inline(i)],
160                    SpecType::Ref(r) => schemas[&*r.name()],
161                }),
162            });
163
164            &*arena.alloc(Operation {
165                id: op.id,
166                method: op.method,
167                path: op.path,
168                resource: op.resource,
169                description: op.description,
170                params,
171                request,
172                response,
173            })
174        }));
175
176        Self {
177            arena,
178            spec,
179            graph,
180            schemas,
181            ops,
182            ids: spec.ids,
183        }
184    }
185
186    /// Inlines schema types used as variants of multiple tagged unions
187    /// with different tags.
188    ///
189    /// In OpenAPI's model of tagged unions, the tag always references a field
190    /// that's defined on each variant struct. This model works well for Python
191    /// and TypeScript, but not Rust; Serde doesn't allow variant structs to
192    /// declare fields with the same name as the tag. The Rust generator
193    /// excludes tag fields when generating structs, but this introduces a
194    /// new problem: a struct can't appear as a variant of multiple unions
195    /// with different tags [^1].
196    ///
197    /// This transformation finds and inlines these structs, so that
198    /// the Rust generator can safely omit their tag fields.
199    ///
200    /// [^1]: If struct A has fields `foo` and `bar`, A is a variant of
201    /// tagged unions C and D, C's tag is `foo`, and D's tag is `bar`...
202    /// only `foo` should be excluded when A is used in C, and only `bar`
203    /// should be excluded when A is used in D; but this can't be modeled
204    /// in Serde without splitting A into two distinct types.
205    pub fn inline_tagged_variants(&mut self) -> &mut Self {
206        // Collect all inlining decisions before mutating the graph,
207        // so that we can check inlinability per variant.
208        let inlinables = self.inlinable_tagged_variants().collect_vec();
209
210        let mut retargets = FxHashMap::default();
211        retargets.reserve(inlinables.len());
212
213        // Add nodes for the inlined variant structs,
214        // and their outgoing edges.
215        for InlinableVariant { tagged, variant } in inlinables {
216            // Duplicate the variant struct as an inline type,
217            // with its original metadata.
218            let id = self.ids.next();
219            let index = self
220                .graph
221                .add_node(GraphType::Inline(GraphInlineType::Struct(id, variant.ty)));
222
223            // Create shadow edges to the original variant struct's fields.
224            // These serve two purposes:
225            //
226            // 1. If a field is recursive, the duplicate joins the field's SCC,
227            //    not the original's SCC, so field edges to the original type
228            //    won't be treated as cyclic.
229            // 2. Hiding the originals' inlines from the duplicate's inlines.
230            //
231            // `fields()` yields edges in reverse order of addition;
232            // we collect and reverse to add them in their original order.
233            let original_field_edges = self.fields(variant.index).collect_vec();
234            for edge in original_field_edges.into_iter().rev() {
235                self.graph.add_edge(
236                    index,
237                    edge.target,
238                    GraphEdge::Field {
239                        meta: edge.meta,
240                        shadow: true,
241                    },
242                );
243            }
244
245            // Inherit from the tagged union (to pick up its own fields)
246            // and the original variant struct (to pick up its ancestors).
247            // The union is added first so that its fields appear first _and_
248            // can be overridden by the variant's fields.
249            self.graph
250                .add_edge(index, tagged.index, GraphEdge::Inherits { shadow: true });
251            self.graph
252                .add_edge(index, variant.index, GraphEdge::Inherits { shadow: true });
253
254            retargets.insert((tagged.index, variant.index), index);
255        }
256
257        // Retarget every tagged union's variant edges to the new structs.
258        let taggeds: FixedBitSet = retargets
259            .keys()
260            .map(|&(tagged, _)| tagged.index())
261            .collect();
262        for index in taggeds.ones().map(NodeIndex::new) {
263            let old_edges = self
264                .graph
265                .edges_directed(index, Direction::Outgoing)
266                .filter(|e| matches!(e.weight(), GraphEdge::Variant(_)))
267                .map(|e| (e.id(), *e.weight(), e.target()))
268                .collect_vec();
269            for &(id, _, _) in &old_edges {
270                self.graph.remove_edge(id);
271            }
272            // Re-add edges. `edges_directed` yields edges in reverse order
273            // of addition; reversing them adds edges in their original order.
274            for (_, weight, target) in old_edges.into_iter().rev() {
275                let new_target = retargets.get(&(index, target)).copied().unwrap_or(target);
276                self.graph.add_edge(index, new_target, weight);
277            }
278        }
279
280        self
281    }
282
283    /// Simplifies trivial `allOf` compositions.
284    ///
285    /// Drops inheritance edges from `Any`, and collapses inline structs and
286    /// unions with a single parent and without own fields or variants,
287    /// to that parent. Trivial named schemas are preserved, so that user code
288    /// can refer to them by name.
289    ///
290    /// This transformation simplifies OpenAPI 3.1 `$ref` schemas with adjacent
291    /// keywords. A schema like `{ "$ref": "...", "description": "..." }` is
292    /// sugar for an inline `allOf` schema with two subschemas: a reference and
293    /// an inline with just a `description`. The latter doesn't contribute any
294    /// type constraints, so the inline `allOf` is redundant.
295    pub fn collapse_trivial_inlines(&mut self) -> &mut Self {
296        // `Any` doesn't contribute any members, and dropping `Inherits` edges
297        // to it here simplifies the logic to find collapsible inlines and
298        // their new targets.
299        let inherits_to_any = self
300            .graph
301            .edge_references()
302            .filter(|edge| {
303                matches!(edge.weight(), GraphEdge::Inherits { .. })
304                    && matches!(
305                        self.graph[edge.target()],
306                        GraphType::Schema(GraphSchemaType::Any(_))
307                            | GraphType::Inline(GraphInlineType::Any(_))
308                    )
309            })
310            .map(|edge| edge.id())
311            .collect_vec();
312        for id in inherits_to_any {
313            self.graph.remove_edge(id);
314        }
315
316        // Find all inlines with no own members (i.e., no fields or variants)
317        // and exactly one parent.
318        let collapsibles: FixedBitSet = self
319            .graph
320            .node_indices()
321            .filter(|&node| {
322                matches!(
323                    self.graph[node],
324                    GraphType::Inline(
325                        GraphInlineType::Struct(..)
326                            | GraphInlineType::Tagged(..)
327                            | GraphInlineType::Untagged(..)
328                    )
329                )
330            })
331            .filter(|&node| {
332                self.graph
333                    .edges_directed(node, Direction::Outgoing)
334                    .all(|edge| {
335                        !matches!(
336                            edge.weight(),
337                            GraphEdge::Field { .. } | GraphEdge::Variant(_)
338                        )
339                    })
340                    && self
341                        .graph
342                        .edges_directed(node, Direction::Outgoing)
343                        .filter(|edge| {
344                            matches!(edge.weight(), GraphEdge::Inherits { .. })
345                                && edge.target() != node
346                        })
347                        .exactly_one()
348                        .is_ok()
349            })
350            .map(|node| node.index())
351            .collect();
352
353        // Compute the transitive closure over the inheritance subgraph
354        // rooted at the collapsible set, then map each collapsible node to its
355        // non-collapsible target. Each collapsible has exactly one
356        // outgoing edge in this subgraph, so its dependency set is a linear
357        // chain ending in either the target or a cycle entirely within the
358        // collapsible set. Only chains with a target are recorded.
359        let closure = Closure::new(&EdgeFiltered::from_fn(&self.graph, |edge| {
360            matches!(edge.weight(), GraphEdge::Inherits { .. })
361                && collapsibles.contains(edge.source().index())
362        }));
363        let collapsed_to: FxHashMap<_, _> = collapsibles
364            .ones()
365            .map(NodeIndex::new)
366            .flat_map(|node| {
367                closure
368                    .dependencies_of(node)
369                    .find(|target| !collapsibles.contains(target.index()))
370                    .map(|target| (node, target))
371            })
372            .collect();
373        if collapsed_to.is_empty() {
374            return self;
375        }
376
377        // Redirect every incoming edge of a collapsible to its target.
378        // Skip edges whose source is also collapsible; we'll remove those last.
379        let sources_to_redirect: FixedBitSet = self
380            .graph
381            .edge_references()
382            .filter(|edge| {
383                collapsed_to.contains_key(&edge.target())
384                    && !collapsed_to.contains_key(&edge.source())
385            })
386            .map(|edge| edge.source().index())
387            .collect();
388        for source in sources_to_redirect.ones().map(NodeIndex::new) {
389            let old_edges = self
390                .graph
391                .edges_directed(source, Direction::Outgoing)
392                .map(|edge| {
393                    let old_target = edge.target();
394                    let new_target = collapsed_to.get(&old_target).copied().unwrap_or(old_target);
395                    (edge.id(), *edge.weight(), new_target)
396                })
397                .collect_vec();
398            for &(id, _, _) in &old_edges {
399                self.graph.remove_edge(id);
400            }
401            // Re-add edges. `edges_directed` yields edges in reverse order
402            // of addition; reversing them adds edges in their original order.
403            // Since edge order matters, we need to remove and re-add them all.
404            for (_, weight, target) in old_edges.into_iter().rev() {
405                self.graph.add_edge(source, target, weight);
406            }
407        }
408
409        // Redirect operation roots, which don't have graph edges.
410        let ops = self
411            .ops
412            .iter()
413            .map(|&op| {
414                let params = op
415                    .params
416                    .iter()
417                    .map(|&param| {
418                        let rewrite = match param {
419                            Parameter::Path(info) => collapsed_to
420                                .get(&info.ty)
421                                .map(|&ty| Parameter::Path(ParameterInfo { ty, ..info })),
422                            Parameter::Query(info) => collapsed_to
423                                .get(&info.ty)
424                                .map(|&ty| Parameter::Query(ParameterInfo { ty, ..info })),
425                        };
426                        rewrite.unwrap_or(param)
427                    })
428                    .collect_vec();
429
430                let request = op
431                    .request
432                    .and_then(|request| match request {
433                        Request::Json(ty) => {
434                            let &ty = collapsed_to.get(&ty)?;
435                            Some(Request::Json(ty))
436                        }
437                        Request::Multipart => None,
438                    })
439                    .or(op.request);
440
441                let response = op
442                    .response
443                    .and_then(|response| match response {
444                        Response::Json(ty) => {
445                            let &ty = collapsed_to.get(&ty)?;
446                            Some(Response::Json(ty))
447                        }
448                    })
449                    .or(op.response);
450
451                if params == op.params && request == op.request && response == op.response {
452                    op
453                } else {
454                    self.arena.alloc(Operation {
455                        params: self.arena.alloc_slice_copy(&params),
456                        request,
457                        response,
458                        ..*op
459                    })
460                }
461            })
462            .collect_vec();
463        if ops != self.ops {
464            self.ops = self.arena.alloc_slice_copy(&ops);
465        }
466
467        // Remove every collapsed node. `remove_node` removes incident edges,
468        // including those between two collapsibles in a chain.
469        for (node, _) in collapsed_to {
470            self.graph.remove_node(node);
471        }
472
473        self
474    }
475
476    /// Builds an immutable [`CookedGraph`] from this mutable raw graph.
477    #[inline]
478    pub fn cook(&self) -> CookedGraph<'a> {
479        CookedGraph::new(self)
480    }
481
482    /// Returns an iterator over all the fields of a struct or union type,
483    /// in reverse insertion order.
484    fn fields(&self, node: NodeIndex<usize>) -> impl Iterator<Item = OutgoingEdge<FieldMeta<'a>>> {
485        self.graph
486            .edges_directed(node, Direction::Outgoing)
487            .filter_map(|e| match e.weight() {
488                &GraphEdge::Field { meta, .. } => {
489                    let target = e.target();
490                    Some(OutgoingEdge { meta, target })
491                }
492                _ => None,
493            })
494    }
495
496    /// Returns an iterator over all the tagged union variant structs
497    /// that should be inlined.
498    fn inlinable_tagged_variants(&self) -> impl Iterator<Item = InlinableVariant<'a>> {
499        // Compute the set of types used by all operations.
500        // Operations don't participate in the graph, but
501        // still need to be considered when deciding
502        // whether to inline a variant struct.
503        //
504        // Otherwise, a struct that's used by same-tag unions
505        // _and_ an operation wouldn't be inlined, incorrectly
506        // removing its tag field.
507        let used_by_ops: FixedBitSet = self
508            .ops
509            .iter()
510            .flat_map(|op| op.types())
511            .map(|index| index.index())
512            .collect();
513
514        self.graph
515            .node_indices()
516            .filter_map(|index| match self.graph[index] {
517                GraphType::Schema(GraphSchemaType::Tagged(_, ty)) => Some(Node { index, ty }),
518                _ => None,
519            })
520            .flat_map(move |tagged| {
521                self.graph
522                    .edges_directed(tagged.index, Direction::Outgoing)
523                    .filter(|e| matches!(e.weight(), GraphEdge::Variant(_)))
524                    .filter_map(move |e| match self.graph[e.target()] {
525                        GraphType::Schema(GraphSchemaType::Struct(_, ty)) => {
526                            let index = e.target();
527                            Some((tagged, Node { index, ty }))
528                        }
529                        _ => None,
530                    })
531            })
532            .filter_map(move |(tagged, variant)| {
533                // A variant struct only needs inlining if it has multiple
534                // distinct uses. Skip if (1) no operation uses the struct,
535                // _and_ (2) every incoming edge is from a tagged union with
536                // the same tag and fields. If both hold, all uses agree, so
537                // the struct can be used directly without inlining.
538                if used_by_ops[variant.index.index()] {
539                    return Some((tagged, variant));
540                }
541
542                // Check that all the variant's inbound edges are from
543                // tagged unions, and that all their tags and field
544                // edges match the first union we found.
545                let first_tagged = self
546                    .graph
547                    .neighbors_directed(variant.index, Direction::Incoming)
548                    .find_map(|index| match self.graph[index] {
549                        GraphType::Schema(GraphSchemaType::Tagged(_, ty)) => {
550                            Some(Node { index, ty })
551                        }
552                        _ => None,
553                    })?;
554                let all_agree = self
555                    .graph
556                    .neighbors_directed(variant.index, Direction::Incoming)
557                    .all(|index| match self.graph[index] {
558                        GraphType::Schema(GraphSchemaType::Tagged(_, ty)) => {
559                            ty.tag == first_tagged.ty.tag
560                                && self.fields(index).eq(self.fields(first_tagged.index))
561                        }
562                        _ => false,
563                    });
564                if all_agree {
565                    return None;
566                }
567                Some((tagged, variant))
568            })
569            .filter_map(|(tagged, variant)| {
570                // Skip inlining when the inline copy would be identical
571                // to the original. This happens when the variant
572                // doesn't declare the tag as a field _and_ either
573                // (a) the union has no own fields, or (b) the variant
574                // inherits from the union.
575                let ancestors = EdgeFiltered::from_fn(&self.graph, |e| {
576                    matches!(e.weight(), GraphEdge::Inherits { .. })
577                });
578                let mut dfs = DfsPostOrder::new(&ancestors, variant.index);
579                let has_tag_field = std::iter::from_fn(|| dfs.next(&ancestors))
580                    .filter(|&n| {
581                        matches!(
582                            self.graph[n],
583                            GraphType::Schema(GraphSchemaType::Struct(..))
584                                | GraphType::Inline(GraphInlineType::Struct(..))
585                        )
586                    })
587                    .any(|n| {
588                        self.fields(n).any(|f| {
589                            matches!(f.meta.name, StructFieldName::Name(name)
590                                if name == tagged.ty.tag)
591                        })
592                    });
593
594                // If the variant declares or inherits the tag field,
595                // we must inline, so that the inline copy can safely
596                // omit the tag.
597                if has_tag_field {
598                    return Some(InlinableVariant { tagged, variant });
599                }
600
601                // If the DFS visited the union, the variant already inherits
602                // its fields; the inline copy would be identical.
603                if dfs.discovered[tagged.index.index()] {
604                    return None;
605                }
606
607                // If the variant doesn't inherit from the union, but the union
608                // has no fields of its own, the inline copy would be identical.
609                self.fields(tagged.index).next()?;
610
611                Some(InlinableVariant { tagged, variant })
612            })
613    }
614}
615
616/// The final dependency graph of all the types in a [`Spec`],
617/// backed by a dense [`DiGraph`].
618///
619/// This graph has all transformations applied, and is ready for
620/// code generation.
621#[derive(Debug)]
622pub struct CookedGraph<'a> {
623    arena: &'a Arena,
624    pub(super) graph: CookedDiGraph<'a>,
625    info: &'a Info,
626    schemas: FxHashMap<&'a str, NodeIndex<usize>>,
627    ops: &'a [&'a GraphOperation<'a>],
628    /// Additional metadata for each node.
629    pub(super) metadata: CookedGraphMetadata<'a>,
630}
631
632impl<'a> CookedGraph<'a> {
633    fn new(raw: &RawGraph<'a>) -> Self {
634        // Build a dense graph, mapping sparse raw node indices to
635        // dense cooked node indices.
636        let mut graph =
637            CookedDiGraph::with_capacity(raw.graph.node_count(), raw.graph.edge_count());
638        let mut indices =
639            FxHashMap::with_capacity_and_hasher(raw.graph.node_count(), FxBuildHasher);
640        for raw_index in raw.graph.node_indices() {
641            let cooked_index = graph.add_node(raw.graph[raw_index]);
642            indices.insert(raw_index, cooked_index);
643        }
644
645        // Copy edges.
646        //
647        // `raw.graph.edges()` yields edges in reverse order of addition.
648        // The raw graph adds edges in declaration order, so `edges()`
649        // yields them reversed. Re-adding them to the cooked graph in that
650        // reversed order means they're now stored in reverse-declaration order,
651        // letting the cooked graph's accessors yield edges in declaration order
652        // without any extra work.
653        for index in raw.graph.node_indices() {
654            let from = indices[&index];
655            let edges = raw
656                .graph
657                .edges(index)
658                .map(|e| (indices[&e.target()], *e.weight()));
659            for (to, kind) in edges {
660                graph.add_edge(from, to, kind);
661            }
662        }
663
664        // Remap schema type references in operations.
665        let ops: &_ = raw.arena.alloc_slice_exact(raw.ops.iter().map(|&op| {
666            &*raw.arena.alloc(Operation {
667                id: op.id,
668                method: op.method,
669                path: op.path,
670                resource: op.resource,
671                description: op.description,
672                params: raw
673                    .arena
674                    .alloc_slice_exact(op.params.iter().map(|p| match p {
675                        Parameter::Path(info) => Parameter::Path(ParameterInfo {
676                            name: info.name,
677                            ty: indices[&info.ty],
678                            required: info.required,
679                            description: info.description,
680                            style: info.style,
681                        }),
682                        Parameter::Query(info) => Parameter::Query(ParameterInfo {
683                            name: info.name,
684                            ty: indices[&info.ty],
685                            required: info.required,
686                            description: info.description,
687                            style: info.style,
688                        }),
689                    })),
690                request: op.request.as_ref().map(|r| match r {
691                    Request::Json(ty) => Request::Json(indices[ty]),
692                    Request::Multipart => Request::Multipart,
693                }),
694                response: op.response.as_ref().map(|r| match r {
695                    Response::Json(ty) => Response::Json(indices[ty]),
696                }),
697            })
698        }));
699
700        let metadata = MetadataBuilder::new(raw.arena, &graph, ops).build();
701
702        Self {
703            arena: raw.arena,
704            graph,
705            info: raw.spec.info,
706            schemas: raw
707                .schemas
708                .iter()
709                .map(|(&name, index)| (name, indices[index]))
710                .collect(),
711            ops,
712            metadata,
713        }
714    }
715
716    /// Returns this graph's arena.
717    #[inline]
718    pub fn arena(&self) -> &'a Arena {
719        self.arena
720    }
721
722    /// Returns [`Info`] from the [`Document`][crate::parse::Document]
723    /// used to build this graph.
724    #[inline]
725    pub fn info(&self) -> &'a Info {
726        self.info
727    }
728
729    /// Returns an iterator over all the named schemas in this graph.
730    #[inline]
731    pub fn schemas(&self) -> impl Iterator<Item = SchemaTypeView<'_, 'a>> + use<'_, 'a> {
732        self.graph
733            .node_indices()
734            .filter_map(|index| match self.graph[index] {
735                GraphType::Schema(ty) => Some(SchemaTypeView::new(self, index, ty)),
736                _ => None,
737            })
738    }
739
740    /// Looks up and returns a schema by name.
741    #[inline]
742    pub fn schema(&self, name: &str) -> Option<SchemaTypeView<'_, 'a>> {
743        self.schemas
744            .get(name)
745            .and_then(|&index| match self.graph[index] {
746                GraphType::Schema(ty) => Some(SchemaTypeView::new(self, index, ty)),
747                _ => None,
748            })
749    }
750
751    /// Looks up and returns a type view by its identifier.
752    #[inline]
753    pub fn view(&self, id: TypeId) -> TypeView<'_, 'a> {
754        TypeView::new(self, id.0)
755    }
756
757    /// Returns an iterator over all primitive type nodes in this graph.
758    #[inline]
759    pub fn primitives(&self) -> impl Iterator<Item = PrimitiveView<'_, 'a>> + use<'_, 'a> {
760        self.graph
761            .node_indices()
762            .filter_map(|index| match self.graph[index] {
763                GraphType::Schema(GraphSchemaType::Primitive(_, p))
764                | GraphType::Inline(GraphInlineType::Primitive(_, p)) => {
765                    Some(PrimitiveView::new(self, index, p))
766                }
767                _ => None,
768            })
769    }
770
771    /// Returns an iterator over all the operations in this graph.
772    #[inline]
773    pub fn operations(&self) -> impl Iterator<Item = OperationView<'_, 'a>> + use<'_, 'a> {
774        self.ops.iter().map(|&op| OperationView::new(self, op))
775    }
776
777    #[inline]
778    pub(super) fn inherits(
779        &self,
780        node: NodeIndex<usize>,
781    ) -> impl Iterator<Item = OutgoingEdge<()>> {
782        self.graph
783            .edges_directed(node, Direction::Outgoing)
784            .filter(|e| matches!(e.weight(), GraphEdge::Inherits { .. }))
785            .map(|e| OutgoingEdge {
786                meta: (),
787                target: e.target(),
788            })
789    }
790
791    #[inline]
792    pub(super) fn fields(
793        &self,
794        node: NodeIndex<usize>,
795    ) -> impl Iterator<Item = OutgoingEdge<FieldMeta<'a>>> {
796        self.graph
797            .edges_directed(node, Direction::Outgoing)
798            .filter_map(|e| match e.weight() {
799                &GraphEdge::Field { meta, .. } => {
800                    let target = e.target();
801                    Some(OutgoingEdge { meta, target })
802                }
803                _ => None,
804            })
805    }
806
807    #[inline]
808    pub(super) fn variants(
809        &self,
810        node: NodeIndex<usize>,
811    ) -> impl Iterator<Item = OutgoingEdge<VariantMeta<'a>>> {
812        self.graph
813            .edges_directed(node, Direction::Outgoing)
814            .filter_map(|e| match e.weight() {
815                &GraphEdge::Variant(meta) => {
816                    let target = e.target();
817                    Some(OutgoingEdge { meta, target })
818                }
819                _ => None,
820            })
821    }
822}
823
824/// A variant that should be inlined into its tagged union.
825struct InlinableVariant<'a> {
826    /// The tagged union that owns this variant.
827    tagged: Node<GraphTagged<'a>>,
828    /// The original variant struct node.
829    variant: Node<GraphStruct<'a>>,
830}
831
832/// An edge between two types in the type graph.
833///
834/// Edges describe the relationship between their source and target types.
835#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
836pub enum GraphEdge<'a> {
837    /// The source type inherits from the target type.
838    Inherits { shadow: bool },
839    /// The source struct, tagged union, or untagged union
840    /// has the target type as a field.
841    Field { shadow: bool, meta: FieldMeta<'a> },
842    /// The source union has the target type as a variant.
843    Variant(VariantMeta<'a>),
844    /// The source type is an array, map, or optional that contains
845    /// the target type.
846    Contains,
847}
848
849impl GraphEdge<'_> {
850    /// Returns `true` if the target type should be excluded from
851    /// the source type's [inlines], but still considered a dependency.
852    ///
853    /// Shadow edges prevent inlined variant structs from claiming
854    /// their originals' inlines.
855    ///
856    /// [inlines]: crate::ir::views::View::inlines
857    #[inline]
858    pub fn shadow(&self) -> bool {
859        matches!(
860            self,
861            GraphEdge::Field { shadow: true, .. } | GraphEdge::Inherits { shadow: true }
862        )
863    }
864}
865
866/// Metadata describing an edge from a source to a target type.
867#[derive(Clone, Copy, Debug, Eq, PartialEq)]
868pub struct OutgoingEdge<T> {
869    pub meta: T,
870    pub target: NodeIndex<usize>,
871}
872
873#[derive(Clone, Copy)]
874struct Node<Ty> {
875    index: NodeIndex<usize>,
876    ty: Ty,
877}
878
879/// Precomputed metadata for schema types and operations in the graph.
880pub(super) struct CookedGraphMetadata<'a> {
881    /// Transitive closure over the type graph.
882    pub closure: Closure,
883    /// Maps each type to its SCC equivalence class for boxing decisions.
884    /// Two types in the same class form a cycle that requires `Box<T>`.
885    pub box_sccs: Vec<usize>,
886    /// Whether each type can implement `Eq` and `Hash`.
887    pub hashable: FixedBitSet,
888    /// Whether each type can implement `Default`.
889    pub defaultable: FixedBitSet,
890    /// Maps each type to the operations that use it.
891    pub used_by: Vec<Vec<GraphOperation<'a>>>,
892    /// Maps each operation to the types that it uses.
893    pub uses: FxHashMap<GraphOperation<'a>, FixedBitSet>,
894    /// Maps each inline type to its canonical path through the graph.
895    pub paths: FxHashMap<InlineTypeId, InlineTypePath<'a>>,
896    /// Opaque extended data for each type.
897    pub extensions: Vec<AtomicRefCell<ExtensionMap>>,
898}
899
900impl Debug for CookedGraphMetadata<'_> {
901    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
902        f.debug_struct("CookedGraphMetadata")
903            .field("closure", &self.closure)
904            .field("box_sccs", &self.box_sccs)
905            .field("hashable", &self.hashable)
906            .field("defaultable", &self.defaultable)
907            .field("used_by", &self.used_by)
908            .field("uses", &self.uses)
909            .finish_non_exhaustive()
910    }
911}
912
913/// Precomputed bitsets indicating which types can derive
914/// `Eq` / `Hash` and `Default`.
915struct HashDefault {
916    hashable: FixedBitSet,
917    defaultable: FixedBitSet,
918}
919
920/// Precomputed metadata for an operation that references
921/// types in the graph.
922struct Operations<'a> {
923    /// All the types that each operation depends on, directly and transitively.
924    pub uses: FxHashMap<GraphOperation<'a>, FixedBitSet>,
925    /// All the operations that use each type, directly and transitively.
926    pub used_by: Vec<Vec<GraphOperation<'a>>>,
927}
928
929/// A canonical path through the graph to an inline type.
930#[derive(Clone, Copy, Debug)]
931pub(super) struct InlineTypePath<'a> {
932    pub root: InlineTypePathRoot<'a, NodeIndex<usize>, &'a str>,
933    pub edges: &'a [EdgeIndex<usize>],
934}
935
936struct MetadataBuilder<'graph, 'a> {
937    arena: &'a Arena,
938    graph: &'graph CookedDiGraph<'a>,
939    ops: &'graph [&'graph GraphOperation<'a>],
940    /// The full transitive closure of each type's dependencies.
941    closure: Closure,
942}
943
944impl<'graph, 'a> MetadataBuilder<'graph, 'a> {
945    fn new(
946        arena: &'a Arena,
947        graph: &'graph CookedDiGraph<'a>,
948        ops: &'graph [&'graph GraphOperation<'a>],
949    ) -> Self {
950        Self {
951            arena,
952            graph,
953            ops,
954            closure: Closure::new(graph),
955        }
956    }
957
958    fn build(self) -> CookedGraphMetadata<'a> {
959        let operations = self.operations();
960        let HashDefault {
961            hashable,
962            defaultable,
963        } = self.hash_default();
964        let box_sccs = self.box_sccs();
965        let paths = self.paths();
966        CookedGraphMetadata {
967            closure: self.closure,
968            box_sccs,
969            hashable,
970            defaultable,
971            used_by: operations.used_by,
972            uses: operations.uses,
973            paths,
974            // `AtomicRefCell` doesn't implement `Clone`,
975            // so we use this idiom instead of `vec!`.
976            extensions: std::iter::repeat_with(AtomicRefCell::default)
977                .take(self.graph.node_count())
978                .collect(),
979        }
980    }
981
982    fn operations(&self) -> Operations<'a> {
983        let mut operations = Operations {
984            uses: FxHashMap::default(),
985            used_by: vec![vec![]; self.graph.node_count()],
986        };
987
988        for &&op in self.ops {
989            // Forward propagation: start from the direct types, then
990            // expand to the full transitive dependency set.
991            let mut dependencies = FixedBitSet::with_capacity(self.graph.node_count());
992            for &node in op.types() {
993                dependencies.extend(self.closure.dependencies_of(node).map(|n| n.index()));
994            }
995            operations.uses.entry(op).insert_entry(dependencies);
996        }
997
998        // Backward propagation: mark types as used by their operations.
999        for (op, deps) in &operations.uses {
1000            for node in deps.ones() {
1001                operations.used_by[node].push(*op);
1002            }
1003        }
1004
1005        operations
1006    }
1007
1008    /// Precomputes canonical paths for all inline type nodes.
1009    fn paths(&self) -> FxHashMap<InlineTypeId, InlineTypePath<'a>> {
1010        #[derive(Clone)]
1011        struct PartialPath<'a> {
1012            root: InlineTypePathRoot<'a, NodeIndex<usize>, &'a str>,
1013            edges: Vec<EdgeIndex<usize>>,
1014        }
1015
1016        let filtered = EdgeFiltered::from_fn(self.graph, |e| {
1017            !e.weight().shadow() && matches!(self.graph[e.target()], GraphType::Inline(_))
1018        });
1019
1020        let mut by_node = FxHashMap::default();
1021        let mut bfs = EdgeBfs::new(&filtered);
1022
1023        // Expand paths for each named schema root.
1024        for index in self.graph.node_indices() {
1025            if matches!(self.graph[index], GraphType::Schema(_)) && bfs.discover(index) {
1026                by_node.insert(
1027                    index,
1028                    PartialPath {
1029                        root: InlineTypePathRoot::Schema(index),
1030                        edges: vec![],
1031                    },
1032                );
1033            }
1034            while let Some(edge) = bfs.next() {
1035                let parent = &by_node[&edge.source()];
1036                let mut child = parent.clone();
1037                child.edges.push(edge.id());
1038                by_node.insert(edge.target(), child);
1039            }
1040        }
1041
1042        // Expand paths for each operation: operations aren't part of the graph;
1043        // their parameter, request, and response types are the roots.
1044        for op in self.ops {
1045            for param in op.params {
1046                let (usage, info) = match param {
1047                    Parameter::Path(info) => (OperationUsage::Path(info.name), info),
1048                    Parameter::Query(info) => (OperationUsage::Query(info.name), info),
1049                };
1050                if matches!(self.graph[info.ty], GraphType::Inline(_)) && bfs.discover(info.ty) {
1051                    by_node.insert(
1052                        info.ty,
1053                        PartialPath {
1054                            root: InlineTypePathRoot::Operation {
1055                                id: op.id,
1056                                resource: op.resource,
1057                                usage,
1058                            },
1059                            edges: vec![],
1060                        },
1061                    );
1062                }
1063            }
1064            if let Some(Request::Json(index)) = op.request
1065                && matches!(self.graph[index], GraphType::Inline(_))
1066                && bfs.discover(index)
1067            {
1068                by_node.insert(
1069                    index,
1070                    PartialPath {
1071                        root: InlineTypePathRoot::Operation {
1072                            id: op.id,
1073                            resource: op.resource,
1074                            usage: OperationUsage::Request,
1075                        },
1076                        edges: vec![],
1077                    },
1078                );
1079            }
1080            if let Some(Response::Json(index)) = op.response
1081                && matches!(self.graph[index], GraphType::Inline(_))
1082                && bfs.discover(index)
1083            {
1084                by_node.insert(
1085                    index,
1086                    PartialPath {
1087                        root: InlineTypePathRoot::Operation {
1088                            id: op.id,
1089                            resource: op.resource,
1090                            usage: OperationUsage::Response,
1091                        },
1092                        edges: vec![],
1093                    },
1094                );
1095            }
1096            while let Some(edge) = bfs.next() {
1097                let parent = &by_node[&edge.source()];
1098                let mut child = parent.clone();
1099                child.edges.push(edge.id());
1100                by_node.insert(edge.target(), child);
1101            }
1102        }
1103
1104        by_node
1105            .into_iter()
1106            .filter_map(
1107                |(index, PartialPath { root, edges })| match self.graph[index] {
1108                    GraphType::Inline(ty) => Some((
1109                        ty.id(),
1110                        InlineTypePath {
1111                            root,
1112                            edges: self.arena.alloc_slice(edges),
1113                        },
1114                    )),
1115                    _ => None,
1116                },
1117            )
1118            .collect()
1119    }
1120
1121    fn box_sccs(&self) -> Vec<usize> {
1122        let box_edges = EdgeFiltered::from_fn(self.graph, |e| match e.weight() {
1123            // Inheritance edges don't contribute to cycles;
1124            // a type can't inherit from itself.
1125            GraphEdge::Inherits { .. } => false,
1126            GraphEdge::Contains => match self.graph[e.source()] {
1127                GraphType::Schema(GraphSchemaType::Container(_, c))
1128                | GraphType::Inline(GraphInlineType::Container(_, c)) => {
1129                    // Array and map containers are heap-allocated,
1130                    // cycles through these edges don't need `Box`.
1131                    !matches!(c, GraphContainer::Array { .. } | GraphContainer::Map { .. })
1132                }
1133                _ => true,
1134            },
1135            _ => true,
1136        });
1137        let mut scc = TarjanScc::new();
1138        scc.run(&box_edges, |_| ());
1139        self.graph
1140            .node_indices()
1141            .map(|node| scc.node_component_index(&box_edges, node))
1142            .collect()
1143    }
1144
1145    fn hash_default(&self) -> HashDefault {
1146        // Mark all leaf types that can't derive `Eq` / `Hash` or `Default`.
1147        let n = self.graph.node_count();
1148        let mut unhashable = FixedBitSet::with_capacity(n);
1149        let mut undefaultable = FixedBitSet::with_capacity(n);
1150        for node in self.graph.node_indices() {
1151            use {GraphType::*, PrimitiveType::*};
1152            match &self.graph[node] {
1153                Schema(GraphSchemaType::Primitive(_, F32 | F64))
1154                | Inline(GraphInlineType::Primitive(_, F32 | F64)) => {
1155                    unhashable.insert(node.index());
1156                }
1157                Schema(
1158                    GraphSchemaType::Primitive(_, Url)
1159                    | GraphSchemaType::Tagged(_, _)
1160                    | GraphSchemaType::Untagged(_, _),
1161                )
1162                | Inline(
1163                    GraphInlineType::Primitive(_, Url)
1164                    | GraphInlineType::Tagged(_, _)
1165                    | GraphInlineType::Untagged(_, _),
1166                ) => {
1167                    undefaultable.insert(node.index());
1168                }
1169                _ => (),
1170            }
1171        }
1172
1173        // Compute the transitive closure over the inheritance subgraph.
1174        let inherits = Closure::new(&EdgeFiltered::from_fn(self.graph, |e| {
1175            matches!(e.weight(), GraphEdge::Inherits { .. })
1176        }));
1177
1178        // Propagate unhashability backward, from leaves to roots.
1179        //
1180        // This is conservative: if a descendant overrides an inherited
1181        // unhashable or undefaultable field with a different hashable or
1182        // defaultable type, that descendant is still marked.
1183        let mut queue: VecDeque<_> = unhashable.ones().map(NodeIndex::new).collect();
1184        while let Some(node) = queue.pop_front() {
1185            for edge in self.graph.edges_directed(node, Direction::Incoming) {
1186                let source = edge.source();
1187                match edge.weight() {
1188                    GraphEdge::Contains | GraphEdge::Variant(_) => {
1189                        if !unhashable.put(source.index()) {
1190                            queue.push_back(source);
1191                        }
1192                    }
1193                    GraphEdge::Field { .. } => {
1194                        if !unhashable.put(source.index()) {
1195                            queue.push_back(source);
1196                        }
1197                        // Every type that inherits from `source` also
1198                        // inherits this unhashable field, so mark all
1199                        // descendants of `source` as unhashable.
1200                        for desc in inherits.dependents_of(source).filter(|&d| d != source) {
1201                            if !unhashable.put(desc.index()) {
1202                                queue.push_back(desc);
1203                            }
1204                        }
1205                    }
1206                    // Don't follow inheritance edges: a parent's intrinsic
1207                    // unhashability (e.g., being a tagged union) doesn't
1208                    // make its children unhashable, because children only
1209                    // inherit the parent's fields, not its shape.
1210                    GraphEdge::Inherits { .. } => {}
1211                }
1212            }
1213        }
1214
1215        // Propagate undefaultability backward.
1216        let mut queue: VecDeque<_> = undefaultable.ones().map(NodeIndex::new).collect();
1217        while let Some(node) = queue.pop_front() {
1218            for edge in self.graph.edges_directed(node, Direction::Incoming) {
1219                if !matches!(
1220                    edge.weight(),
1221                    GraphEdge::Field { meta, .. } if meta.required
1222                ) {
1223                    // Optional fields become `AbsentOr<T>`,
1224                    // which is always `Default`.
1225                    continue;
1226                }
1227                let source = edge.source();
1228                if !undefaultable.put(source.index()) {
1229                    queue.push_back(source);
1230                }
1231                // Every type that inherits from `source` also
1232                // inherits this undefaultable field, so mark all
1233                // descendants of `source` as undefaultable.
1234                for desc in inherits.dependents_of(source).filter(|&d| d != source) {
1235                    if !undefaultable.put(desc.index()) {
1236                        queue.push_back(desc);
1237                    }
1238                }
1239            }
1240        }
1241
1242        HashDefault {
1243            hashable: invert(unhashable),
1244            defaultable: invert(undefaultable),
1245        }
1246    }
1247}
1248
1249/// Inverts every bit in the bitset.
1250fn invert(mut bits: FixedBitSet) -> FixedBitSet {
1251    bits.toggle_range(..);
1252    bits
1253}
1254
1255/// Visits all the types and references contained within a [`SpecType`].
1256#[derive(Debug)]
1257struct SpecTypeVisitor<'a> {
1258    stack: Vec<(Option<(&'a SpecType<'a>, GraphEdge<'a>)>, &'a SpecType<'a>)>,
1259}
1260
1261impl<'a> SpecTypeVisitor<'a> {
1262    /// Creates a visitor with `roots` on the stack of types to visit.
1263    #[inline]
1264    fn new(roots: impl Iterator<Item = &'a SpecType<'a>>) -> Self {
1265        let mut stack = roots.map(|root| (None, root)).collect_vec();
1266        stack.reverse();
1267        Self { stack }
1268    }
1269}
1270
1271impl<'a> Iterator for SpecTypeVisitor<'a> {
1272    type Item = (Option<(&'a SpecType<'a>, GraphEdge<'a>)>, &'a SpecType<'a>);
1273
1274    fn next(&mut self) -> Option<Self::Item> {
1275        let (parent, top) = self.stack.pop()?;
1276        if matches!(
1277            parent,
1278            Some((
1279                _,
1280                GraphEdge::Variant(VariantMeta::Untagged(UntaggedVariantMeta::Null))
1281            ))
1282        ) {
1283            // Unit variants form self-edges; skip them
1284            // to avoid an infinite loop.
1285            return Some((parent, top));
1286        }
1287        match top {
1288            SpecType::Schema(SpecSchemaType::Struct(_, ty))
1289            | SpecType::Inline(SpecInlineType::Struct(_, ty)) => {
1290                self.stack.extend(
1291                    itertools::chain!(
1292                        ty.fields.iter().map(|field| (
1293                            GraphEdge::Field {
1294                                shadow: false,
1295                                meta: FieldMeta {
1296                                    name: field.name,
1297                                    required: field.required,
1298                                    description: field.description,
1299                                    flattened: field.flattened,
1300                                },
1301                            },
1302                            field.ty
1303                        )),
1304                        ty.parents
1305                            .iter()
1306                            .map(|parent| (GraphEdge::Inherits { shadow: false }, *parent)),
1307                    )
1308                    .map(|(edge, ty)| (Some((top, edge)), ty))
1309                    .rev(),
1310                );
1311            }
1312            SpecType::Schema(SpecSchemaType::Untagged(_, ty))
1313            | SpecType::Inline(SpecInlineType::Untagged(_, ty)) => {
1314                self.stack.extend(
1315                    itertools::chain!(
1316                        ty.fields.iter().map(|field| (
1317                            GraphEdge::Field {
1318                                shadow: false,
1319                                meta: FieldMeta {
1320                                    name: field.name,
1321                                    required: field.required,
1322                                    description: field.description,
1323                                    flattened: field.flattened,
1324                                },
1325                            },
1326                            field.ty
1327                        )),
1328                        ty.variants.iter().map(|variant| match variant {
1329                            &SpecUntaggedVariant::Some(hint, ty) => {
1330                                let meta = UntaggedVariantMeta::Type { hint };
1331                                (GraphEdge::Variant(meta.into()), ty)
1332                            }
1333                            // `null` variants have no target type;
1334                            // we represent these variants as self-edges.
1335                            SpecUntaggedVariant::Null => {
1336                                (GraphEdge::Variant(UntaggedVariantMeta::Null.into()), top)
1337                            }
1338                        }),
1339                        ty.parents
1340                            .iter()
1341                            .map(|parent| (GraphEdge::Inherits { shadow: false }, *parent)),
1342                    )
1343                    .map(|(edge, ty)| (Some((top, edge)), ty))
1344                    .rev(),
1345                );
1346            }
1347            SpecType::Schema(SpecSchemaType::Tagged(_, ty))
1348            | SpecType::Inline(SpecInlineType::Tagged(_, ty)) => {
1349                self.stack.extend(
1350                    itertools::chain!(
1351                        ty.fields.iter().map(|field| (
1352                            GraphEdge::Field {
1353                                shadow: false,
1354                                meta: FieldMeta {
1355                                    name: field.name,
1356                                    required: field.required,
1357                                    description: field.description,
1358                                    flattened: field.flattened,
1359                                },
1360                            },
1361                            field.ty
1362                        )),
1363                        ty.variants.iter().map(|variant| (
1364                            GraphEdge::Variant(
1365                                TaggedVariantMeta {
1366                                    name: variant.name,
1367                                    aliases: variant.aliases,
1368                                }
1369                                .into()
1370                            ),
1371                            variant.ty
1372                        )),
1373                        ty.parents
1374                            .iter()
1375                            .map(|parent| (GraphEdge::Inherits { shadow: false }, *parent)),
1376                    )
1377                    .map(|(edge, ty)| (Some((top, edge)), ty))
1378                    .rev(),
1379                );
1380            }
1381            SpecType::Schema(SpecSchemaType::Container(_, container))
1382            | SpecType::Inline(SpecInlineType::Container(_, container)) => {
1383                self.stack
1384                    .push((Some((top, GraphEdge::Contains)), container.inner().ty));
1385            }
1386            SpecType::Schema(
1387                SpecSchemaType::Enum(..) | SpecSchemaType::Primitive(..) | SpecSchemaType::Any(_),
1388            )
1389            | SpecType::Inline(
1390                SpecInlineType::Enum(..) | SpecInlineType::Primitive(..) | SpecInlineType::Any(_),
1391            ) => (),
1392            SpecType::Ref(_) => (),
1393        }
1394        Some((parent, top))
1395    }
1396}
1397
1398/// A map that can store one value for each type.
1399pub(super) type ExtensionMap = FxHashMap<StdTypeId, Box<dyn Extension>>;
1400
1401pub trait Extension: Any + Send + Sync {
1402    fn into_inner(self: Box<Self>) -> Box<dyn Any>;
1403}
1404
1405impl dyn Extension {
1406    #[inline]
1407    pub fn downcast_ref<T: 'static>(&self) -> Option<&T> {
1408        (self as &dyn Any).downcast_ref::<T>()
1409    }
1410}
1411
1412impl<T: Send + Sync + 'static> Extension for T {
1413    #[inline]
1414    fn into_inner(self: Box<Self>) -> Box<dyn Any> {
1415        self
1416    }
1417}
1418
1419/// Strongly connected components (SCCs) in topological order.
1420///
1421/// [`TopoSccs`] uses Tarjan's single-pass algorithm to find all SCCs,
1422/// and provides topological ordering, efficient membership testing, and
1423/// condensation for computing the transitive closure. These are
1424/// building blocks for cycle detection and dependency propagation.
1425struct TopoSccs<G> {
1426    graph: G,
1427    tarjan: TarjanScc<NodeIndex<usize>>,
1428    sccs: Vec<Vec<usize>>,
1429}
1430
1431impl<G> TopoSccs<G>
1432where
1433    G: Closable<NodeIndex<usize>> + Copy,
1434{
1435    fn new(graph: G) -> Self {
1436        let mut sccs = Vec::new();
1437        let mut tarjan = TarjanScc::new();
1438        tarjan.run(graph, |scc_nodes| {
1439            sccs.push(scc_nodes.iter().map(|node| node.index()).collect());
1440        });
1441        // Tarjan's algorithm returns SCCs in reverse topological order;
1442        // reverse them to get the topological order.
1443        sccs.reverse();
1444        Self {
1445            graph,
1446            tarjan,
1447            sccs,
1448        }
1449    }
1450
1451    #[inline]
1452    fn scc_count(&self) -> usize {
1453        self.sccs.len()
1454    }
1455
1456    /// Returns the topological index of the SCC that contains the given node.
1457    #[inline]
1458    fn topo_index(&self, node: NodeIndex<usize>) -> usize {
1459        // Tarjan's algorithm returns indices in reverse topological order;
1460        // inverting the component index gets us the topological index.
1461        self.sccs.len() - 1 - self.tarjan.node_component_index(self.graph, node)
1462    }
1463
1464    /// Builds a condensed DAG of SCCs.
1465    ///
1466    /// The condensed graph is represented as an adjacency list, where both
1467    /// the node indices and the neighbors of each node are stored in
1468    /// topological order. This specific ordering is required by
1469    /// [`tred::dag_transitive_reduction_closure`].
1470    fn condensation(&self) -> UnweightedList<usize> {
1471        let mut dag = UnweightedList::with_capacity(self.scc_count());
1472        for to in 0..self.scc_count() {
1473            dag.add_node();
1474            for neighbor in self.sccs[to].iter().flat_map(|&index| {
1475                self.graph
1476                    .neighbors_directed(NodeIndex::new(index), Direction::Incoming)
1477            }) {
1478                let from = self.topo_index(neighbor);
1479                if from != to {
1480                    dag.update_edge(from, to, ());
1481                }
1482            }
1483        }
1484        dag
1485    }
1486}
1487
1488/// The transitive closure of a graph.
1489#[derive(Debug)]
1490pub(super) struct Closure {
1491    /// Maps each node index to its SCC's topological index.
1492    scc_indices: Vec<usize>,
1493    /// Members of each SCC, indexed by topological SCC index.
1494    scc_members: Vec<Vec<usize>>,
1495    /// Maps each SCC to a list of all the SCCs that it transitively depends on,
1496    /// excluding itself.
1497    scc_deps: Vec<Vec<usize>>,
1498    /// Maps each SCC to a list of all the SCCs that transitively depend on it,
1499    /// excluding itself.
1500    scc_rdeps: Vec<Vec<usize>>,
1501}
1502
1503impl Closure {
1504    /// Computes the transitive closure of a graph.
1505    fn new<G>(graph: G) -> Self
1506    where
1507        G: Closable<NodeIndex<usize>> + Copy,
1508    {
1509        let sccs = TopoSccs::new(graph);
1510        let condensation = sccs.condensation();
1511        let (_, closure) = tred::dag_transitive_reduction_closure(&condensation);
1512
1513        // Build the forward and reverse adjacency lists
1514        // from the transitive closure graph.
1515        let scc_deps = (0..sccs.scc_count())
1516            .map(|scc| closure.neighbors(scc).collect_vec())
1517            .collect_vec();
1518        let mut scc_rdeps = vec![vec![]; sccs.scc_count()];
1519        for (scc, deps) in scc_deps.iter().enumerate() {
1520            for &dep in deps {
1521                scc_rdeps[dep].push(scc);
1522            }
1523        }
1524
1525        let mut scc_indices = vec![0; graph.node_count()];
1526        for node in graph.node_identifiers() {
1527            scc_indices[node.index()] = sccs.topo_index(node);
1528        }
1529
1530        Closure {
1531            scc_indices,
1532            scc_members: sccs.sccs.iter().cloned().collect_vec(),
1533            scc_deps,
1534            scc_rdeps,
1535        }
1536    }
1537
1538    /// Returns the topological SCC index for the given node.
1539    #[inline]
1540    pub fn scc_index_of(&self, node: NodeIndex<usize>) -> usize {
1541        self.scc_indices[node.index()]
1542    }
1543
1544    /// Iterates over all nodes that `node` transitively depends on,
1545    /// including `node` and all members of its SCC.
1546    pub fn dependencies_of(
1547        &self,
1548        node: NodeIndex<usize>,
1549    ) -> impl Iterator<Item = NodeIndex<usize>> {
1550        let scc = self.scc_index_of(node);
1551        std::iter::once(scc)
1552            .chain(self.scc_deps[scc].iter().copied())
1553            .flat_map(|s| self.scc_members[s].iter().copied()) // Expand SCCs to nodes.
1554            .map(NodeIndex::new)
1555    }
1556
1557    /// Iterates over all nodes that transitively depend on `node`,
1558    /// including `node` and all members of its SCC.
1559    pub fn dependents_of(&self, node: NodeIndex<usize>) -> impl Iterator<Item = NodeIndex<usize>> {
1560        let scc = self.scc_index_of(node);
1561        std::iter::once(scc)
1562            .chain(self.scc_rdeps[scc].iter().copied())
1563            .flat_map(|s| self.scc_members[s].iter().copied())
1564            .map(NodeIndex::new)
1565    }
1566
1567    /// Returns whether `node` transitively depends on `other`,
1568    /// or `false` when `node == other`.
1569    #[inline]
1570    pub fn depends_on(&self, node: NodeIndex<usize>, other: NodeIndex<usize>) -> bool {
1571        if node == other {
1572            return false;
1573        }
1574        let scc = self.scc_index_of(node);
1575        let other_scc = self.scc_index_of(other);
1576        scc == other_scc || self.scc_deps[scc].contains(&other_scc)
1577    }
1578}
1579
1580/// Trait requirements for computing a transitive closure.
1581trait Closable<N>:
1582    NodeCount
1583    + IntoNodeIdentifiers<NodeId = N>
1584    + IntoNeighbors<NodeId = N>
1585    + IntoNeighborsDirected<NodeId = N>
1586    + NodeIndexable<NodeId = N>
1587{
1588}
1589
1590impl<N, G> Closable<N> for G where
1591    G: NodeCount
1592        + IntoNodeIdentifiers<NodeId = N>
1593        + IntoNeighbors<NodeId = N>
1594        + IntoNeighborsDirected<NodeId = N>
1595        + NodeIndexable<NodeId = N>
1596{
1597}
1598
1599/// A breadth-first search that yields discovery edges instead of nodes.
1600///
1601/// A standard [`Bfs`] yields each node as it's dequeued. This variant yields
1602/// the edge that first discovered each target, which is the primitive we need
1603/// to build inline type paths.
1604///
1605/// [`Bfs`]: petgraph::visit::Bfs
1606struct EdgeBfs<G, N, VM> {
1607    graph: G,
1608    queue: VecDeque<N>,
1609    discovered: VM,
1610}
1611
1612impl<G, N, VM> EdgeBfs<G, N, VM>
1613where
1614    N: Copy,
1615    VM: VisitMap<N>,
1616{
1617    /// Creates an empty traversal for the given graph.
1618    fn new(graph: G) -> Self
1619    where
1620        G: GraphRef + Visitable<NodeId = N, Map = VM>,
1621    {
1622        let discovered = graph.visit_map();
1623        Self {
1624            graph,
1625            queue: VecDeque::new(),
1626            discovered,
1627        }
1628    }
1629
1630    /// Marks `node` as discovered and enqueues it for expansion.
1631    /// Returns `true` if the node was newly discovered.
1632    fn discover(&mut self, node: N) -> bool {
1633        if self.discovered.visit(node) {
1634            self.queue.push_back(node);
1635            true
1636        } else {
1637            false
1638        }
1639    }
1640
1641    /// Returns the next edge whose target has not been discovered.
1642    ///
1643    /// The traversal keeps the source node at the front of the queue until
1644    /// the targets of all its outgoing edges have been discovered. Each
1645    /// returned edge marks its target as discovered and enqueues it for
1646    /// later expansion.
1647    fn next(&mut self) -> Option<G::EdgeRef>
1648    where
1649        G: IntoEdges<NodeId = N>,
1650    {
1651        loop {
1652            let &source = self.queue.front()?;
1653            for edge in self.graph.edges(source) {
1654                let target = edge.target();
1655                if self.discovered.visit(target) {
1656                    self.queue.push_back(target);
1657                    return Some(edge);
1658                }
1659            }
1660            self.queue.pop_front();
1661        }
1662    }
1663}
1664
1665#[cfg(test)]
1666mod tests {
1667    use super::*;
1668
1669    use crate::tests::assert_matches;
1670
1671    /// Creates a simple graph: `A -> B -> C`.
1672    fn linear_graph() -> DiGraph<(), (), usize> {
1673        let mut g = DiGraph::default();
1674        let a = g.add_node(());
1675        let b = g.add_node(());
1676        let c = g.add_node(());
1677        g.extend_with_edges([(a, b), (b, c)]);
1678        g
1679    }
1680
1681    /// Creates a cyclic graph: `A -> B -> C -> A`, with `D -> A`.
1682    fn cyclic_graph() -> DiGraph<(), (), usize> {
1683        let mut g = DiGraph::default();
1684        let a = g.add_node(());
1685        let b = g.add_node(());
1686        let c = g.add_node(());
1687        let d = g.add_node(());
1688        g.extend_with_edges([(a, b), (b, c), (c, a), (d, a)]);
1689        g
1690    }
1691
1692    // MARK: SCC detection
1693
1694    #[test]
1695    fn test_linear_graph_has_singleton_sccs() {
1696        let g = linear_graph();
1697        let sccs = TopoSccs::new(&g);
1698        let sizes = sccs.sccs.iter().map(|scc| scc.len()).collect_vec();
1699        assert_matches!(&*sizes, [1, 1, 1]);
1700    }
1701
1702    #[test]
1703    fn test_cyclic_graph_has_one_multi_node_scc() {
1704        let g = cyclic_graph();
1705        let sccs = TopoSccs::new(&g);
1706
1707        // A-B-C form one SCC; D is its own SCC. Since D has an edge to
1708        // the cycle, D must precede the cycle in topological order.
1709        let sizes = sccs.sccs.iter().map(|scc| scc.len()).collect_vec();
1710        assert_matches!(&*sizes, [1, 3]);
1711    }
1712
1713    // MARK: Topological ordering
1714
1715    #[test]
1716    fn test_sccs_are_in_topological_order() {
1717        let g = cyclic_graph();
1718        let sccs = TopoSccs::new(&g);
1719
1720        let d_topo = sccs.topo_index(3.into());
1721        let a_topo = sccs.topo_index(0.into());
1722        assert!(
1723            d_topo < a_topo,
1724            "D should precede A-B-C in topological order"
1725        );
1726    }
1727
1728    #[test]
1729    fn test_topo_index_consistent_within_scc() {
1730        let g = cyclic_graph();
1731        let sccs = TopoSccs::new(&g);
1732
1733        // A, B, C are in the same SCC, so they should have
1734        // the same topological index.
1735        let a_topo = sccs.topo_index(0.into());
1736        let b_topo = sccs.topo_index(1.into());
1737        let c_topo = sccs.topo_index(2.into());
1738
1739        assert_eq!(a_topo, b_topo);
1740        assert_eq!(b_topo, c_topo);
1741    }
1742
1743    // MARK: Condensation
1744
1745    #[test]
1746    fn test_condensation_has_correct_node_count() {
1747        let g = cyclic_graph();
1748        let sccs = TopoSccs::new(&g);
1749        let dag = sccs.condensation();
1750
1751        assert_eq!(dag.node_count(), 2);
1752    }
1753
1754    #[test]
1755    fn test_condensation_has_correct_edges() {
1756        let g = cyclic_graph();
1757        let sccs = TopoSccs::new(&g);
1758        let dag = sccs.condensation();
1759
1760        // D should have an edge to the A-B-C SCC, and
1761        // A-B-C shouldn't create a self-loop.
1762        let d_topo = sccs.topo_index(3.into());
1763        let abc_topo = sccs.topo_index(0.into());
1764
1765        let d_neighbors = dag.neighbors(d_topo).collect_vec();
1766        assert_eq!(&*d_neighbors, [abc_topo]);
1767
1768        let abc_neighbors = dag.neighbors(abc_topo).collect_vec();
1769        assert!(abc_neighbors.is_empty());
1770    }
1771
1772    #[test]
1773    fn test_condensation_neighbors_in_topological_order() {
1774        // Matches Petgraph's `dag_to_toposorted_adjacency_list` example:
1775        // edges added as `(top, second), (top, first)`, but neighbors should be
1776        // `[first, second]` in topological order.
1777        let mut g = DiGraph::<(), (), usize>::default();
1778        let second = g.add_node(());
1779        let top = g.add_node(());
1780        let first = g.add_node(());
1781        g.extend_with_edges([(top, second), (top, first), (first, second)]);
1782
1783        let sccs = TopoSccs::new(&g);
1784        let dag = sccs.condensation();
1785
1786        let top_topo = sccs.topo_index(top);
1787        assert_eq!(top_topo, 0);
1788
1789        let first_topo = sccs.topo_index(first);
1790        assert_eq!(first_topo, 1);
1791
1792        let second_topo = sccs.topo_index(second);
1793        assert_eq!(second_topo, 2);
1794
1795        let neighbors = dag.neighbors(top_topo).collect_vec();
1796        assert_eq!(&*neighbors, [first_topo, second_topo]);
1797    }
1798}