Skip to main content

sim_shape/primitives/
object.rs

1//! Object-grammar shapes: `ObjectExpr`, `FieldSpec`, and `FieldShape` for
2//! matching structured object expressions field by field.
3
4use std::sync::Arc;
5
6use crate::base::{MatchScore, Shape, ShapeDoc, ShapeMatch};
7use crate::duplicate_keys::{reject_duplicate_expr_symbol_keys, reject_duplicate_symbol_keys};
8use sim_kernel::{Cx, Expr, Result, Symbol, Value};
9
10/// The decoded form of an object expression: a class symbol and its fields.
11///
12/// A view over the `expr/object` extension form, letting object-grammar shapes
13/// read a structured object's class and named field expressions without
14/// re-walking the raw `Expr`.
15#[derive(Clone, Debug, PartialEq, Eq)]
16pub struct ObjectExpr {
17    /// The class this object form is tagged with.
18    pub class: Symbol,
19    /// The object's named fields, in source order.
20    pub fields: Vec<(Symbol, Expr)>,
21}
22
23impl ObjectExpr {
24    /// Encode this object back into its `expr/object` extension expression.
25    pub fn to_expr(self) -> Expr {
26        Expr::Extension {
27            tag: Symbol::qualified("expr", "object"),
28            payload: Box::new(Expr::Map(vec![
29                (Expr::Symbol(Symbol::new("class")), Expr::Symbol(self.class)),
30                (
31                    Expr::Symbol(Symbol::new("fields")),
32                    Expr::Map(
33                        self.fields
34                            .into_iter()
35                            .map(|(key, value)| (Expr::Symbol(key), value))
36                            .collect(),
37                    ),
38                ),
39            ])),
40        }
41    }
42
43    /// Decode an `expr/object` extension expression, or `None` if it is not a
44    /// well-formed object expression.
45    pub fn parse(expr: &Expr) -> Option<Self> {
46        Self::parse_checked(expr).ok().flatten()
47    }
48
49    pub(crate) fn parse_checked(expr: &Expr) -> Result<Option<Self>> {
50        let Expr::Extension { tag, payload } = expr else {
51            return Ok(None);
52        };
53        if *tag != Symbol::qualified("expr", "object") {
54            return Ok(None);
55        }
56        let Expr::Map(entries) = payload.as_ref() else {
57            return Ok(None);
58        };
59        reject_duplicate_expr_symbol_keys(entries, "shape-object")?;
60        let mut class = None;
61        let mut fields = None;
62        for (key, value) in entries {
63            let Expr::Symbol(key) = key else {
64                continue;
65            };
66            if *key == Symbol::new("class") {
67                if let Expr::Symbol(symbol) = value {
68                    class = Some(symbol.clone());
69                }
70            } else if *key == Symbol::new("fields")
71                && let Expr::Map(entries) = value
72            {
73                reject_duplicate_expr_symbol_keys(entries, "shape-object fields")?;
74                let parsed = entries
75                    .iter()
76                    .map(|(field, value)| match field {
77                        Expr::Symbol(symbol) => Some((symbol.clone(), value.clone())),
78                        _ => None,
79                    })
80                    .collect::<Option<Vec<_>>>();
81                fields = parsed;
82            }
83        }
84        let Some(class) = class else {
85            return Ok(None);
86        };
87        let Some(fields) = fields else {
88            return Ok(None);
89        };
90        Ok(Some(Self { class, fields }))
91    }
92
93    /// The expression bound to the named field, if present.
94    pub fn field(&self, name: &Symbol) -> Option<&Expr> {
95        self.fields
96            .iter()
97            .find_map(|(field, value)| (field == name).then_some(value))
98    }
99}
100
101/// A single field requirement within a [`FieldShape`]: a name, a shape, and
102/// whether the field must be present.
103pub struct FieldSpec {
104    pub(crate) name: Symbol,
105    pub(crate) shape: Arc<dyn Shape>,
106    pub(crate) required: bool,
107}
108
109impl FieldSpec {
110    /// Build a required field spec binding `name` to `shape`.
111    pub fn required(name: Symbol, shape: Arc<dyn Shape>) -> Self {
112        Self {
113            name,
114            shape,
115            required: true,
116        }
117    }
118
119    /// The field name.
120    pub fn name(&self) -> &Symbol {
121        &self.name
122    }
123
124    /// The shape the field's value must match.
125    pub fn shape(&self) -> &Arc<dyn Shape> {
126        &self.shape
127    }
128}
129
130/// A shape that matches an object form field by field.
131///
132/// Each [`FieldSpec`] checks the matching field's value; required fields must
133/// be present. When bound to a class the object's class must match; when
134/// anonymous, plain `Map` expressions match as well.
135pub struct FieldShape {
136    class: Option<Symbol>,
137    fields: Vec<FieldSpec>,
138}
139
140impl FieldShape {
141    /// Build a field shape that requires the object's class to be `class`.
142    pub fn new(class: Symbol, fields: Vec<FieldSpec>) -> Self {
143        Self {
144            class: Some(class),
145            fields,
146        }
147    }
148
149    /// Build a class-free field shape that also accepts plain map expressions.
150    pub fn anonymous(fields: Vec<FieldSpec>) -> Self {
151        Self {
152            class: None,
153            fields,
154        }
155    }
156
157    /// The required class symbol, or `None` for an anonymous field shape.
158    pub fn class_symbol(&self) -> Option<&Symbol> {
159        self.class.as_ref()
160    }
161
162    /// The field specs this shape checks.
163    pub fn fields(&self) -> &[FieldSpec] {
164        &self.fields
165    }
166
167    fn match_entries(
168        &self,
169        cx: &mut Cx,
170        class: Option<&Symbol>,
171        entries: &[(Symbol, Expr)],
172        context: &str,
173    ) -> Result<ShapeMatch> {
174        match reject_duplicate_symbol_keys(entries, context) {
175            Ok(()) => {}
176            Err(sim_kernel::Error::Eval(message)) => return Ok(ShapeMatch::reject(message)),
177            Err(err) => return Err(err),
178        }
179
180        if let Some(expected) = &self.class
181            && class != Some(expected)
182        {
183            return Ok(ShapeMatch::reject(format!("expected class {}", expected)));
184        }
185
186        let mut matched = ShapeMatch::accept(MatchScore::exact(20));
187        for spec in &self.fields {
188            let Some(value) = entries
189                .iter()
190                .find_map(|(name, value)| (name == &spec.name).then_some(value))
191            else {
192                if spec.required {
193                    return Ok(ShapeMatch::reject(format!("missing field {}", spec.name)));
194                }
195                continue;
196            };
197            let field_match = spec.shape.check_expr(cx, value)?;
198            if !field_match.accepted {
199                return Ok(field_match);
200            }
201            matched.captures.extend(field_match.captures);
202            matched.score += field_match.score;
203        }
204        Ok(matched)
205    }
206}
207
208impl Shape for FieldShape {
209    fn is_effectful(&self) -> bool {
210        self.fields.iter().any(|field| field.shape.is_effectful())
211    }
212
213    fn check_value(&self, cx: &mut Cx, value: Value) -> Result<ShapeMatch> {
214        let expr = value.object().as_expr(cx)?;
215        self.check_expr(cx, &expr)
216    }
217
218    fn check_expr(&self, cx: &mut Cx, expr: &Expr) -> Result<ShapeMatch> {
219        match ObjectExpr::parse_checked(expr) {
220            Ok(Some(object)) => {
221                return self.match_entries(
222                    cx,
223                    Some(&object.class),
224                    &object.fields,
225                    "shape-object fields",
226                );
227            }
228            Ok(None) => {}
229            Err(sim_kernel::Error::Eval(message)) => return Ok(ShapeMatch::reject(message)),
230            Err(err) => return Err(err),
231        }
232        if self.class.is_none()
233            && let Expr::Map(entries) = expr
234        {
235            let entries = entries
236                .iter()
237                .map(|(key, value)| match key {
238                    Expr::Symbol(symbol) => Some((symbol.clone(), value.clone())),
239                    _ => None,
240                })
241                .collect::<Option<Vec<_>>>();
242            if let Some(entries) = entries {
243                return self.match_entries(cx, None, &entries, "shape-fields");
244            }
245        }
246        Ok(ShapeMatch::reject("expected object fields"))
247    }
248
249    fn describe(&self, cx: &mut Cx) -> Result<ShapeDoc> {
250        let mut doc = match &self.class {
251            Some(class) => ShapeDoc::new(format!("fields {}", class)),
252            None => ShapeDoc::new("fields"),
253        };
254        for spec in &self.fields {
255            let detail = spec.shape.describe(cx)?;
256            doc = doc.with_detail(format!("{}: {}", spec.name, detail.name));
257        }
258        Ok(doc)
259    }
260}