Skip to main content

oapi_codegen/lower/
recurse.rs

1//! Giving a recursive type a size by boxing the field that closes the cycle.
2//!
3//! A schema may refer to itself, directly or through other schemas. Lowered
4//! as written, `Node { child: Node }` is a type that holds itself, and rustc
5//! rejects it with `E0072`. The fix rustc itself suggests is a `Box`, so this
6//! pass inserts one.
7//!
8//! # What counts as holding a type
9//!
10//! A field holds its type when the size of that type counts towards the size of
11//! the struct. `Vec<T>`, `HashMap<String, T>` and `Box<T>` keep what they hold
12//! on the heap, so they hold nothing and already break a cycle. `Option<T>`
13//! stores its `T` inline, so `Option<Node>` inside `Node` is just as infinite as
14//! `Node`. That last point is easy to get wrong: making a recursive property
15//! optional does not fix anything.
16//!
17//! # Which edge gets the box
18//!
19//! Every edge on a cycle, and not one chosen edge. Boxing a single edge is
20//! enough for rustc, but the choice would fall out of item order, so `A` and
21//! `B` that refer to each other would get one box on whichever the walk met
22//! first. Boxing both states the same fact about both types.
23//!
24//! An alias holds its target and offers nothing to box, so a cycle made only of
25//! aliases is [`Error::RecursiveAlias`] instead.
26
27use std::collections::BTreeMap;
28use std::collections::BTreeSet;
29
30use crate::error::Error;
31use crate::error::Result;
32use crate::ir::EnumKind;
33use crate::ir::Item;
34use crate::ir::Module;
35use crate::ir::RustType;
36use crate::naming::Case;
37use crate::naming::to_ident;
38
39/// Box every field and variant of `module` that closes a type cycle.
40///
41/// Fails when a cycle runs only through aliases, because a `Box` there still
42/// expands forever.
43pub fn box_recursive_types(module: &mut Module) -> Result<()> {
44    let graph = Graph::of(module);
45    graph.check_alias_cycles()?;
46
47    for item in &mut module.items {
48        let owner = canonical(item.name());
49        match item {
50            Item::Struct(strukt) => {
51                for field in &mut strukt.fields {
52                    box_held(&mut field.ty, &owner, &graph);
53                }
54            }
55            Item::Enum(enumeration) => {
56                if let EnumKind::Union(variants) = &mut enumeration.kind {
57                    for variant in variants {
58                        box_held(&mut variant.ty, &owner, &graph);
59                    }
60                }
61            }
62            // An alias offers nothing to box. `check_alias_cycles` has already
63            // rejected the only cycle that could reach one.
64            Item::Alias(_) => {}
65        }
66    }
67    return Ok(());
68}
69
70/// Which items hold which, plus the strongly connected components of that
71/// relation.
72///
73/// An edge `a -> b` means an item `a` holds an item `b` in a position that
74/// counts towards its size. Two items sit in one component exactly when each
75/// holds the other, so an edge needs a box exactly when its two ends share a
76/// component. Reading the components one time keeps the pass linear in the
77/// number of edges; asking "does `b` reach `a`" per edge walks the whole graph
78/// per edge instead.
79struct Graph {
80    /// Item names, in module order, indexed by node.
81    names: Vec<String>,
82    /// Node per item name.
83    nodes: BTreeMap<String, usize>,
84    /// Held items per node.
85    edges: Vec<Vec<usize>>,
86    /// Whether each node is an alias, which offers nothing to box.
87    is_alias: Vec<bool>,
88    /// Component per node.
89    component: Vec<usize>,
90}
91
92impl Graph {
93    /// Build the holding graph of `module` and its components.
94    fn of(module: &Module) -> Self {
95        let names: Vec<String> = module.items.iter().map(|item| return canonical(item.name())).collect();
96        let nodes: BTreeMap<String, usize> = names
97            .iter()
98            .enumerate()
99            .map(|(node, name)| return (name.clone(), node))
100            .collect();
101
102        let mut edges = Vec::with_capacity(module.items.len());
103        let mut is_alias = Vec::with_capacity(module.items.len());
104        for item in &module.items {
105            let mut targets = BTreeSet::new();
106            match item {
107                Item::Struct(strukt) => {
108                    for field in &strukt.fields {
109                        collect_held(&field.ty, &mut targets);
110                    }
111                }
112                Item::Enum(enumeration) => {
113                    if let EnumKind::Union(variants) = &enumeration.kind {
114                        for variant in variants {
115                            collect_held(&variant.ty, &mut targets);
116                        }
117                    }
118                }
119                Item::Alias(alias) => collect_held(&alias.ty, &mut targets),
120            }
121            is_alias.push(matches!(item, Item::Alias(_)));
122            // A name the module does not declare cannot close a cycle inside it,
123            // so it gets no node and no edge.
124            edges.push(
125                targets
126                    .iter()
127                    .filter_map(|target| return nodes.get(target).copied())
128                    .collect(),
129            );
130        }
131
132        let component = components(&edges);
133        return Self {
134            names,
135            nodes,
136            edges,
137            is_alias,
138            component,
139        };
140    }
141
142    /// Whether an edge between `from` and `to` closes a cycle.
143    ///
144    /// The caller only asks about a pair it holds an edge for, so one shared
145    /// component is the same statement as "each reaches the other".
146    fn on_a_cycle(&self, from: &str, to: &str) -> bool {
147        let (Some(from), Some(to)) = (self.nodes.get(from), self.nodes.get(to)) else {
148            return false;
149        };
150        let (Some(from), Some(to)) = (self.component.get(*from), self.component.get(*to)) else {
151            return false;
152        };
153        return from == to;
154    }
155
156    /// Reject a cycle whose every member is an alias.
157    ///
158    /// Such a cycle has no field and no variant to box, and `type A = Box<B>`
159    /// with `type B = Box<A>` still expands forever.
160    fn check_alias_cycles(&self) -> Result<()> {
161        let mut members: BTreeMap<usize, Vec<usize>> = BTreeMap::new();
162        for (node, component) in self.component.iter().enumerate() {
163            members.entry(*component).or_default().push(node);
164        }
165        for group in members.values() {
166            if !self.is_cyclic(group) || !group.iter().all(|node| return self.is_alias(*node)) {
167                continue;
168            }
169            return Err(Error::RecursiveAlias {
170                cycle: self.cycle_through(group),
171                hint: "Give one of these schemas `type: object` with properties, so the generator emits a struct it \
172                       can box, or break the chain of `$ref`s."
173                    .to_owned(),
174            });
175        }
176        return Ok(());
177    }
178
179    /// Whether the items of one component refer to each other in a cycle.
180    ///
181    /// Every node is its own component, so a lone node is only cyclic when it
182    /// holds itself.
183    fn is_cyclic(&self, group: &[usize]) -> bool {
184        if group.len() > 1 {
185            return true;
186        }
187        return group
188            .first()
189            .is_some_and(|node| return self.edges_of(*node).contains(node));
190    }
191
192    /// The items `node` holds. An unknown node holds nothing.
193    fn edges_of(&self, node: usize) -> &[usize] {
194        return self.edges.get(node).map_or(&[], Vec::as_slice);
195    }
196
197    /// Whether `node` is an alias. An unknown node is not.
198    fn is_alias(&self, node: usize) -> bool {
199        return self.is_alias.get(node).copied().unwrap_or(false);
200    }
201
202    /// The name of `node`, empty for an unknown one.
203    fn name_of(&self, node: usize) -> String {
204        return self.names.get(node).cloned().unwrap_or_default();
205    }
206
207    /// Name the members of `group` in the order they refer to each other,
208    /// closing on the name the walk started from.
209    ///
210    /// An alias holds at most one item, so the walk never branches.
211    fn cycle_through(&self, group: &[usize]) -> Vec<String> {
212        let Some(start) = group.first().copied() else {
213            return Vec::new();
214        };
215        let mut path = vec![self.name_of(start)];
216        let mut current = start;
217        for _ in 0..group.len() {
218            let Some(next) = self.edges_of(current).first().copied() else {
219                break;
220            };
221            path.push(self.name_of(next));
222            if next == start {
223                break;
224            }
225            current = next;
226        }
227        return path;
228    }
229}
230
231/// The strongly connected component of each node, by Tarjan's algorithm.
232///
233/// The walk carries its own stack, so a long chain of items cannot overflow the
234/// real one. It reads each node and each edge one time.
235fn components(edges: &[Vec<usize>]) -> Vec<usize> {
236    let mut walk = Walk::over(edges.len());
237    // Each entry is a node and how many of its edges the walk has taken.
238    let mut work: Vec<(usize, usize)> = Vec::new();
239
240    for root in 0..edges.len() {
241        if walk.is_open_or_done(root) {
242            continue;
243        }
244        walk.open(root);
245        work.push((root, 0));
246
247        while let Some(&(node, taken)) = work.last() {
248            let next = edges.get(node).and_then(|held| return held.get(taken)).copied();
249            if let Some(next) = next {
250                if let Some(entry) = work.last_mut() {
251                    entry.1 += 1;
252                }
253                walk.step(node, next, &mut work);
254                continue;
255            }
256
257            work.pop();
258            if let Some(&(parent, _)) = work.last() {
259                walk.carry_up(parent, node);
260            }
261            walk.close(node);
262        }
263    }
264    return walk.component;
265}
266
267/// The per-node bookkeeping of [`components`].
268///
269/// Every method reads a node through `get`, because the workspace denies
270/// `indexing_slicing`. A node outside the graph cannot arise: each one comes
271/// from `0..edges.len()` or from an edge, and an edge is built from a node that
272/// the module declares.
273struct Walk {
274    /// The order the walk reached each node in, or [`Walk::UNREACHED`].
275    order: Vec<usize>,
276    /// The oldest order each node can reach while the walk holds it open.
277    lowest: Vec<usize>,
278    /// Whether each node is on [`Walk::path`].
279    open: Vec<bool>,
280    /// The component of each node, filled in as each component closes.
281    component: Vec<usize>,
282    /// The nodes the walk holds open, oldest first.
283    path: Vec<usize>,
284    /// The order to give the next node the walk reaches.
285    next_order: usize,
286    /// The number to give the next component that closes.
287    next_component: usize,
288}
289
290impl Walk {
291    /// The order of a node the walk has not reached.
292    const UNREACHED: usize = usize::MAX;
293
294    /// Bookkeeping for a graph of `count` nodes.
295    fn over(count: usize) -> Self {
296        return Self {
297            order: vec![Self::UNREACHED; count],
298            lowest: vec![0; count],
299            open: vec![false; count],
300            component: vec![0; count],
301            path: Vec::new(),
302            next_order: 0,
303            next_component: 0,
304        };
305    }
306
307    /// Whether the walk has already reached `node`.
308    fn is_open_or_done(&self, node: usize) -> bool {
309        return self.order_of(node) != Self::UNREACHED;
310    }
311
312    /// The order the walk reached `node` in.
313    fn order_of(&self, node: usize) -> usize {
314        return self.order.get(node).copied().unwrap_or(Self::UNREACHED);
315    }
316
317    /// The oldest order `node` can reach.
318    fn lowest_of(&self, node: usize) -> usize {
319        return self.lowest.get(node).copied().unwrap_or(Self::UNREACHED);
320    }
321
322    /// Give `node` its order and hold it open.
323    fn open(&mut self, node: usize) {
324        if let Some(order) = self.order.get_mut(node) {
325            *order = self.next_order;
326        }
327        if let Some(lowest) = self.lowest.get_mut(node) {
328            *lowest = self.next_order;
329        }
330        if let Some(open) = self.open.get_mut(node) {
331            *open = true;
332        }
333        self.next_order += 1;
334        self.path.push(node);
335    }
336
337    /// Take the edge from `node` to `next`.
338    ///
339    /// An unreached `next` is opened and queued on `work`. A `next` the walk
340    /// still holds open closes a loop, so `node` inherits its order.
341    fn step(&mut self, node: usize, next: usize, work: &mut Vec<(usize, usize)>) {
342        if !self.is_open_or_done(next) {
343            self.open(next);
344            work.push((next, 0));
345            return;
346        }
347        if self.open.get(next).copied().unwrap_or(false) {
348            self.lower(node, self.order_of(next));
349        }
350    }
351
352    /// Carry what `node` reached up to the `parent` the walk came from.
353    fn carry_up(&mut self, parent: usize, node: usize) {
354        self.lower(parent, self.lowest_of(node));
355    }
356
357    /// Lower the oldest order `node` can reach to `order`, when that is older.
358    fn lower(&mut self, node: usize, order: usize) {
359        if let Some(lowest) = self.lowest.get_mut(node) {
360            *lowest = (*lowest).min(order);
361        }
362    }
363
364    /// Close the component `node` roots, when it roots one.
365    ///
366    /// A node that reaches nothing older than itself roots a component holding
367    /// every node opened since.
368    fn close(&mut self, node: usize) {
369        if self.lowest_of(node) != self.order_of(node) {
370            return;
371        }
372        while let Some(member) = self.path.pop() {
373            if let Some(open) = self.open.get_mut(member) {
374                *open = false;
375            }
376            if let Some(component) = self.component.get_mut(member) {
377                *component = self.next_component;
378            }
379            if member == node {
380                break;
381            }
382        }
383        self.next_component += 1;
384    }
385}
386
387/// Add every named type that `ty` holds to `out`.
388///
389/// `Vec`, `Map` and `Box` put what they hold on the heap, so the walk stops at
390/// each of them. `Box` counts here as well as the other two, so a run over a
391/// module this pass has already boxed sees the cycle as broken and changes
392/// nothing.
393fn collect_held(ty: &RustType, out: &mut BTreeSet<String>) {
394    match ty {
395        RustType::Named(name) => {
396            out.insert(canonical(name));
397        }
398        RustType::Option(inner) => collect_held(inner, out),
399        _ => {}
400    }
401}
402
403/// Box the named types inside `ty` that hold `owner` back.
404///
405/// A type that holds itself is one node and one component, so it needs no case
406/// of its own.
407fn box_held(ty: &mut RustType, owner: &str, graph: &Graph) {
408    match ty {
409        RustType::Named(name) => {
410            let target = canonical(name);
411            if graph.on_a_cycle(&target, owner) {
412                let inner = std::mem::replace(ty, RustType::Bool);
413                *ty = RustType::Boxed(Box::new(inner));
414            }
415        }
416        RustType::Option(inner) => box_held(inner, owner, graph),
417        _ => {}
418    }
419}
420
421/// The name an item is known by in the graph, matching how the emitter names it.
422fn canonical(name: &str) -> String {
423    return to_ident(name, Case::Pascal).logical().to_owned();
424}
425
426#[cfg(test)]
427mod tests {
428    use super::*;
429    use crate::ir::Alias;
430    use crate::ir::Enum;
431    use crate::ir::Field;
432    use crate::ir::Struct;
433    use crate::ir::UnionVariant;
434
435    /// A struct of one field, which is the shape every case below needs.
436    fn one_field(name: &str, field: &str, ty: RustType) -> Item {
437        return Item::Struct(Struct {
438            name: to_ident(name, Case::Pascal),
439            doc: None,
440            deprecated: None,
441            fields: vec![Field {
442                name: to_ident(field, Case::Snake),
443                rename: None,
444                doc: None,
445                deprecated: None,
446                ty,
447                required: true,
448                omit_empty: None,
449                serde_skip: false,
450                default: None,
451                constraints: None,
452            }],
453            additional_properties: None,
454            deny_unknown_fields: false,
455        });
456    }
457
458    /// The type of the first field of the first item named `name`.
459    fn field_type(module: &Module, name: &str) -> RustType {
460        for item in &module.items {
461            if let Item::Struct(strukt) = item
462                && strukt.name.logical() == name
463            {
464                return strukt.fields[0].ty.clone();
465            }
466        }
467        panic!("no struct named `{name}`");
468    }
469
470    fn named(name: &str) -> RustType {
471        return RustType::Named(name.to_owned());
472    }
473
474    fn boxed(inner: RustType) -> RustType {
475        return RustType::Boxed(Box::new(inner));
476    }
477
478    /// A field is boxed when, and only when, its type holds the owner back
479    /// without going through the heap.
480    #[test]
481    fn only_a_field_that_holds_its_owner_is_boxed() {
482        let cases: &[(&str, RustType, RustType)] = &[
483            ("direct", named("Node"), boxed(named("Node"))),
484            (
485                "through an option",
486                RustType::Option(Box::new(named("Node"))),
487                RustType::Option(Box::new(boxed(named("Node")))),
488            ),
489            (
490                "through a vec",
491                RustType::Vec(Box::new(named("Node"))),
492                RustType::Vec(Box::new(named("Node"))),
493            ),
494            (
495                "through a map",
496                RustType::Map(Box::new(named("Node"))),
497                RustType::Map(Box::new(named("Node"))),
498            ),
499            ("a scalar", RustType::String, RustType::String),
500        ];
501
502        for (label, input, want) in cases {
503            let mut module = Module {
504                items: vec![one_field("Node", "child", input.clone())],
505            };
506            box_recursive_types(&mut module).expect("no alias cycle in this module");
507            assert_eq!(field_type(&module, "Node"), *want, "self-reference {label}");
508        }
509    }
510
511    /// Both sides of a two-type cycle are boxed, so neither depends on the order
512    /// the items happen to sit in.
513    #[test]
514    fn both_sides_of_a_mutual_cycle_are_boxed() {
515        let mut module = Module {
516            items: vec![
517                one_field("Parent", "child", named("Kid")),
518                one_field("Kid", "parent", named("Parent")),
519            ],
520        };
521        box_recursive_types(&mut module).expect("no alias cycle in this module");
522        assert_eq!(field_type(&module, "Parent"), boxed(named("Kid")));
523        assert_eq!(field_type(&module, "Kid"), boxed(named("Parent")));
524    }
525
526    /// A type that a cycle merely points at is not part of the cycle, so it
527    /// keeps its plain field.
528    #[test]
529    fn a_type_the_cycle_only_points_at_is_left_alone() {
530        let mut module = Module {
531            items: vec![
532                one_field("Node", "child", named("Node")),
533                one_field("Holder", "node", named("Node")),
534            ],
535        };
536        box_recursive_types(&mut module).expect("no alias cycle in this module");
537        assert_eq!(field_type(&module, "Holder"), named("Node"));
538    }
539
540    /// A union variant holds its type the way a field does, so it is boxed too.
541    #[test]
542    fn a_union_variant_that_holds_its_own_enum_is_boxed() {
543        let mut module = Module {
544            items: vec![Item::Enum(Enum {
545                name: to_ident("Expression", Case::Pascal),
546                doc: None,
547                deprecated: None,
548                kind: EnumKind::Union(vec![
549                    UnionVariant {
550                        name: to_ident("Text", Case::Pascal),
551                        ty: RustType::String,
552                    },
553                    UnionVariant {
554                        name: to_ident("Nested", Case::Pascal),
555                        ty: named("Expression"),
556                    },
557                ]),
558            })],
559        };
560        box_recursive_types(&mut module).expect("no alias cycle in this module");
561        let Item::Enum(enumeration) = &module.items[0] else {
562            panic!("the item is an enum");
563        };
564        let EnumKind::Union(variants) = &enumeration.kind else {
565            panic!("the enum is a union");
566        };
567        assert_eq!(variants[0].ty, RustType::String);
568        assert_eq!(variants[1].ty, boxed(named("Expression")));
569    }
570
571    /// An alias offers nothing to box, so a cycle running through one is broken
572    /// at the struct field instead.
573    #[test]
574    fn a_cycle_through_an_alias_is_boxed_at_the_struct() {
575        let mut module = Module {
576            items: vec![
577                Item::Alias(Alias {
578                    name: to_ident("Wrapper", Case::Pascal),
579                    doc: None,
580                    deprecated: None,
581                    ty: named("Holder"),
582                }),
583                one_field("Holder", "wrapped", named("Wrapper")),
584            ],
585        };
586        box_recursive_types(&mut module).expect("this cycle holds a struct, so it is not alias-only");
587        assert_eq!(field_type(&module, "Holder"), boxed(named("Wrapper")));
588    }
589
590    /// A cycle made only of aliases has nowhere to put a box, so it is an error.
591    #[test]
592    fn an_alias_only_cycle_is_rejected() {
593        let alias = |name: &str, target: &str| {
594            return Item::Alias(Alias {
595                name: to_ident(name, Case::Pascal),
596                doc: None,
597                deprecated: None,
598                ty: named(target),
599            });
600        };
601        let mut module = Module {
602            items: vec![alias("Loop", "Ring"), alias("Ring", "Loop")],
603        };
604        let outcome = box_recursive_types(&mut module);
605        assert!(
606            matches!(outcome, Err(Error::RecursiveAlias { .. })),
607            "a cycle of aliases must be rejected, and gave: {outcome:?}",
608        );
609    }
610
611    /// An alias that refers to itself is the same problem with one member.
612    #[test]
613    fn a_self_referencing_alias_is_rejected() {
614        let mut module = Module {
615            items: vec![Item::Alias(Alias {
616                name: to_ident("Loop", Case::Pascal),
617                doc: None,
618                deprecated: None,
619                ty: named("Loop"),
620            })],
621        };
622        assert!(matches!(
623            box_recursive_types(&mut module),
624            Err(Error::RecursiveAlias { .. })
625        ));
626    }
627
628    /// A module with no cycle keeps every type exactly as it was.
629    #[test]
630    fn a_module_without_a_cycle_is_unchanged() {
631        let mut module = Module {
632            items: vec![
633                one_field("Holder", "node", named("Node")),
634                one_field("Node", "id", RustType::String),
635            ],
636        };
637        let before = module.clone();
638        box_recursive_types(&mut module).expect("no alias cycle in this module");
639        assert_eq!(module, before);
640    }
641
642    /// An edge that is already boxed breaks the cycle, so nothing else on it is
643    /// boxed.
644    ///
645    /// A `Box` is heap indirection, so a boxed field holds nothing. A walk that
646    /// stepped through a `Box` would still see the old cycle and would box a
647    /// second edge that needs no box.
648    #[test]
649    fn an_existing_box_breaks_the_cycle_for_every_other_edge() {
650        let mut module = Module {
651            items: vec![
652                one_field("Parent", "child", boxed(named("Kid"))),
653                one_field("Kid", "parent", named("Parent")),
654            ],
655        };
656        box_recursive_types(&mut module).expect("no alias cycle in this module");
657        assert_eq!(field_type(&module, "Parent"), boxed(named("Kid")));
658        assert_eq!(field_type(&module, "Kid"), named("Parent"));
659    }
660}