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}