Skip to main content

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}