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::Access;
430    use crate::ir::Alias;
431    use crate::ir::Enum;
432    use crate::ir::Field;
433    use crate::ir::Struct;
434    use crate::ir::UnionVariant;
435
436    /// A struct of one field, which is the shape every case below needs.
437    fn one_field(name: &str, field: &str, ty: RustType) -> Item {
438        return Item::Struct(Struct {
439            name: to_ident(name, Case::Pascal),
440            doc: None,
441            deprecated: None,
442            fields: vec![Field {
443                name: to_ident(field, Case::Snake),
444                rename: None,
445                doc: None,
446                deprecated: None,
447                ty,
448                required: true,
449                omit_empty: None,
450                serde_skip: false,
451                default: None,
452                constraints: None,
453                access: Access::ReadWrite,
454            }],
455            additional_properties: None,
456            deny_unknown_fields: false,
457        });
458    }
459
460    /// The type of the first field of the first item named `name`.
461    fn field_type(module: &Module, name: &str) -> RustType {
462        for item in &module.items {
463            if let Item::Struct(strukt) = item
464                && strukt.name.logical() == name
465            {
466                return strukt.fields[0].ty.clone();
467            }
468        }
469        panic!("no struct named `{name}`");
470    }
471
472    fn named(name: &str) -> RustType {
473        return RustType::Named(name.to_owned());
474    }
475
476    fn boxed(inner: RustType) -> RustType {
477        return RustType::Boxed(Box::new(inner));
478    }
479
480    /// A field is boxed when, and only when, its type holds the owner back
481    /// without going through the heap.
482    #[test]
483    fn only_a_field_that_holds_its_owner_is_boxed() {
484        let cases: &[(&str, RustType, RustType)] = &[
485            ("direct", named("Node"), boxed(named("Node"))),
486            (
487                "through an option",
488                RustType::Option(Box::new(named("Node"))),
489                RustType::Option(Box::new(boxed(named("Node")))),
490            ),
491            (
492                "through a vec",
493                RustType::Vec(Box::new(named("Node"))),
494                RustType::Vec(Box::new(named("Node"))),
495            ),
496            (
497                "through a map",
498                RustType::Map(Box::new(named("Node"))),
499                RustType::Map(Box::new(named("Node"))),
500            ),
501            ("a scalar", RustType::String, RustType::String),
502        ];
503
504        for (label, input, want) in cases {
505            let mut module = Module {
506                items: vec![one_field("Node", "child", input.clone())],
507            };
508            box_recursive_types(&mut module).expect("no alias cycle in this module");
509            assert_eq!(field_type(&module, "Node"), *want, "self-reference {label}");
510        }
511    }
512
513    /// Both sides of a two-type cycle are boxed, so neither depends on the order
514    /// the items happen to sit in.
515    #[test]
516    fn both_sides_of_a_mutual_cycle_are_boxed() {
517        let mut module = Module {
518            items: vec![
519                one_field("Parent", "child", named("Kid")),
520                one_field("Kid", "parent", named("Parent")),
521            ],
522        };
523        box_recursive_types(&mut module).expect("no alias cycle in this module");
524        assert_eq!(field_type(&module, "Parent"), boxed(named("Kid")));
525        assert_eq!(field_type(&module, "Kid"), boxed(named("Parent")));
526    }
527
528    /// A type that a cycle merely points at is not part of the cycle, so it
529    /// keeps its plain field.
530    #[test]
531    fn a_type_the_cycle_only_points_at_is_left_alone() {
532        let mut module = Module {
533            items: vec![
534                one_field("Node", "child", named("Node")),
535                one_field("Holder", "node", named("Node")),
536            ],
537        };
538        box_recursive_types(&mut module).expect("no alias cycle in this module");
539        assert_eq!(field_type(&module, "Holder"), named("Node"));
540    }
541
542    /// A union variant holds its type the way a field does, so it is boxed too.
543    #[test]
544    fn a_union_variant_that_holds_its_own_enum_is_boxed() {
545        let mut module = Module {
546            items: vec![Item::Enum(Enum {
547                name: to_ident("Expression", Case::Pascal),
548                doc: None,
549                deprecated: None,
550                kind: EnumKind::Union(vec![
551                    UnionVariant {
552                        name: to_ident("Text", Case::Pascal),
553                        ty: RustType::String,
554                    },
555                    UnionVariant {
556                        name: to_ident("Nested", Case::Pascal),
557                        ty: named("Expression"),
558                    },
559                ]),
560            })],
561        };
562        box_recursive_types(&mut module).expect("no alias cycle in this module");
563        let Item::Enum(enumeration) = &module.items[0] else {
564            panic!("the item is an enum");
565        };
566        let EnumKind::Union(variants) = &enumeration.kind else {
567            panic!("the enum is a union");
568        };
569        assert_eq!(variants[0].ty, RustType::String);
570        assert_eq!(variants[1].ty, boxed(named("Expression")));
571    }
572
573    /// An alias offers nothing to box, so a cycle running through one is broken
574    /// at the struct field instead.
575    #[test]
576    fn a_cycle_through_an_alias_is_boxed_at_the_struct() {
577        let mut module = Module {
578            items: vec![
579                Item::Alias(Alias {
580                    name: to_ident("Wrapper", Case::Pascal),
581                    doc: None,
582                    deprecated: None,
583                    ty: named("Holder"),
584                }),
585                one_field("Holder", "wrapped", named("Wrapper")),
586            ],
587        };
588        box_recursive_types(&mut module).expect("this cycle holds a struct, so it is not alias-only");
589        assert_eq!(field_type(&module, "Holder"), boxed(named("Wrapper")));
590    }
591
592    /// A cycle made only of aliases has nowhere to put a box, so it is an error.
593    #[test]
594    fn an_alias_only_cycle_is_rejected() {
595        let alias = |name: &str, target: &str| {
596            return Item::Alias(Alias {
597                name: to_ident(name, Case::Pascal),
598                doc: None,
599                deprecated: None,
600                ty: named(target),
601            });
602        };
603        let mut module = Module {
604            items: vec![alias("Loop", "Ring"), alias("Ring", "Loop")],
605        };
606        let outcome = box_recursive_types(&mut module);
607        assert!(
608            matches!(outcome, Err(Error::RecursiveAlias { .. })),
609            "a cycle of aliases must be rejected, and gave: {outcome:?}",
610        );
611    }
612
613    /// An alias that refers to itself is the same problem with one member.
614    #[test]
615    fn a_self_referencing_alias_is_rejected() {
616        let mut module = Module {
617            items: vec![Item::Alias(Alias {
618                name: to_ident("Loop", Case::Pascal),
619                doc: None,
620                deprecated: None,
621                ty: named("Loop"),
622            })],
623        };
624        assert!(matches!(
625            box_recursive_types(&mut module),
626            Err(Error::RecursiveAlias { .. })
627        ));
628    }
629
630    /// A module with no cycle keeps every type exactly as it was.
631    #[test]
632    fn a_module_without_a_cycle_is_unchanged() {
633        let mut module = Module {
634            items: vec![
635                one_field("Holder", "node", named("Node")),
636                one_field("Node", "id", RustType::String),
637            ],
638        };
639        let before = module.clone();
640        box_recursive_types(&mut module).expect("no alias cycle in this module");
641        assert_eq!(module, before);
642    }
643
644    /// An edge that is already boxed breaks the cycle, so nothing else on it is
645    /// boxed.
646    ///
647    /// A `Box` is heap indirection, so a boxed field holds nothing. A walk that
648    /// stepped through a `Box` would still see the old cycle and would box a
649    /// second edge that needs no box.
650    #[test]
651    fn an_existing_box_breaks_the_cycle_for_every_other_edge() {
652        let mut module = Module {
653            items: vec![
654                one_field("Parent", "child", boxed(named("Kid"))),
655                one_field("Kid", "parent", named("Parent")),
656            ],
657        };
658        box_recursive_types(&mut module).expect("no alias cycle in this module");
659        assert_eq!(field_type(&module, "Parent"), boxed(named("Kid")));
660        assert_eq!(field_type(&module, "Kid"), named("Parent"));
661    }
662}