shape/case_enum/one.rs
1use indexmap::IndexSet;
2
3use crate::location::Location;
4use crate::merge::MergeSet;
5use crate::Shape;
6use crate::ShapeCase;
7
8/// The contents of a [`ShapeCase::One`]. Constructing this requires simplifying the shapes it
9/// contains so there aren't unnecessary duplicates left over and so hashes are consistent.
10///
11/// This intentionally can't be constructed directly because it prevents accidental unsimplified
12/// [`ShapeCase::One`]s. Construct this via [`Shape::one`] instead.
13#[derive(Clone, Debug, PartialEq, Eq, Hash)]
14pub struct One {
15 pub(crate) shapes: MergeSet<Shape>,
16}
17
18impl One {
19 pub fn is_empty(&self) -> bool {
20 self.shapes.is_empty()
21 }
22
23 pub fn iter(&self) -> impl Iterator<Item = &Shape> {
24 self.shapes.iter()
25 }
26
27 pub fn contains(&self, shape: &Shape) -> bool {
28 self.shapes.contains(shape)
29 }
30}
31
32impl Shape {
33 pub fn never(locations: impl IntoIterator<Item = Location>) -> Self {
34 Shape::new(
35 ShapeCase::One(One {
36 shapes: MergeSet::new([]),
37 }),
38 locations,
39 )
40 }
41
42 #[must_use]
43 pub fn is_never(&self) -> bool {
44 matches!(self.case(), ShapeCase::One(One { shapes }) if shapes.is_empty())
45 }
46}
47
48/// Builds a [`Shape`] with [`ShapeCase::One`] _unless_ the shapes can simplify to a single
49/// [`ShapeCase`].
50pub(crate) fn one(shapes: impl Iterator<Item = Shape>, mut locations: IndexSet<Location>) -> Shape {
51 let mut shapes = shapes.peekable();
52 if shapes.peek().is_none() {
53 // The empty One<> union represents an unsatisfiable shape
54 // analogous to TypeScript's `never` type.
55 return Shape::never(locations);
56 }
57
58 let mut new_shapes = MergeSet::new([]);
59
60 // Flatten any nested `One` shapes
61 for shape in shapes {
62 if let ShapeCase::One(One {
63 shapes: inner_shapes,
64 }) = shape.case.as_ref()
65 {
66 locations.extend(shape.locations().cloned());
67 new_shapes.extend(inner_shapes);
68 } else {
69 new_shapes.insert(shape);
70 }
71 }
72
73 if new_shapes.is_empty() {
74 return Shape::never(locations.iter().cloned());
75 }
76
77 // The presence of the general Unknown shape subsumes all
78 // other union member shapes.
79 if let Some(unknown) = new_shapes
80 .iter()
81 .find(|shape| shape.case() == ShapeCase::Unknown)
82 {
83 return unknown.clone();
84 }
85
86 // The presence of the general Bool shape subsumes all
87 // specific literal boolean shapes.
88 let boolean_shape = ShapeCase::Bool(None);
89 if new_shapes.iter().any(|shape| shape.case() == boolean_shape) {
90 new_shapes.retain(|shape| !matches!(shape.case.as_ref(), ShapeCase::Bool(Some(_))));
91 } else {
92 // Also, if new_shapes contains both true and false, then we
93 // can remove them and simplify the union by adding
94 // boolean_shape.
95 //
96 // Theoretically this same logic could apply to Int or
97 // String, but that would require somehow forming a union of
98 // every possible integer or string value.
99 let true_shape = ShapeCase::Bool(Some(true));
100 let false_shape = ShapeCase::Bool(Some(false));
101
102 let mut true_shapes = new_shapes
103 .iter()
104 .filter(|shape| shape.case() == true_shape)
105 .peekable();
106 let mut false_shapes = new_shapes
107 .iter()
108 .filter(|shape| shape.case() == false_shape)
109 .peekable();
110
111 if true_shapes.peek().is_some() && false_shapes.peek().is_some() {
112 new_shapes.insert(Shape::bool(
113 true_shapes
114 .chain(false_shapes)
115 .flat_map(Shape::locations)
116 .cloned(),
117 ));
118 new_shapes.retain(|shape| !matches!(shape.case(), ShapeCase::Bool(Some(_))));
119 }
120 }
121
122 // The presence of the general String shape subsumes all
123 // specific literal string shapes.
124 if new_shapes
125 .iter()
126 .any(|shape| shape.case() == ShapeCase::String(None))
127 {
128 new_shapes.retain(|shape| !matches!(shape.case.as_ref(), ShapeCase::String(Some(_))));
129 }
130
131 if new_shapes
132 .iter()
133 .any(|shape| shape.case() == ShapeCase::Float)
134 {
135 // The presence of the general Float shape subsumes all
136 // general and literal Int shapes.
137 new_shapes.retain(|shape| !matches!(shape.case.as_ref(), ShapeCase::Int(_)));
138 } else if new_shapes
139 .iter()
140 .any(|shape| shape.case() == ShapeCase::Int(None))
141 {
142 // The presence of the general Int shape subsumes all
143 // specific literal int shapes.
144 new_shapes.retain(|shape| !matches!(shape.case.as_ref(), ShapeCase::Int(Some(_))));
145 }
146
147 // It's tempting to try to simplify new_shapes further by
148 // removing any shape that satisfies another shape, since that's
149 // one interpretation of the cases above: specific primitive
150 // value shapes like 123 satisfy general primitive shapes like
151 // Int, and therefore add nothing to the union if the general
152 // shape is present.
153 //
154 // However, in the cases above, the specific value shapes happen
155 // to be fully subsumed by the general shape, adding no further
156 // information to the general union. When we're dealing with
157 // arrays or objects, a shape can satisfy another shape and
158 // still add information to the union, so information would be
159 // lost if we removed such shapes here.
160 //
161 // For example, since { a: Int } is a more general shape than
162 // (is satisfied by) { a: Int, b: String }, including { a: Int,
163 // b: String } in a union with { a: Int } technically does not
164 // change the set of values represented by the union, but you
165 // might still like to know that the b field's shape across the
166 // union is String | None, rather than just None, as it would be
167 // if we removed the more specific shape from new_shapes.
168 //
169 // As a side note, detecting satisfaction among all pairs of
170 // members of the union is a quadratic operation in the size of
171 // the union, so it's nice we don't need to do all that work.
172
173 // A union of one shape is equivalent to that shape by itself.
174 if let (Some(only_shape), 1) = (new_shapes.first(), new_shapes.len()) {
175 return only_shape.clone();
176 }
177
178 Shape::new(ShapeCase::One(One { shapes: new_shapes }), locations)
179}