spacetimedb_sats/
sum_type.rs

1use crate::algebraic_value::de::{ValueDeserializeError, ValueDeserializer};
2use crate::algebraic_value::ser::value_serialize;
3use crate::de::Deserialize;
4use crate::meta_type::MetaType;
5use crate::{AlgebraicType, AlgebraicValue, SpacetimeType, SumTypeVariant};
6
7/// The tag used for the `Interval` variant of the special `ScheduleAt` sum type.
8pub const SCHEDULE_AT_INTERVAL_TAG: &str = "Interval";
9/// The tag used for the `Time` variant of the special `ScheduleAt` sum type.
10pub const SCHEDULE_AT_TIME_TAG: &str = "Time";
11/// The tag used for the `some` variant of the special `option` sum type.
12pub const OPTION_SOME_TAG: &str = "some";
13/// The tag used for the `none` variant of the special `option` sum type.
14pub const OPTION_NONE_TAG: &str = "none";
15
16/// A structural sum type.
17///
18/// Unlike most languages, sums in SATS are *[structural]* and not nominal.
19/// When checking whether two nominal types are the same,
20/// their names and/or declaration sites (e.g., module / namespace) are considered.
21/// Meanwhile, a structural type system would only check the structure of the type itself,
22/// e.g., the names of its variants and their inner data types in the case of a sum.
23///
24/// This is also known as a discriminated union (implementation) or disjoint union.
25/// Another name is [coproduct (category theory)](https://ncatlab.org/nlab/show/coproduct).
26///
27/// These structures are known as sum types because the number of possible values a sum
28/// ```text
29/// { N_0(T_0), N_1(T_1), ..., N_n(T_n) }
30/// ```
31/// is:
32/// ```text
33/// Σ (i ∈ 0..n). values(T_i)
34/// ```
35/// so for example, `values({ A(U64), B(Bool) }) = values(U64) + values(Bool)`.
36///
37/// See also:
38/// - <https://en.wikipedia.org/wiki/Tagged_union>
39/// - <https://ncatlab.org/nlab/show/sum+type>
40///
41/// [structural]: https://en.wikipedia.org/wiki/Structural_type_system
42#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, SpacetimeType)]
43#[sats(crate = crate)]
44pub struct SumType {
45    /// The possible variants of the sum type.
46    ///
47    /// The order is relevant as it defines the tags of the variants at runtime.
48    pub variants: Box<[SumTypeVariant]>,
49}
50
51impl std::fmt::Debug for SumType {
52    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53        f.write_str("SumType ")?;
54        f.debug_map()
55            .entries(
56                self.variants
57                    .iter()
58                    .map(|variant| (crate::dbg_aggregate_name(&variant.name), &variant.algebraic_type)),
59            )
60            .finish()
61    }
62}
63
64impl SumType {
65    /// Returns a sum type with these possible `variants`.
66    pub const fn new(variants: Box<[SumTypeVariant]>) -> Self {
67        Self { variants }
68    }
69
70    /// Returns a sum type of unnamed variants taken from `types`.
71    pub fn new_unnamed(types: Box<[AlgebraicType]>) -> Self {
72        let variants = Vec::from(types).into_iter().map(|ty| ty.into()).collect();
73        Self { variants }
74    }
75
76    /// Check whether this sum type is a structural option type.
77    ///
78    /// A structural option type has `some(T)` as its first variant and `none` as its second.
79    /// That is, `{ some(T), none }` or `some: T | none` depending on your notation.
80    /// Note that `some` and `none` are lowercase, unlike Rust's `Option`.
81    /// Order matters, and an option type with these variants in the opposite order will not be recognized.
82    ///
83    /// If the type does look like a structural option type, returns the type `T`.
84    pub fn as_option(&self) -> Option<&AlgebraicType> {
85        match &*self.variants {
86            [first, second] if Self::are_variants_option(first, second) => Some(&first.algebraic_type),
87            _ => None,
88        }
89    }
90
91    /// Check whether this sum type is a structural option type.
92    ///
93    /// A structural option type has `some(T)` as its first variant and `none` as its second.
94    /// That is, `{ some(T), none }` or `some: T | none` depending on your notation.
95    /// Note that `some` and `none` are lowercase, unlike Rust's `Option`.
96    /// Order matters, and an option type with these variants in the opposite order will not be recognized.
97    ///
98    /// If the type does look like a structural option type, returns the type `T`.
99    pub fn as_option_mut(&mut self) -> Option<&mut AlgebraicType> {
100        match &mut *self.variants {
101            [first, second] if Self::are_variants_option(first, second) => Some(&mut first.algebraic_type),
102            _ => None,
103        }
104    }
105
106    fn are_variants_option(first: &SumTypeVariant, second: &SumTypeVariant) -> bool {
107        second.is_unit() // Done first to avoid pointer indirection when it doesn't matter.
108        && first.has_name(OPTION_SOME_TAG)
109        && second.has_name(OPTION_NONE_TAG)
110    }
111
112    /// Check whether this sum type is a structural option type.
113    ///
114    /// A structural option type has `some(T)` as its first variant and `none` as its second.
115    /// That is, `{ some(T), none }` or `some: T | none` depending on your notation.
116    /// Note that `some` and `none` are lowercase, unlike Rust's `Option`.
117    /// Order matters, and an option type with these variants in the opposite order will not be recognized.
118    pub fn is_option(&self) -> bool {
119        self.as_option().is_some()
120    }
121
122    /// Return whether this sum type is empty, that is, has no variants.
123    pub fn is_empty(&self) -> bool {
124        self.variants.is_empty()
125    }
126
127    /// Return whether this sum type is the special `ScheduleAt` type,
128    /// `Interval(u64) | Time(u64)`.
129    /// Does not follow `Ref`s.
130    pub fn is_schedule_at(&self) -> bool {
131        match &*self.variants {
132            [first, second] => {
133                first.has_name(SCHEDULE_AT_INTERVAL_TAG)
134                    && first.algebraic_type.is_time_duration()
135                    && second.has_name(SCHEDULE_AT_TIME_TAG)
136                    && second.algebraic_type.is_timestamp()
137            }
138            _ => false,
139        }
140    }
141
142    /// Returns whether this sum type is a special known type, currently `Option` or `ScheduleAt`.
143    pub fn is_special(&self) -> bool {
144        self.is_option() || self.is_schedule_at()
145    }
146
147    /// Returns whether this sum type is like on in C without data attached to the variants.
148    pub fn is_simple_enum(&self) -> bool {
149        self.variants.iter().all(SumTypeVariant::is_unit)
150    }
151
152    /// Returns the sum type variant using `tag_name` with their tag position.
153    pub fn get_variant(&self, tag_name: &str) -> Option<(u8, &SumTypeVariant)> {
154        self.variants.iter().enumerate().find_map(|(pos, x)| {
155            if x.name.as_deref() == Some(tag_name) {
156                Some((pos as u8, x))
157            } else {
158                None
159            }
160        })
161    }
162
163    /// Returns the sum type variant using `tag_name` with their tag position, if this is a [Self::is_simple_enum]
164    pub fn get_variant_simple(&self, tag_name: &str) -> Option<(u8, &SumTypeVariant)> {
165        if self.is_simple_enum() {
166            self.get_variant(tag_name)
167        } else {
168            None
169        }
170    }
171
172    /// Returns the sum type variant with the given `tag`.
173    pub fn get_variant_by_tag(&self, tag: u8) -> Option<&SumTypeVariant> {
174        self.variants.get(tag as usize)
175    }
176}
177
178impl From<Box<[SumTypeVariant]>> for SumType {
179    fn from(fields: Box<[SumTypeVariant]>) -> Self {
180        SumType::new(fields)
181    }
182}
183impl<const N: usize> From<[SumTypeVariant; N]> for SumType {
184    fn from(fields: [SumTypeVariant; N]) -> Self {
185        SumType::new(fields.into())
186    }
187}
188impl<const N: usize> From<[(Option<&str>, AlgebraicType); N]> for SumType {
189    fn from(fields: [(Option<&str>, AlgebraicType); N]) -> Self {
190        fields.map(|(s, t)| SumTypeVariant::new(t, s.map(<_>::into))).into()
191    }
192}
193impl<const N: usize> From<[(&str, AlgebraicType); N]> for SumType {
194    fn from(fields: [(&str, AlgebraicType); N]) -> Self {
195        fields.map(|(s, t)| SumTypeVariant::new_named(t, s)).into()
196    }
197}
198impl<const N: usize> From<[AlgebraicType; N]> for SumType {
199    fn from(fields: [AlgebraicType; N]) -> Self {
200        fields.map(SumTypeVariant::from).into()
201    }
202}
203
204impl MetaType for SumType {
205    fn meta_type() -> AlgebraicType {
206        AlgebraicType::product([("variants", AlgebraicType::array(SumTypeVariant::meta_type()))])
207    }
208}
209
210impl SumType {
211    pub fn as_value(&self) -> AlgebraicValue {
212        value_serialize(self)
213    }
214
215    pub fn from_value(value: &AlgebraicValue) -> Result<SumType, ValueDeserializeError> {
216        Self::deserialize(ValueDeserializer::from_ref(value))
217    }
218}