Skip to main content

sim_relation_core/
domain.rs

1use crate::{DomainId, ToRelationDatum};
2use sim_kernel::{Datum, NumberLiteral, Ref, Symbol};
3use std::{collections::BTreeMap, fmt};
4
5/// Exact physical representation requested from a storage provider.
6#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
7pub enum StorageRepr {
8    /// Boolean bit.
9    Bool,
10    /// Signed 64-bit integer.
11    I64,
12    /// Finite IEEE-754 binary64 value.
13    F64,
14    /// UTF-8 text.
15    Text,
16    /// Arbitrary bytes.
17    Bytes,
18}
19
20/// An exact value at the provider boundary.
21#[derive(Clone, Debug, PartialEq)]
22pub enum StorageValue {
23    /// Boolean value.
24    Bool(bool),
25    /// Signed integer value.
26    I64(i64),
27    /// Finite float value (negative zero is normalized).
28    F64(f64),
29    /// Text value.
30    Text(String),
31    /// Byte value.
32    Bytes(Vec<u8>),
33}
34
35/// A semantic promise made by a logical domain.
36#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
37pub enum DomainTrait {
38    /// Equality is supported.
39    Equatable,
40    /// Total ordering is supported.
41    Ordered,
42}
43
44/// Domain validation or conversion failure.
45#[derive(Clone, Debug, PartialEq, Eq)]
46pub enum DomainError {
47    /// Two specs used the same id.
48    DuplicateId(DomainId),
49    /// A Shape reference cannot be resolved durably.
50    InvalidShapeRef,
51    /// Traits contradict one another.
52    IncoherentTraits,
53    /// A storage value used the wrong representation.
54    StorageMismatch,
55    /// A floating value was not finite.
56    NonFiniteFloat,
57    /// A Datum did not exactly represent this base domain.
58    DatumMismatch,
59}
60impl fmt::Display for DomainError {
61    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62        write!(f, "{self:?}")
63    }
64}
65impl std::error::Error for DomainError {}
66
67/// An open logical-domain declaration.
68#[derive(Clone, Debug, PartialEq, Eq)]
69pub struct DomainSpec {
70    id: DomainId,
71    storage: StorageRepr,
72    shape: Ref,
73    traits: Vec<DomainTrait>,
74}
75impl DomainSpec {
76    /// Constructs and validates a domain declaration.
77    pub fn new(
78        id: DomainId,
79        storage: StorageRepr,
80        shape: Ref,
81        traits: impl IntoIterator<Item = DomainTrait>,
82    ) -> Result<Self, DomainError> {
83        if matches!(shape, Ref::Handle(_) | Ref::Coord(_)) {
84            return Err(DomainError::InvalidShapeRef);
85        }
86        let mut traits: Vec<_> = traits.into_iter().collect();
87        traits.sort();
88        traits.dedup();
89        if traits.contains(&DomainTrait::Ordered) && !traits.contains(&DomainTrait::Equatable) {
90            return Err(DomainError::IncoherentTraits);
91        }
92        Ok(Self {
93            id,
94            storage,
95            shape,
96            traits,
97        })
98    }
99    /// Returns the domain id.
100    pub fn id(&self) -> &DomainId {
101        &self.id
102    }
103    /// Returns its physical storage representation.
104    pub const fn storage(&self) -> StorageRepr {
105        self.storage
106    }
107    /// Returns its unresolved Shape reference.
108    pub const fn shape(&self) -> &Ref {
109        &self.shape
110    }
111    /// Returns its normalized semantic traits.
112    pub fn traits(&self) -> &[DomainTrait] {
113        &self.traits
114    }
115}
116
117/// A validated collection of open domain declarations.
118#[derive(Clone, Debug, Default, PartialEq, Eq)]
119pub struct DomainCatalog(BTreeMap<DomainId, DomainSpec>);
120impl DomainCatalog {
121    /// Validates and builds a catalog.
122    pub fn new(specs: impl IntoIterator<Item = DomainSpec>) -> Result<Self, DomainError> {
123        let mut map = BTreeMap::new();
124        for spec in specs {
125            let id = spec.id.clone();
126            if map.insert(id.clone(), spec).is_some() {
127                return Err(DomainError::DuplicateId(id));
128            }
129        }
130        Ok(Self(map))
131    }
132    /// Looks up a declaration without provider-specific dispatch.
133    pub fn get(&self, id: &DomainId) -> Option<&DomainSpec> {
134        self.0.get(id)
135    }
136    /// Iterates in stable domain-id order.
137    pub fn iter(&self) -> impl Iterator<Item = &DomainSpec> {
138        self.0.values()
139    }
140}
141
142impl ToRelationDatum for DomainCatalog {
143    fn to_datum(&self) -> Datum {
144        Datum::Node {
145            tag: Symbol::qualified("relation", "domain-catalog"),
146            fields: vec![(
147                Symbol::new("domains"),
148                Datum::Vector(self.iter().map(ToRelationDatum::to_datum).collect()),
149            )],
150        }
151    }
152}
153
154/// The five portable base domains.
155#[derive(Clone, Copy, Debug, PartialEq, Eq)]
156pub enum BaseDomain {
157    /// Boolean.
158    Bool,
159    /// Signed 64-bit integer.
160    I64,
161    /// Finite binary64 float.
162    F64,
163    /// UTF-8 text.
164    Text,
165    /// Bytes.
166    Bytes,
167}
168impl BaseDomain {
169    /// Returns the stable open domain id.
170    pub fn id(self) -> DomainId {
171        DomainId::new(Symbol::qualified(
172            "relation",
173            match self {
174                Self::Bool => "bool",
175                Self::I64 => "i64",
176                Self::F64 => "f64",
177                Self::Text => "text",
178                Self::Bytes => "bytes",
179            },
180        ))
181        .expect("built-in id")
182    }
183    /// Returns the complete built-in declaration, suitable for any catalog.
184    pub fn spec(self) -> DomainSpec {
185        let storage = match self {
186            Self::Bool => StorageRepr::Bool,
187            Self::I64 => StorageRepr::I64,
188            Self::F64 => StorageRepr::F64,
189            Self::Text => StorageRepr::Text,
190            Self::Bytes => StorageRepr::Bytes,
191        };
192        DomainSpec::new(
193            self.id(),
194            storage,
195            Ref::Symbol(Symbol::qualified(
196                "relation",
197                match self {
198                    Self::Bool => "BoolShape",
199                    Self::I64 => "I64Shape",
200                    Self::F64 => "FiniteF64Shape",
201                    Self::Text => "TextShape",
202                    Self::Bytes => "BytesShape",
203                },
204            )),
205            [DomainTrait::Equatable, DomainTrait::Ordered],
206        )
207        .expect("built-in domain declaration is coherent")
208    }
209    /// Converts a provider value into its exact kernel datum.
210    pub fn to_datum(self, value: StorageValue) -> Result<Datum, DomainError> {
211        match (self, value) {
212            (Self::Bool, StorageValue::Bool(v)) => Ok(Datum::Bool(v)),
213            (Self::I64, StorageValue::I64(v)) => Ok(Datum::Number(NumberLiteral {
214                domain: Symbol::qualified("core", "i64"),
215                canonical: v.to_string(),
216            })),
217            (Self::F64, StorageValue::F64(v)) if v.is_finite() => {
218                let v = if v == 0.0 { 0.0 } else { v };
219                Ok(Datum::Number(NumberLiteral {
220                    domain: Symbol::qualified("core", "f64"),
221                    canonical: v.to_string(),
222                }))
223            }
224            (Self::F64, StorageValue::F64(_)) => Err(DomainError::NonFiniteFloat),
225            (Self::Text, StorageValue::Text(v)) => Ok(Datum::String(v)),
226            (Self::Bytes, StorageValue::Bytes(v)) => Ok(Datum::Bytes(v)),
227            _ => Err(DomainError::StorageMismatch),
228        }
229    }
230    /// Converts an exact kernel datum back to the provider representation.
231    pub fn from_datum(self, datum: &Datum) -> Result<StorageValue, DomainError> {
232        match (self, datum) {
233            (Self::Bool, Datum::Bool(v)) => Ok(StorageValue::Bool(*v)),
234            (Self::I64, Datum::Number(v)) if v.domain == Symbol::qualified("core", "i64") => v
235                .canonical
236                .parse()
237                .map(StorageValue::I64)
238                .map_err(|_| DomainError::DatumMismatch),
239            (Self::F64, Datum::Number(v)) if v.domain == Symbol::qualified("core", "f64") => {
240                let n: f64 = v
241                    .canonical
242                    .parse()
243                    .map_err(|_| DomainError::DatumMismatch)?;
244                if !n.is_finite() {
245                    Err(DomainError::NonFiniteFloat)
246                } else {
247                    Ok(StorageValue::F64(if n == 0.0 { 0.0 } else { n }))
248                }
249            }
250            (Self::Text, Datum::String(v)) => Ok(StorageValue::Text(v.clone())),
251            (Self::Bytes, Datum::Bytes(v)) => Ok(StorageValue::Bytes(v.clone())),
252            _ => Err(DomainError::DatumMismatch),
253        }
254    }
255}
256
257impl ToRelationDatum for DomainSpec {
258    fn to_datum(&self) -> Datum {
259        Datum::Node {
260            tag: Symbol::qualified("relation", "domain"),
261            fields: vec![
262                (Symbol::new("id"), Datum::Symbol(self.id.symbol().clone())),
263                (
264                    Symbol::new("storage"),
265                    Datum::Symbol(Symbol::qualified(
266                        "relation",
267                        match self.storage {
268                            StorageRepr::Bool => "bool",
269                            StorageRepr::I64 => "i64",
270                            StorageRepr::F64 => "f64",
271                            StorageRepr::Text => "text",
272                            StorageRepr::Bytes => "bytes",
273                        },
274                    )),
275                ),
276                (Symbol::new("shape"), ref_datum(&self.shape)),
277                (
278                    Symbol::new("traits"),
279                    Datum::Vector(
280                        self.traits
281                            .iter()
282                            .map(|v| {
283                                Datum::Symbol(Symbol::qualified(
284                                    "relation",
285                                    match v {
286                                        DomainTrait::Equatable => "equatable",
287                                        DomainTrait::Ordered => "ordered",
288                                    },
289                                ))
290                            })
291                            .collect(),
292                    ),
293                ),
294            ],
295        }
296    }
297}
298fn ref_datum(value: &Ref) -> Datum {
299    match value {
300        Ref::Symbol(v) => Datum::Node {
301            tag: Symbol::qualified("core", "ref-symbol"),
302            fields: vec![(Symbol::new("symbol"), Datum::Symbol(v.clone()))],
303        },
304        Ref::Content(v) => Datum::Node {
305            tag: Symbol::qualified("core", "ref-content"),
306            fields: vec![
307                (Symbol::new("algorithm"), Datum::Symbol(v.algorithm.clone())),
308                (Symbol::new("bytes"), Datum::Bytes(v.bytes.to_vec())),
309            ],
310        },
311        _ => unreachable!("validated durable ref"),
312    }
313}