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}