datum/
atom.rs

1/*
2 * datum-rs - Quick to implement S-expression format
3 * Written starting in 2024 by contributors (see CREDITS.txt at repository's root)
4 * To the extent possible under law, the author(s) have dedicated all copyright and related and neighboring rights to this software to the public domain worldwide. This software is distributed without any warranty.
5 * A copy of the Unlicense should have been supplied as COPYING.txt in this repository. Alternatively, you can find it at <https://unlicense.org/>.
6 */
7
8use core::{
9    convert::TryFrom,
10    fmt::{Display, Write},
11    hash::Hash,
12    ops::Deref,
13};
14
15use crate::{datum_error, DatumError, DatumResult, DatumToken};
16
17/// Atomic Datum AST value.
18/// This enum also contains the functions that convert between tokens and atoms.
19/// You can think of it as the bridge between Datum's tokenization model and value model.
20/// Accordingly, it does not contain offsets.
21///
22/// Implements [Hash] despite potentially containing floats; if this is a problem for your application then don't use the [Hash] implementation.
23#[derive(Clone, Copy, PartialEq, PartialOrd, Debug)]
24pub enum DatumAtom<B: Deref<Target = str>> {
25    String(B),
26    Symbol(B),
27    Integer(i64),
28    Float(f64),
29    Boolean(bool),
30    Nil,
31}
32
33impl<B: Deref<Target = str>> Default for DatumAtom<B> {
34    fn default() -> Self {
35        Self::Nil
36    }
37}
38
39impl<B: Default + Deref<Target = str>> TryFrom<DatumToken<B>> for DatumAtom<B> {
40    type Error = DatumError;
41
42    /// Tries to convert from a DatumToken.
43    /// Due to the strings involved, this has to be done via ownership transfer.
44    fn try_from(token: DatumToken<B>) -> DatumResult<DatumAtom<B>> {
45        match token {
46            DatumToken::String(_, b) => Ok(DatumAtom::String(b)),
47            DatumToken::Symbol(_, b) => Ok(DatumAtom::Symbol(b)),
48            DatumToken::SpecialID(at, b) => {
49                if b.eq_ignore_ascii_case("t") {
50                    Ok(DatumAtom::Boolean(true))
51                } else if b.eq_ignore_ascii_case("f") {
52                    Ok(DatumAtom::Boolean(false))
53                } else if b.eq_ignore_ascii_case("nil") {
54                    Ok(DatumAtom::Nil)
55                } else if b.eq("{}#") {
56                    Ok(DatumAtom::Symbol(B::default()))
57                } else if b.eq_ignore_ascii_case("i+nan.0") {
58                    Ok(DatumAtom::Float(f64::NAN))
59                } else if b.eq_ignore_ascii_case("i+inf.0") {
60                    Ok(DatumAtom::Float(f64::INFINITY))
61                } else if b.eq_ignore_ascii_case("i-inf.0") {
62                    Ok(DatumAtom::Float(f64::NEG_INFINITY))
63                } else if b.starts_with('x') || b.starts_with('X') {
64                    let res = i64::from_str_radix(&b[1..], 16);
65                    if let Ok(v) = res {
66                        Ok(DatumAtom::Integer(v))
67                    } else {
68                        Err(datum_error!(BadData, at, "invalid hex integer"))
69                    }
70                } else {
71                    Err(datum_error!(BadData, at, "invalid special ID"))
72                }
73            }
74            DatumToken::Integer(_, v) => Ok(DatumAtom::Integer(v)),
75            DatumToken::Float(_, v) => Ok(DatumAtom::Float(v)),
76            _ => Err(datum_error!(
77                BadData,
78                token.offset(),
79                "token not atomizable"
80            )),
81        }
82    }
83}
84
85impl<B: Deref<Target = str>> DatumAtom<B> {
86    /// Writes a value from the atom.
87    pub fn write(&self, f: &mut dyn Write) -> core::fmt::Result {
88        match &self {
89            DatumAtom::String(v) => DatumToken::String(0, v.deref()).write(f),
90            DatumAtom::Symbol(v) => DatumToken::Symbol(0, v.deref()).write(f),
91            DatumAtom::Integer(v) => {
92                let v: DatumToken<&'static str> = DatumToken::Integer(0, *v);
93                v.write(f)
94            }
95            DatumAtom::Float(v) => {
96                let v: DatumToken<&'static str> = DatumToken::Float(0, *v);
97                v.write(f)
98            }
99            DatumAtom::Boolean(true) => f.write_str("#t"),
100            DatumAtom::Boolean(false) => f.write_str("#f"),
101            DatumAtom::Nil => f.write_str("#nil"),
102        }
103    }
104}
105
106impl<B: Deref<Target = str>> Display for DatumAtom<B> {
107    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
108        self.write(f)
109    }
110}
111
112impl<B: Deref<Target = str>> Hash for DatumAtom<B> {
113    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
114        // **Notice: The 'type ID namespace' is shared with DatumValue.**
115        // Specifically, type 6 goes to lists.
116        match self {
117            Self::String(s) => {
118                state.write_u8(0);
119                s.hash(state)
120            }
121            Self::Symbol(s) => {
122                state.write_u8(1);
123                s.hash(state)
124            }
125            Self::Integer(i) => {
126                state.write_u8(2);
127                i.hash(state)
128            }
129            Self::Float(f) => {
130                state.write_u8(3);
131                f.to_bits().hash(state)
132            }
133            Self::Boolean(b) => {
134                state.write_u8(4);
135                b.hash(state)
136            }
137            Self::Nil => state.write_u8(5),
138        }
139    }
140}
141
142macro_rules! as_x_result {
143    ($caller:ident, $callee:ident, $type:ty) => {
144        #[doc = concat!("Wraps [DatumMayContainAtom::", stringify!($callee), "] to return [Result], using the given error generator.")]
145        fn $caller<E, F: FnOnce() -> E>(&self, err: F) -> Result<$type, E> {
146            match self.$callee() {
147                Some(v) => Ok(v),
148                None => Err(err())
149            }
150        }
151    };
152}
153
154macro_rules! as_x {
155    ($name:ident, $name_result:ident, $variant:ident, $type:ty) => {
156        #[doc = concat!("Attempts to retrieve [DatumAtom::", stringify!($variant), "], else [None].")]
157        fn $name(&self) -> Option<$type> {
158            if let Some(DatumAtom::$variant(res)) = self.as_atom() {
159                Some(res)
160            } else {
161                None
162            }
163        }
164        as_x_result!($name_result, $name, $type);
165    };
166    ($name:ident, $name_result:ident, $variant:ident, $type:ty, $adjust:tt) => {
167        #[doc = concat!("Attempts to retrieve [DatumAtom::", stringify!($variant), "], else [None].")]
168        fn $name(&self) -> Option<$type> {
169            if let Some(DatumAtom::$variant(res)) = self.as_atom() {
170                Some($adjust res)
171            } else {
172                None
173            }
174        }
175        as_x_result!($name_result, $name, $type);
176    };
177}
178
179/// Implemented by structs which may contain an Atom to give easy access to said Atom.
180pub trait DatumMayContainAtom<B: Deref<Target = str>> {
181    /// Retrieves a reference to the possible interior [DatumAtom].
182    fn as_atom(&self) -> Option<&DatumAtom<B>>;
183    as_x_result!(as_atom_result, as_atom, &DatumAtom<B>);
184    as_x!(as_str, as_str_result, String, &B);
185    as_x!(as_sym, as_sym_result, Symbol, &B);
186    as_x!(as_i64, as_i64_result, Integer, i64, *);
187    as_x!(as_f64, as_f64_result, Float, f64, *);
188    /// From the interior [DatumAtom] (if any), retrieves the float (if it is), else [None].
189    /// This version will cast integers to floats if necessary.
190    fn as_number(&self) -> Option<f64> {
191        if let Some(res) = self.as_atom() {
192            if let DatumAtom::Float(res) = res {
193                Some(*res)
194            } else if let DatumAtom::Integer(res) = res {
195                Some(*res as f64)
196            } else {
197                None
198            }
199        } else {
200            None
201        }
202    }
203    as_x_result!(as_number_result, as_number, f64);
204    as_x!(as_bool, as_bool_result, Boolean, bool, *);
205    /// From the interior [DatumAtom] (if any), returns [Some] if a nil value, else [None].
206    fn as_nil(&self) -> Option<()> {
207        if let Some(DatumAtom::Nil) = self.as_atom() {
208            Some(())
209        } else {
210            None
211        }
212    }
213    as_x_result!(as_nil_result, as_nil, ());
214}
215
216impl<B: Deref<Target = str>> DatumMayContainAtom<B> for DatumAtom<B> {
217    fn as_atom(&self) -> Option<&DatumAtom<B>> {
218        Some(self)
219    }
220}