shape/case_enum/one.rs
1use indexmap::IndexSet;
2
3use crate::case_enum::Error;
4use crate::location::Location;
5use crate::merge::MergeSet;
6use crate::Shape;
7use crate::ShapeCase;
8
9/// The contents of a [`ShapeCase::One`]. Constructing this requires simplifying the shapes it
10/// contains so there aren't unnecessary duplicates left over and so hashes are consistent.
11///
12/// This intentionally can't be constructed directly because it prevents accidental unsimplified
13/// [`ShapeCase::One`]s. Construct this via [`Shape::one`] instead.
14#[derive(Clone, Debug, PartialEq, Eq, Hash)]
15pub struct One {
16 pub(crate) shapes: MergeSet<Shape>,
17}
18
19impl One {
20 pub fn is_empty(&self) -> bool {
21 self.shapes.is_empty()
22 }
23
24 pub fn iter(&self) -> impl Iterator<Item = &Shape> {
25 self.shapes.iter()
26 }
27
28 pub fn contains(&self, shape: &Shape) -> bool {
29 self.shapes.contains(shape)
30 }
31}
32
33impl Shape {
34 /// The unsatisfiable shape, analogous to TypeScript's `never` type: an
35 /// empty `One<>` union, which no value inhabits.
36 ///
37 /// A `Never` is always the end of some line of reasoning. It may be an
38 /// intersection of incompatible types, a `!` assertion on a shape that is
39 /// always `None`, or a union with nothing left in it, and a caller handed
40 /// a bare `Never` cannot tell which. So `reason` is required, and is
41 /// attached as an [`Error`] in the shape's [`crate::meta::ShapeMeta`],
42 /// exactly as [`Shape::error`] attaches its message. It is reachable via
43 /// [`own_errors`](Shape::own_errors) and [`errors`](Shape::errors), and
44 /// renders as the `(err "...")` suffix of
45 /// [`pretty_print`](Shape::pretty_print).
46 ///
47 /// The reason is diagnostic, never semantic. [`PartialEq`] and
48 /// [`Hash`](std::hash::Hash) for [`Shape`] compare only the
49 /// [`ShapeCase`], so two `Never`s with different reasons remain equal and
50 /// hash alike, and [`is_never`](Shape::is_never) does not consult the
51 /// reason either.
52 ///
53 /// Write the reason as a statement of the conflict rather than of its
54 /// consequence, for example `"no value is both a number and a string"`
55 /// rather than `"this can never match"`, so that it still reads correctly
56 /// when it surfaces nested inside a larger diagnostic.
57 #[must_use]
58 pub fn never(reason: impl Into<String>, locations: impl IntoIterator<Item = Location>) -> Self {
59 Shape::new_with_errors(
60 ShapeCase::One(One {
61 shapes: MergeSet::new([]),
62 }),
63 locations,
64 [Error {
65 message: reason.into(),
66 }],
67 )
68 }
69
70 #[must_use]
71 pub fn is_never(&self) -> bool {
72 matches!(self.case(), ShapeCase::One(One { shapes }) if shapes.is_empty())
73 }
74}
75
76/// Builds a [`Shape`] with [`ShapeCase::One`] _unless_ the shapes can simplify to a single
77/// [`ShapeCase`].
78pub(crate) fn one(shapes: impl Iterator<Item = Shape>, mut locations: IndexSet<Location>) -> Shape {
79 let mut shapes = shapes.peekable();
80 if shapes.peek().is_none() {
81 // The empty One<> union represents an unsatisfiable shape
82 // analogous to TypeScript's `never` type.
83 return Shape::never("a union of no shapes admits no value", locations);
84 }
85
86 let mut new_shapes = MergeSet::new([]);
87
88 // Flatten any nested `One` shapes
89 for shape in shapes {
90 if let ShapeCase::One(One {
91 shapes: inner_shapes,
92 }) = shape.case()
93 {
94 locations.extend(shape.locations().cloned());
95 new_shapes.extend(inner_shapes);
96 } else {
97 new_shapes.insert(shape);
98 }
99 }
100
101 if new_shapes.is_empty() {
102 // Flattening consumed every member, which means every member was
103 // itself a Never. Those members' own reasons are dropped rather than
104 // collected here, so this reason states only what is certain from
105 // this vantage point.
106 return Shape::never(
107 "every member of this union admits no value",
108 locations.iter().cloned(),
109 );
110 }
111
112 // The presence of the general Unknown shape subsumes all
113 // other union member shapes.
114 if let Some(unknown) = new_shapes
115 .iter()
116 .find(|shape| shape.case() == ShapeCase::Unknown)
117 {
118 return unknown.clone();
119 }
120
121 // The presence of the general Bool shape subsumes all
122 // specific literal boolean shapes.
123 let boolean_shape = ShapeCase::Bool(None);
124 if new_shapes.iter().any(|shape| shape.case() == boolean_shape) {
125 new_shapes.retain(|shape| !matches!(shape.case(), ShapeCase::Bool(Some(_))));
126 } else {
127 // Also, if new_shapes contains both true and false, then we
128 // can remove them and simplify the union by adding
129 // boolean_shape.
130 //
131 // Theoretically this same logic could apply to Int or
132 // String, but that would require somehow forming a union of
133 // every possible integer or string value.
134 let true_shape = ShapeCase::Bool(Some(true));
135 let false_shape = ShapeCase::Bool(Some(false));
136
137 let mut true_shapes = new_shapes
138 .iter()
139 .filter(|shape| shape.case() == true_shape)
140 .peekable();
141 let mut false_shapes = new_shapes
142 .iter()
143 .filter(|shape| shape.case() == false_shape)
144 .peekable();
145
146 if true_shapes.peek().is_some() && false_shapes.peek().is_some() {
147 new_shapes.insert(Shape::bool(
148 true_shapes
149 .chain(false_shapes)
150 .flat_map(Shape::locations)
151 .cloned(),
152 ));
153 new_shapes.retain(|shape| !matches!(shape.case(), ShapeCase::Bool(Some(_))));
154 }
155 }
156
157 // The presence of the general String shape subsumes all
158 // specific literal string shapes.
159 if new_shapes
160 .iter()
161 .any(|shape| shape.case() == ShapeCase::String(None))
162 {
163 new_shapes.retain(|shape| !matches!(shape.case(), ShapeCase::String(Some(_))));
164 }
165
166 if new_shapes
167 .iter()
168 .any(|shape| shape.case() == ShapeCase::Float)
169 {
170 // The presence of the general Float shape subsumes all
171 // general and literal Int shapes.
172 new_shapes.retain(|shape| !matches!(shape.case(), ShapeCase::Int(_)));
173 } else if new_shapes
174 .iter()
175 .any(|shape| shape.case() == ShapeCase::Int(None))
176 {
177 // The presence of the general Int shape subsumes all
178 // specific literal int shapes.
179 new_shapes.retain(|shape| !matches!(shape.case(), ShapeCase::Int(Some(_))));
180 }
181
182 // It's tempting to try to simplify new_shapes further by
183 // removing any shape that satisfies another shape, since that's
184 // one interpretation of the cases above: specific primitive
185 // value shapes like 123 satisfy general primitive shapes like
186 // Int, and therefore add nothing to the union if the general
187 // shape is present.
188 //
189 // However, in the cases above, the specific value shapes happen
190 // to be fully subsumed by the general shape, adding no further
191 // information to the general union. When we're dealing with
192 // arrays or objects, a shape can satisfy another shape and
193 // still add information to the union, so information would be
194 // lost if we removed such shapes here.
195 //
196 // For example, since { a: Int } is a more general shape than
197 // (is satisfied by) { a: Int, b: String }, including { a: Int,
198 // b: String } in a union with { a: Int } technically does not
199 // change the set of values represented by the union, but you
200 // might still like to know that the b field's shape across the
201 // union is String | None, rather than just None, as it would be
202 // if we removed the more specific shape from new_shapes.
203 //
204 // As a side note, detecting satisfaction among all pairs of
205 // members of the union is a quadratic operation in the size of
206 // the union, so it's nice we don't need to do all that work.
207
208 // A union of one shape is equivalent to that shape by itself.
209 if let (Some(only_shape), 1) = (new_shapes.first(), new_shapes.len()) {
210 return only_shape.clone();
211 }
212
213 Shape::new(ShapeCase::One(One { shapes: new_shapes }), locations)
214}