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}