typify_impl/
cycles.rs

1// Copyright 2023 Oxide Computer Company
2
3use std::{
4    collections::{BTreeMap, BTreeSet},
5    ops::Range,
6};
7
8use crate::{
9    type_entry::{
10        TypeEntry, TypeEntryDetails, TypeEntryEnum, TypeEntryNewtype, TypeEntryStruct,
11        VariantDetails,
12    },
13    TypeId, TypeSpace,
14};
15
16impl TypeSpace {
17    /// We need to root out any containment cycles, breaking them by inserting
18    /// a `Box` type. Our choice of *where* to break cycles is more arbitrary
19    /// than optimal, but is well beyond sufficient.
20    pub fn break_cycles(&mut self, range: Range<u64>) {
21        enum Node {
22            Start {
23                type_id: TypeId,
24            },
25            Processing {
26                type_id: TypeId,
27                children_ids: Vec<TypeId>,
28            },
29        }
30
31        let mut visited = BTreeSet::<TypeId>::new();
32
33        for id in range {
34            let type_id = TypeId(id);
35
36            // This isn't strictly necessary, but we'll short-circuit some work
37            // by checking this right away.
38            if visited.contains(&type_id) {
39                continue;
40            }
41
42            let mut active = BTreeSet::<TypeId>::new();
43            let mut stack = Vec::<Node>::new();
44
45            active.insert(type_id.clone());
46            stack.push(Node::Start { type_id });
47
48            while let Some(top) = stack.last_mut() {
49                match top {
50                    // Skip right to the end since we've already seen this type.
51                    Node::Start { type_id } if visited.contains(type_id) => {
52                        assert!(active.contains(type_id));
53
54                        let type_id = type_id.clone();
55                        *top = Node::Processing {
56                            type_id,
57                            children_ids: Vec::new(),
58                        };
59                    }
60
61                    // Break any immediate cycles and queue up this type for
62                    // descent into its child types.
63                    Node::Start { type_id } => {
64                        assert!(active.contains(type_id));
65
66                        visited.insert(type_id.clone());
67
68                        // Determine which child types form cycles--and
69                        // therefore need to be snipped--and the rest--into
70                        // which we should descend. We make this its own block
71                        // to clarify the lifetime of the exclusive reference
72                        // to the type. We don't really *need* to have an
73                        // exclusive reference here, but there's no point in
74                        // writing `get_child_ids` again for shared references.
75                        let (snip, descend) = {
76                            let type_entry = self.id_to_entry.get_mut(type_id).unwrap();
77
78                            let child_ids = get_child_ids(type_entry)
79                                .into_iter()
80                                .map(|child_id| child_id.clone());
81
82                            // If the child type is in active then we've found
83                            // a cycle (otherwise we'll descend).
84                            child_ids.partition::<Vec<_>, _>(|child_id| active.contains(child_id))
85                        };
86
87                        // Note that while `snip` might contain duplicates,
88                        // `id_to_box` is idempotent insofar as the same input
89                        // TypeId will result in the same output TypeId. Ergo
90                        // the resulting pairs from which we construct the
91                        // mapping would contain exact duplicates; it would not
92                        // contain two values associated with the same key.
93                        let replace = snip
94                            .into_iter()
95                            .map(|type_id| {
96                                let box_id = self.id_to_box(&type_id);
97
98                                (type_id, box_id)
99                            })
100                            .collect::<BTreeMap<_, _>>();
101
102                        // Break any cycles by reassigning the child type to a box.
103                        let type_entry = self.id_to_entry.get_mut(type_id).unwrap();
104                        let child_ids = get_child_ids(type_entry);
105                        for child_id in child_ids {
106                            if let Some(replace_id) = replace.get(child_id) {
107                                *child_id = replace_id.clone();
108                            }
109                        }
110
111                        // Descend into child types.
112                        let node = Node::Processing {
113                            type_id: type_id.clone(),
114                            children_ids: descend,
115                        };
116                        *top = node;
117                    }
118
119                    // If there are children left, push the next child onto the
120                    // stack. If there are none left, pop this type.
121                    Node::Processing {
122                        type_id,
123                        children_ids,
124                    } => {
125                        if let Some(type_id) = children_ids.pop() {
126                            // Descend into the next child node.
127                            active.insert(type_id.clone());
128                            stack.push(Node::Start { type_id });
129                        } else {
130                            // All done; remove the item from the active list
131                            // and stack.
132                            active.remove(type_id);
133                            let _ = stack.pop();
134                        }
135                    }
136                }
137            }
138        }
139    }
140}
141
142/// For types that could potentially participate in a cycle, return a list of
143/// mutable references to the child types.
144fn get_child_ids(type_entry: &mut TypeEntry) -> Vec<&mut TypeId> {
145    match &mut type_entry.details {
146        TypeEntryDetails::Enum(TypeEntryEnum { variants, .. }) => variants
147            .iter_mut()
148            .flat_map(|variant| match &mut variant.details {
149                VariantDetails::Simple => Vec::new(),
150                VariantDetails::Item(type_id) => vec![type_id],
151                VariantDetails::Tuple(type_ids) => type_ids.iter_mut().collect(),
152                VariantDetails::Struct(properties) => properties
153                    .iter_mut()
154                    .map(|prop| &mut prop.type_id)
155                    .collect(),
156            })
157            .collect::<Vec<_>>(),
158
159        TypeEntryDetails::Struct(TypeEntryStruct { properties, .. }) => properties
160            .iter_mut()
161            .map(|prop| &mut prop.type_id)
162            .collect(),
163
164        TypeEntryDetails::Newtype(TypeEntryNewtype { type_id, .. }) => {
165            vec![type_id]
166        }
167
168        // Unnamed types that can participate in containment cycles.
169        TypeEntryDetails::Option(type_id) => vec![type_id],
170        TypeEntryDetails::Array(type_id, _) => vec![type_id],
171        TypeEntryDetails::Tuple(type_ids) => type_ids.iter_mut().collect(),
172
173        _ => Vec::new(),
174    }
175}
176
177#[cfg(test)]
178mod tests {
179    use schema::Schema;
180    use schemars::JsonSchema;
181
182    use crate::test_util::validate_output;
183
184    #[test]
185    fn test_trivial_cycle() {
186        #[derive(JsonSchema, Schema)]
187        #[allow(dead_code)]
188        struct A {
189            a: Box<A>,
190        }
191
192        validate_output::<A>();
193    }
194
195    #[test]
196    fn test_optional_trivial_cycle() {
197        #[derive(JsonSchema, Schema)]
198        #[allow(dead_code)]
199        struct A {
200            a: Option<Box<A>>,
201        }
202
203        validate_output::<A>();
204    }
205
206    #[test]
207    fn test_enum_trivial_cycles() {
208        #[derive(JsonSchema, Schema)]
209        #[allow(dead_code)]
210        enum A {
211            Variant0(u64),
212            Variant1 {
213                a: u64,
214                b: Vec<A>,
215                rop: Option<Box<A>>,
216            },
217            Variant2 {
218                a: Box<A>,
219            },
220            Variant3(u64, Box<A>),
221            Variant4(Option<Box<A>>, String),
222        }
223
224        validate_output::<A>();
225    }
226
227    #[test]
228    fn test_newtype_trivial_cycle() {
229        #[derive(JsonSchema, Schema)]
230        #[allow(dead_code)]
231        struct A(Box<A>);
232
233        validate_output::<A>();
234    }
235
236    #[test]
237    fn test_abab_cycle() {
238        #[derive(JsonSchema, Schema)]
239        #[allow(dead_code)]
240        struct A(B);
241
242        #[derive(JsonSchema, Schema)]
243        #[allow(dead_code)]
244        struct B(Box<A>);
245
246        validate_output::<A>();
247    }
248}