Skip to main content

sim_citizen/
field.rs

1//! The CitizenField trait for encoding and decoding citizen field values.
2
3use std::convert::TryFrom;
4
5use sim_kernel::{CapabilityName, Cx, Error, Expr, NumberLiteral, Result, Symbol, Value};
6use sim_value::kind::expr_kind;
7
8/// Encodes and decodes a Rust type as one citizen constructor field.
9///
10/// Generated and hand-written citizens use this trait to project each field to
11/// an `Expr` for the constructor encoding and to recover it on read-construct.
12/// The kernel owns `Expr`/`Value`/`Cx`; sim-citizen provides the field codec
13/// and the standard scalar/collection implementations.
14///
15/// # Examples
16///
17/// The expr-level encode/decode pair round-trips without a context:
18///
19/// ```
20/// # use sim_citizen::CitizenField;
21/// let original: Vec<i64> = vec![1, 2, 3];
22/// let expr = original.encode_field();
23/// let decoded = Vec::<i64>::decode_field_expr(&expr, "numbers").unwrap();
24/// assert_eq!(decoded, original);
25/// ```
26pub trait CitizenField: Sized {
27    /// Encodes the field value as its constructor `Expr`.
28    fn encode_field(&self) -> Expr;
29    /// Decodes the field from a constructor `Expr`, naming `field` in errors.
30    fn decode_field_expr(expr: &Expr, field: &'static str) -> Result<Self>;
31
32    /// Decodes the field from a runtime [`Value`] via its `Expr` projection.
33    ///
34    /// Default implementation projects `value` to an `Expr` through `cx` and
35    /// delegates to [`CitizenField::decode_field_expr`].
36    fn decode_field_value(cx: &mut Cx, value: Value, field: &'static str) -> Result<Self> {
37        let expr = value_to_expr(cx, value, field)?;
38        Self::decode_field_expr(&expr, field)
39    }
40}
41
42/// Projects a runtime [`Value`] to an `Expr`, tagging errors with `field`.
43pub fn value_to_expr(cx: &mut Cx, value: Value, field: &'static str) -> Result<Expr> {
44    value
45        .object()
46        .as_expr(cx)
47        .map_err(|err| field_error(field, format!("cannot convert value to Expr: {err}")))
48}
49
50/// Builds a runtime [`Value`] from an `Expr` using `cx`'s factory.
51///
52/// Reconstructs literals, symbols, lists, and symbol-keyed maps, falling back
53/// to the factory's generic `expr` path for other shapes. Used to turn citizen
54/// constructor arguments back into values for read-construct.
55pub fn value_from_expr(cx: &mut Cx, expr: &Expr) -> Result<Value> {
56    match expr {
57        Expr::Nil => cx.factory().nil(),
58        Expr::Bool(value) => cx.factory().bool(*value),
59        Expr::Number(value) => cx
60            .factory()
61            .number_literal(value.domain.clone(), value.canonical.clone()),
62        Expr::Symbol(value) => cx.factory().symbol(value.clone()),
63        Expr::String(value) => cx.factory().string(value.clone()),
64        Expr::Bytes(value) => cx.factory().bytes(value.clone()),
65        Expr::List(items) => {
66            let values = items
67                .iter()
68                .map(|item| value_from_expr(cx, item))
69                .collect::<Result<Vec<_>>>()?;
70            cx.factory().list(values)
71        }
72        Expr::Map(entries) => {
73            let entries = entries
74                .iter()
75                .map(|(key, value)| {
76                    let Expr::Symbol(key) = key else {
77                        return Err(field_error(
78                            "map",
79                            format!("expected symbol key, found {}", expr_kind(key)),
80                        ));
81                    };
82                    Ok((key.clone(), value_from_expr(cx, value)?))
83                })
84                .collect::<Result<Vec<_>>>()?;
85            cx.factory().table(entries)
86        }
87        other => cx.factory().expr(other.clone()),
88    }
89}
90
91/// Checks that a citizen's version argument equals the `expected` version.
92///
93/// Citizen constructors carry a leading `v<N>` version symbol; this verifies it
94/// matches, returning an error naming `class` otherwise.
95pub fn decode_version(cx: &mut Cx, value: Value, expected: u32, class: Symbol) -> Result<()> {
96    let expected_symbol = Symbol::new(format!("v{expected}"));
97    match value_to_expr(cx, value, "version")? {
98        Expr::Symbol(actual) if actual == expected_symbol => Ok(()),
99        other => Err(Error::Eval(format!(
100            "citizen {class} expects version {expected_symbol}, found {}",
101            expr_kind(&other)
102        ))),
103    }
104}
105
106/// Builds the standard error for a wrong read-constructor argument count.
107pub fn arity_error(class: Symbol, expected: usize, actual: usize) -> Error {
108    Error::Eval(format!(
109        "citizen {class} expects {expected} read-constructor argument(s), found {actual}"
110    ))
111}
112
113/// Builds a `citizen field <field>: <message>` evaluation error.
114pub fn field_error(field: &'static str, message: impl Into<String>) -> Error {
115    Error::Eval(format!("citizen field {field}: {}", message.into()))
116}
117
118macro_rules! signed_int_field {
119    ($($ty:ty),* $(,)?) => {
120        $(
121            impl CitizenField for $ty {
122                fn encode_field(&self) -> Expr {
123                    int_expr(self.to_string())
124                }
125
126                fn decode_field_expr(expr: &Expr, field: &'static str) -> Result<Self> {
127                    let value = decode_integer_text(expr, field)?;
128                    <$ty>::try_from(value).map_err(|_| {
129                        field_error(field, format!("integer {value} is out of range"))
130                    })
131                }
132            }
133        )*
134    };
135}
136
137macro_rules! unsigned_int_field {
138    ($($ty:ty),* $(,)?) => {
139        $(
140            impl CitizenField for $ty {
141                fn encode_field(&self) -> Expr {
142                    int_expr(self.to_string())
143                }
144
145                fn decode_field_expr(expr: &Expr, field: &'static str) -> Result<Self> {
146                    let value = decode_integer_text(expr, field)?;
147                    <$ty>::try_from(value).map_err(|_| {
148                        field_error(field, format!("integer {value} is out of range"))
149                    })
150                }
151            }
152        )*
153    };
154}
155
156signed_int_field!(i8, i16, i32, i64, i128, isize);
157unsigned_int_field!(u8, u16, u32, u64, usize);
158
159impl CitizenField for bool {
160    fn encode_field(&self) -> Expr {
161        Expr::Bool(*self)
162    }
163
164    fn decode_field_expr(expr: &Expr, field: &'static str) -> Result<Self> {
165        match expr {
166            Expr::Bool(value) => Ok(*value),
167            other => Err(field_error(
168                field,
169                format!("expected bool, found {}", expr_kind(other)),
170            )),
171        }
172    }
173}
174
175impl CitizenField for String {
176    fn encode_field(&self) -> Expr {
177        Expr::String(self.clone())
178    }
179
180    fn decode_field_expr(expr: &Expr, field: &'static str) -> Result<Self> {
181        match expr {
182            Expr::String(value) => Ok(value.clone()),
183            other => Err(field_error(
184                field,
185                format!("expected string, found {}", expr_kind(other)),
186            )),
187        }
188    }
189}
190
191impl CitizenField for Symbol {
192    fn encode_field(&self) -> Expr {
193        Expr::Symbol(self.clone())
194    }
195
196    fn decode_field_expr(expr: &Expr, field: &'static str) -> Result<Self> {
197        match expr {
198            Expr::Symbol(value) => Ok(value.clone()),
199            other => Err(field_error(
200                field,
201                format!("expected symbol, found {}", expr_kind(other)),
202            )),
203        }
204    }
205}
206
207impl CitizenField for Expr {
208    fn encode_field(&self) -> Expr {
209        self.clone()
210    }
211
212    fn decode_field_expr(expr: &Expr, _field: &'static str) -> Result<Self> {
213        Ok(expr.clone())
214    }
215}
216
217impl CitizenField for CapabilityName {
218    fn encode_field(&self) -> Expr {
219        Expr::String(self.as_str().to_owned())
220    }
221
222    fn decode_field_expr(expr: &Expr, field: &'static str) -> Result<Self> {
223        match expr {
224            Expr::String(value) => Ok(CapabilityName::new(value.clone())),
225            other => Err(field_error(
226                field,
227                format!(
228                    "expected string capability name, found {}",
229                    expr_kind(other)
230                ),
231            )),
232        }
233    }
234}
235
236impl CitizenField for f64 {
237    fn encode_field(&self) -> Expr {
238        Expr::Number(NumberLiteral {
239            domain: Symbol::qualified("numbers", "f64"),
240            canonical: canonical_f64(*self),
241        })
242    }
243
244    fn decode_field_expr(expr: &Expr, field: &'static str) -> Result<Self> {
245        match expr {
246            Expr::Number(number) => number
247                .canonical
248                .parse::<f64>()
249                .map_err(|err| field_error(field, format!("invalid f64: {err}"))),
250            other => Err(field_error(
251                field,
252                format!("expected number, found {}", expr_kind(other)),
253            )),
254        }
255    }
256}
257
258impl<A, B> CitizenField for (A, B)
259where
260    A: CitizenField,
261    B: CitizenField,
262{
263    fn encode_field(&self) -> Expr {
264        Expr::List(vec![self.0.encode_field(), self.1.encode_field()])
265    }
266
267    fn decode_field_expr(expr: &Expr, field: &'static str) -> Result<Self> {
268        let Expr::List(items) = expr else {
269            return Err(field_error(
270                field,
271                format!("expected pair list, found {}", expr_kind(expr)),
272            ));
273        };
274        let [first, second] = items.as_slice() else {
275            return Err(field_error(
276                field,
277                format!("expected pair list with 2 item(s), found {}", items.len()),
278            ));
279        };
280        Ok((
281            A::decode_field_expr(first, field)?,
282            B::decode_field_expr(second, field)?,
283        ))
284    }
285}
286
287impl<T> CitizenField for Vec<T>
288where
289    T: CitizenField,
290{
291    fn encode_field(&self) -> Expr {
292        Expr::List(self.iter().map(CitizenField::encode_field).collect())
293    }
294
295    fn decode_field_expr(expr: &Expr, field: &'static str) -> Result<Self> {
296        match expr {
297            Expr::List(items) => items
298                .iter()
299                .map(|item| T::decode_field_expr(item, field))
300                .collect(),
301            other => Err(field_error(
302                field,
303                format!("expected list, found {}", expr_kind(other)),
304            )),
305        }
306    }
307}
308
309impl<T> CitizenField for Option<T>
310where
311    T: CitizenField,
312{
313    fn encode_field(&self) -> Expr {
314        self.as_ref()
315            .map(CitizenField::encode_field)
316            .unwrap_or(Expr::Nil)
317    }
318
319    fn decode_field_expr(expr: &Expr, field: &'static str) -> Result<Self> {
320        match expr {
321            Expr::Nil => Ok(None),
322            other => T::decode_field_expr(other, field).map(Some),
323        }
324    }
325}
326
327fn int_expr(canonical: String) -> Expr {
328    Expr::Number(NumberLiteral {
329        domain: Symbol::qualified("citizen", "int"),
330        canonical,
331    })
332}
333
334fn decode_integer_text(expr: &Expr, field: &'static str) -> Result<i128> {
335    match expr {
336        Expr::Number(number) => number
337            .canonical
338            .parse::<i128>()
339            .map_err(|err| field_error(field, format!("invalid integer: {err}"))),
340        other => Err(field_error(
341            field,
342            format!("expected integer number, found {}", expr_kind(other)),
343        )),
344    }
345}
346
347fn canonical_f64(value: f64) -> String {
348    if value.is_nan() {
349        "NaN".to_owned()
350    } else if value == f64::INFINITY {
351        "inf".to_owned()
352    } else if value == f64::NEG_INFINITY {
353        "-inf".to_owned()
354    } else {
355        value.to_string()
356    }
357}