Skip to main content

sim_shape/primitives/
combinators.rs

1//! Primitive combinators: `ListShape`, `CaptureShape`, `OneOfShape`, the
2//! `ShapeExprParser`-driven `PrattShape`, and `EffectfulShape`.
3
4use std::{cmp::Ordering, sync::Arc};
5
6use sim_kernel::{Cx, Error, Expr, Result, ShapeId, ShapeRef, Value, shape_is_subshape_of};
7
8use crate::base::{Bindings, MatchScore, Shape, ShapeDoc, ShapeMatch};
9use crate::diagnostics::{
10    binding_failure_diagnostic, expected_shape_diagnostic, expr_actual_label,
11};
12
13/// A shape that matches a list positionally, with an optional rest shape.
14///
15/// The leading `items` shapes must match the list elements in order. Without a
16/// rest shape the list length must match exactly (a tuple); with one, every
17/// trailing element must match the rest shape (a variadic list).
18///
19/// # Examples
20///
21/// ```rust
22/// # use std::sync::Arc;
23/// use sim_kernel::{Cx, DefaultFactory, Expr, NoopEvalPolicy};
24/// use sim_shape::{ExprKind, ExprKindShape, ListShape, Shape};
25///
26/// let mut cx = Cx::new(Arc::new(NoopEvalPolicy), Arc::new(DefaultFactory));
27/// let shape = ListShape::new(vec![Arc::new(ExprKindShape::new(ExprKind::String))]);
28/// let expr = Expr::List(vec![Expr::String("hi".to_owned())]);
29/// assert!(shape.check_expr(&mut cx, &expr).unwrap().accepted);
30/// // an empty list rejects: the single positional item shape is unmatched.
31/// assert!(!shape.check_expr(&mut cx, &Expr::List(vec![])).unwrap().accepted);
32/// ```
33pub struct ListShape {
34    items: Vec<Arc<dyn Shape>>,
35    rest: Option<Arc<dyn Shape>>,
36}
37
38impl ListShape {
39    /// Build a fixed-length list shape over the given positional item shapes.
40    pub fn new(items: Vec<Arc<dyn Shape>>) -> Self {
41        Self { items, rest: None }
42    }
43
44    /// Build a fixed-length tuple shape; an alias for [`ListShape::new`].
45    pub fn tuple(items: Vec<Arc<dyn Shape>>) -> Self {
46        Self::new(items)
47    }
48
49    /// Build a list shape with leading items and a rest shape for the tail.
50    pub fn with_rest(items: Vec<Arc<dyn Shape>>, rest: Arc<dyn Shape>) -> Self {
51        Self {
52            items,
53            rest: Some(rest),
54        }
55    }
56
57    /// Build a variadic list shape; an alias for [`ListShape::with_rest`].
58    pub fn variadic(prefix: Vec<Arc<dyn Shape>>, rest: Arc<dyn Shape>) -> Self {
59        Self::with_rest(prefix, rest)
60    }
61
62    /// The positional item shapes.
63    pub fn items(&self) -> &[Arc<dyn Shape>] {
64        &self.items
65    }
66
67    /// The rest shape applied to trailing elements, if this list is variadic.
68    pub fn rest(&self) -> Option<&Arc<dyn Shape>> {
69        self.rest.as_ref()
70    }
71
72    fn check_list_value(&self, cx: &mut Cx, head: Value) -> Result<ShapeMatch> {
73        let min_len = self.items.len();
74        {
75            let Some(list) = head.object().as_list() else {
76                return Err(Error::Eval("expected a list value".to_owned()));
77            };
78            let len_cmp = list.len_cmp(cx, min_len)?;
79            if len_cmp == Ordering::Less {
80                return Ok(ShapeMatch::reject_with_diagnostic(
81                    expected_shape_diagnostic(
82                        format!("at least {min_len} list items"),
83                        "fewer list items",
84                    ),
85                ));
86            }
87            if self.rest.is_none() && len_cmp != Ordering::Equal {
88                return Ok(ShapeMatch::reject_with_diagnostic(
89                    expected_shape_diagnostic(format!("{min_len} list items"), "more list items"),
90                ));
91            }
92        }
93
94        let mut out = ShapeMatch::accept(MatchScore::exact(20));
95        // Walk the cdr chain from the head as runtime values, so the rest walk
96        // starts from the correct node even when there are no leading items.
97        let mut current = Some(head);
98
99        for shape in &self.items {
100            let Some(node_value) = current.clone() else {
101                return Ok(ShapeMatch::reject_with_diagnostic(
102                    expected_shape_diagnostic(
103                        format!("at least {min_len} list items"),
104                        "fewer list items",
105                    ),
106                ));
107            };
108            let Some(node) = node_value.object().as_list() else {
109                return Err(Error::Eval("list cdr did not yield a list".to_owned()));
110            };
111            let Some(item) = node.car(cx)? else {
112                return Ok(ShapeMatch::reject_with_diagnostic(
113                    expected_shape_diagnostic(
114                        format!("at least {min_len} list items"),
115                        "fewer list items",
116                    ),
117                ));
118            };
119            let next = node.cdr(cx)?;
120            let matched = shape.check_value(cx, item)?;
121            if !matched.accepted {
122                return Ok(matched);
123            }
124            out.captures.extend(matched.captures);
125            out.score += matched.score;
126            current = next;
127        }
128
129        let Some(rest) = &self.rest else {
130            return Ok(out);
131        };
132
133        // The expr path always walks the tail to collect rest captures; the
134        // value path must do the same, so an equivalent list value captures
135        // identically to its expression form. The old `is_total()` shortcut
136        // returned here and dropped those bindings. We instead walk, but stop
137        // once a total rest accepts an element while binding nothing: further
138        // total, non-binding matches add no captures, so this keeps an
139        // unbounded (lazy or endless) tail terminating without losing fidelity.
140        while let Some(node_value) = current.clone() {
141            let Some(node) = node_value.object().as_list() else {
142                break;
143            };
144            if node.is_empty(cx)? {
145                break;
146            }
147            let Some(item) = node.car(cx)? else {
148                break;
149            };
150            let next = node.cdr(cx)?;
151            let matched = rest.check_value(cx, item)?;
152            if !matched.accepted {
153                return Ok(matched);
154            }
155            let bound_nothing =
156                matched.captures.values().is_empty() && matched.captures.exprs().is_empty();
157            out.captures.extend(matched.captures);
158            out.score += matched.score;
159            current = next;
160            if rest.is_total() && bound_nothing {
161                break;
162            }
163        }
164
165        Ok(out)
166    }
167}
168
169impl Shape for ListShape {
170    fn is_subshape_of(&self, cx: &mut Cx, parent: &dyn Shape) -> Result<Option<bool>> {
171        let Some(parent) = parent.as_any().downcast_ref::<Self>() else {
172            return Ok(None);
173        };
174        // Conservative structural covariance: prove the subshape relation only
175        // when both lists share their fixed arity and rest presence and every
176        // corresponding item (and the rest) is itself a proven subshape. Any
177        // uncertainty stays `None` so the engine never asserts a false
178        // relation; this is what lets dispatch prefer a structurally more
179        // specific list overload over a general one.
180        if self.items.len() != parent.items.len() {
181            return Ok(None);
182        }
183        match (&self.rest, &parent.rest) {
184            (None, None) => {}
185            (Some(child_rest), Some(parent_rest)) => {
186                if !shape_is_subshape_of(cx, child_rest.as_ref(), parent_rest.as_ref())? {
187                    return Ok(None);
188                }
189            }
190            _ => return Ok(None),
191        }
192        for (child_item, parent_item) in self.items.iter().zip(parent.items.iter()) {
193            if !shape_is_subshape_of(cx, child_item.as_ref(), parent_item.as_ref())? {
194                return Ok(None);
195            }
196        }
197        Ok(Some(true))
198    }
199
200    fn check_value(&self, cx: &mut Cx, value: Value) -> Result<ShapeMatch> {
201        if value.object().as_list().is_none() {
202            let expr = value.object().as_expr(cx)?;
203            return self.check_expr(cx, &expr);
204        }
205        self.check_list_value(cx, value)
206    }
207
208    fn check_expr(&self, cx: &mut Cx, expr: &Expr) -> Result<ShapeMatch> {
209        let Expr::List(items) = expr else {
210            return Ok(ShapeMatch::reject_with_diagnostic(
211                expected_shape_diagnostic("list expression", expr_actual_label(expr)),
212            ));
213        };
214
215        if items.len() < self.items.len() {
216            return Ok(ShapeMatch::reject_with_diagnostic(
217                expected_shape_diagnostic(
218                    format!("at least {} list items", self.items.len()),
219                    format!("{} list items", items.len()),
220                ),
221            ));
222        }
223
224        if self.rest.is_none() && items.len() != self.items.len() {
225            return Ok(ShapeMatch::reject_with_diagnostic(
226                expected_shape_diagnostic(
227                    format!("{} list items", self.items.len()),
228                    format!("{} list items", items.len()),
229                ),
230            ));
231        }
232
233        let mut out = ShapeMatch::accept(MatchScore::exact(20));
234
235        for (shape, item) in self.items.iter().zip(items.iter()) {
236            let matched = shape.check_expr(cx, item)?;
237            if !matched.accepted {
238                return Ok(matched);
239            }
240            out.captures.extend(matched.captures);
241            out.score += matched.score;
242        }
243
244        if let Some(rest) = &self.rest {
245            for item in items.iter().skip(self.items.len()) {
246                let matched = rest.check_expr(cx, item)?;
247                if !matched.accepted {
248                    return Ok(matched);
249                }
250                out.captures.extend(matched.captures);
251                out.score += matched.score;
252            }
253        }
254
255        Ok(out)
256    }
257
258    fn describe(&self, cx: &mut Cx) -> Result<ShapeDoc> {
259        let mut doc = ShapeDoc::new("list shape");
260        for item in &self.items {
261            doc = doc.with_detail(item.describe(cx)?.name);
262        }
263        if self.rest.is_some() {
264            doc = doc.with_detail("rest".to_owned());
265        }
266        Ok(doc)
267    }
268}
269
270/// A shape that wraps an inner shape and binds the match under a name.
271///
272/// When the inner shape accepts, the matched expression (and value, where the
273/// inner shape is not total) is recorded in the match captures under `name`,
274/// feeding shape-driven binding.
275pub struct CaptureShape {
276    name: sim_kernel::Symbol,
277    inner: Arc<dyn Shape>,
278}
279
280impl CaptureShape {
281    /// Build a capture that binds the inner shape's match under `name`.
282    pub fn new(name: sim_kernel::Symbol, inner: Arc<dyn Shape>) -> Self {
283        Self { name, inner }
284    }
285
286    /// The capture name bound on a successful match.
287    pub fn name(&self) -> &sim_kernel::Symbol {
288        &self.name
289    }
290
291    /// The inner shape whose match is captured.
292    pub fn inner(&self) -> &Arc<dyn Shape> {
293        &self.inner
294    }
295}
296
297impl Shape for CaptureShape {
298    fn parents(&self, _cx: &mut Cx) -> Result<Vec<ShapeRef>> {
299        Ok(vec![crate::functions::shape_value(
300            sim_kernel::Symbol::qualified("shape-capture-parent", self.name.to_string()),
301            self.inner.clone(),
302        )])
303    }
304
305    fn is_subshape_of(&self, cx: &mut Cx, parent: &dyn Shape) -> Result<Option<bool>> {
306        shape_is_subshape_of(cx, self.inner.as_ref(), parent).map(Some)
307    }
308
309    fn is_total(&self) -> bool {
310        self.inner.is_total()
311    }
312
313    fn check_value(&self, cx: &mut Cx, value: Value) -> Result<ShapeMatch> {
314        let mut matched = self.inner.check_value(cx, value.clone())?;
315        if matched.accepted {
316            matched.captures.bind_value(self.name.clone(), value);
317            if !self.inner.is_total()
318                && let Ok(expr) = matched
319                    .captures
320                    .values()
321                    .last()
322                    .expect("just bound value capture")
323                    .1
324                    .object()
325                    .as_expr(cx)
326            {
327                matched.captures.bind_expr(self.name.clone(), expr);
328            }
329        } else {
330            matched.diagnostics.insert(
331                0,
332                binding_failure_diagnostic(&self.name, "captured value", "rejected value"),
333            );
334        }
335        Ok(matched)
336    }
337
338    fn check_expr(&self, cx: &mut Cx, expr: &Expr) -> Result<ShapeMatch> {
339        let mut matched = self.inner.check_expr(cx, expr)?;
340        if matched.accepted {
341            matched.captures.bind_expr(self.name.clone(), expr.clone());
342        } else {
343            matched.diagnostics.insert(
344                0,
345                binding_failure_diagnostic(
346                    &self.name,
347                    "captured expression",
348                    expr_actual_label(expr),
349                ),
350            );
351        }
352        Ok(matched)
353    }
354
355    fn describe(&self, cx: &mut Cx) -> Result<ShapeDoc> {
356        let inner = self.inner.describe(cx)?;
357        Ok(ShapeDoc::new(format!("capture {}", self.name)).with_detail(inner.name))
358    }
359}
360
361/// A shape that matches if any one of its choice shapes matches.
362///
363/// Choices are tried in order and the first accepting match wins; if none
364/// accept, the rejection collects every choice's diagnostics. Total if any
365/// choice is total.
366pub struct OneOfShape {
367    choices: Vec<Arc<dyn Shape>>,
368}
369
370impl OneOfShape {
371    /// Build an alternation over the given choice shapes.
372    pub fn new(choices: Vec<Arc<dyn Shape>>) -> Self {
373        Self { choices }
374    }
375
376    /// The choice shapes tried in order.
377    pub fn choices(&self) -> &[Arc<dyn Shape>] {
378        &self.choices
379    }
380}
381
382impl Shape for OneOfShape {
383    fn is_total(&self) -> bool {
384        self.choices.iter().any(|choice| choice.is_total())
385    }
386
387    fn is_subshape_of(&self, cx: &mut Cx, parent: &dyn Shape) -> Result<Option<bool>> {
388        for choice in &self.choices {
389            if !shape_is_subshape_of(cx, choice.as_ref(), parent)? {
390                return Ok(Some(false));
391            }
392        }
393        Ok(Some(true))
394    }
395
396    fn check_value(&self, cx: &mut Cx, value: Value) -> Result<ShapeMatch> {
397        let mut diagnostics = Vec::new();
398        for choice in &self.choices {
399            let matched = choice.check_value(cx, value.clone())?;
400            if matched.accepted {
401                return Ok(matched);
402            }
403            diagnostics.extend(matched.diagnostics);
404        }
405        Ok(ShapeMatch {
406            accepted: false,
407            captures: Bindings::new(),
408            score: MatchScore::reject(),
409            diagnostics,
410        })
411    }
412
413    fn check_expr(&self, cx: &mut Cx, expr: &Expr) -> Result<ShapeMatch> {
414        let mut diagnostics = Vec::new();
415        for choice in &self.choices {
416            let matched = choice.check_expr(cx, expr)?;
417            if matched.accepted {
418                return Ok(matched);
419            }
420            diagnostics.extend(matched.diagnostics);
421        }
422        Ok(ShapeMatch {
423            accepted: false,
424            captures: Bindings::new(),
425            score: MatchScore::reject(),
426            diagnostics,
427        })
428    }
429
430    fn describe(&self, cx: &mut Cx) -> Result<ShapeDoc> {
431        let mut doc = ShapeDoc::new("one-of shape");
432        for choice in &self.choices {
433            doc = doc.with_detail(choice.describe(cx)?.name);
434        }
435        Ok(doc)
436    }
437}
438
439/// A parser that turns shape source text into an [`Expr`] for [`PrattShape`].
440///
441/// Implementations plug a concrete grammar (the codec layer, not the kernel)
442/// into shape matching; `parse_expr` should report a parse failure as
443/// `Error::Eval` so the shape rejects rather than aborts.
444pub trait ShapeExprParser: Send + Sync {
445    /// A short human-readable name for this parser, used in shape descriptions.
446    fn label(&self) -> &str;
447
448    /// Parse source text into an expression to match against the inner shape.
449    fn parse_expr(&self, source: &str) -> Result<Expr>;
450}
451
452/// A shape that parses a string expression with a [`ShapeExprParser`] before
453/// matching.
454///
455/// Accepts only `String` expressions: the text is parsed and the resulting
456/// expression is checked against the inner shape. Non-string inputs and parse
457/// failures reject.
458pub struct PrattShape {
459    parser: Arc<dyn ShapeExprParser>,
460    inner: Arc<dyn Shape>,
461}
462
463impl PrattShape {
464    /// Build a shape that parses string input with `parser`, then checks `inner`.
465    pub fn new(parser: Arc<dyn ShapeExprParser>, inner: Arc<dyn Shape>) -> Self {
466        Self { parser, inner }
467    }
468
469    /// The parser applied to string input.
470    pub fn parser(&self) -> &Arc<dyn ShapeExprParser> {
471        &self.parser
472    }
473
474    /// The inner shape checked against the parsed expression.
475    pub fn inner(&self) -> &Arc<dyn Shape> {
476        &self.inner
477    }
478}
479
480impl Shape for PrattShape {
481    fn check_value(&self, cx: &mut Cx, value: Value) -> Result<ShapeMatch> {
482        let expr = value.object().as_expr(cx)?;
483        self.check_expr(cx, &expr)
484    }
485
486    fn check_expr(&self, cx: &mut Cx, expr: &Expr) -> Result<ShapeMatch> {
487        let source = match expr {
488            Expr::String(text) => text.as_str(),
489            _ => {
490                return Ok(ShapeMatch::reject(
491                    "expected string expression for Pratt parse",
492                ));
493            }
494        };
495        let parsed = match self.parser.parse_expr(source) {
496            Ok(parsed) => parsed,
497            Err(Error::Eval(message)) => return Ok(ShapeMatch::reject(message)),
498            Err(other) => return Err(other),
499        };
500        self.inner.check_expr(cx, &parsed)
501    }
502
503    fn describe(&self, cx: &mut Cx) -> Result<ShapeDoc> {
504        let inner = self.inner.describe(cx)?;
505        Ok(ShapeDoc::new("pratt shape")
506            .with_detail(self.parser.label().to_owned())
507            .with_detail(inner.name))
508    }
509}
510
511/// A shape that marks its inner shape's matching as effectful.
512///
513/// Delegates checks to the inner shape but reports `is_effectful() == true` and
514/// carries no shape id, signalling that parse-time validation through this
515/// shape needs a trusted parser position.
516pub struct EffectfulShape {
517    inner: Arc<dyn Shape>,
518}
519
520impl EffectfulShape {
521    /// Wrap an inner shape, marking its matching as effectful.
522    pub fn new(inner: Arc<dyn Shape>) -> Self {
523        Self { inner }
524    }
525
526    /// The wrapped inner shape.
527    pub fn inner(&self) -> &Arc<dyn Shape> {
528        &self.inner
529    }
530}
531
532impl Shape for EffectfulShape {
533    fn id(&self) -> Option<ShapeId> {
534        None
535    }
536
537    fn is_effectful(&self) -> bool {
538        true
539    }
540
541    fn is_total(&self) -> bool {
542        self.inner.is_total()
543    }
544
545    fn is_subshape_of(&self, cx: &mut Cx, parent: &dyn Shape) -> Result<Option<bool>> {
546        let Some(parent) = parent.as_any().downcast_ref::<Self>() else {
547            return Ok(Some(false));
548        };
549        shape_is_subshape_of(cx, self.inner.as_ref(), parent.inner.as_ref()).map(Some)
550    }
551
552    fn check_value(&self, cx: &mut Cx, value: Value) -> Result<ShapeMatch> {
553        self.inner.check_value(cx, value)
554    }
555
556    fn check_expr(&self, cx: &mut Cx, expr: &Expr) -> Result<ShapeMatch> {
557        self.inner.check_expr(cx, expr)
558    }
559
560    fn describe(&self, cx: &mut Cx) -> Result<ShapeDoc> {
561        let inner = self.inner.describe(cx)?;
562        Ok(
563            ShapeDoc::new(format!("effectful {}", inner.name)).with_detail(
564                "effectful parse-time validation requires a trusted parser position".to_owned(),
565            ),
566        )
567    }
568}