Skip to main content

sim_shape/primitives/combinators/
list.rs

1//! Positional and variadic list-shape combinator.
2
3use std::{cmp::Ordering, sync::Arc};
4
5use sim_kernel::{Cx, Error, Expr, Result, Value, shape_is_subshape_of};
6
7use crate::base::{MatchScore, Shape, ShapeDoc, ShapeMatch};
8use crate::diagnostics::{expected_shape_diagnostic, expr_actual_label};
9
10/// A shape that matches a list positionally, with an optional rest shape.
11///
12/// The leading `items` shapes must match the list elements in order. Without a
13/// rest shape the list length must match exactly (a tuple); with one, every
14/// trailing element must match the rest shape (a variadic list).
15///
16/// # Examples
17///
18/// ```rust
19/// # use std::sync::Arc;
20/// use sim_kernel::{Cx, DefaultFactory, Expr, NoopEvalPolicy};
21/// use sim_shape::{ExprKind, ExprKindShape, ListShape, Shape};
22///
23/// let mut cx = Cx::new(Arc::new(NoopEvalPolicy), Arc::new(DefaultFactory));
24/// let shape = ListShape::new(vec![Arc::new(ExprKindShape::new(ExprKind::String))]);
25/// let expr = Expr::List(vec![Expr::String("hi".to_owned())]);
26/// assert!(shape.check_expr(&mut cx, &expr).unwrap().accepted);
27/// // an empty list rejects: the single positional item shape is unmatched.
28/// assert!(!shape.check_expr(&mut cx, &Expr::List(vec![])).unwrap().accepted);
29/// ```
30pub struct ListShape {
31    items: Vec<Arc<dyn Shape>>,
32    rest: Option<Arc<dyn Shape>>,
33}
34
35impl ListShape {
36    /// Build a fixed-length list shape over the given positional item shapes.
37    pub fn new(items: Vec<Arc<dyn Shape>>) -> Self {
38        Self { items, rest: None }
39    }
40
41    /// Build a fixed-length tuple shape; an alias for [`ListShape::new`].
42    pub fn tuple(items: Vec<Arc<dyn Shape>>) -> Self {
43        Self::new(items)
44    }
45
46    /// Build a list shape with leading items and a rest shape for the tail.
47    pub fn with_rest(items: Vec<Arc<dyn Shape>>, rest: Arc<dyn Shape>) -> Self {
48        Self {
49            items,
50            rest: Some(rest),
51        }
52    }
53
54    /// Build a variadic list shape; an alias for [`ListShape::with_rest`].
55    pub fn variadic(prefix: Vec<Arc<dyn Shape>>, rest: Arc<dyn Shape>) -> Self {
56        Self::with_rest(prefix, rest)
57    }
58
59    /// The positional item shapes.
60    pub fn items(&self) -> &[Arc<dyn Shape>] {
61        &self.items
62    }
63
64    /// The rest shape applied to trailing elements, if this list is variadic.
65    pub fn rest(&self) -> Option<&Arc<dyn Shape>> {
66        self.rest.as_ref()
67    }
68
69    fn check_list_value(&self, cx: &mut Cx, head: Value) -> Result<ShapeMatch> {
70        let min_len = self.items.len();
71        {
72            let Some(list) = head.object().as_list() else {
73                return Err(Error::Eval("expected a list value".to_owned()));
74            };
75            let len_cmp = list.len_cmp(cx, min_len)?;
76            if len_cmp == Ordering::Less {
77                return Ok(ShapeMatch::reject_with_diagnostic(
78                    expected_shape_diagnostic(
79                        format!("at least {min_len} list items"),
80                        "fewer list items",
81                    ),
82                ));
83            }
84            if self.rest.is_none() && len_cmp != Ordering::Equal {
85                return Ok(ShapeMatch::reject_with_diagnostic(
86                    expected_shape_diagnostic(format!("{min_len} list items"), "more list items"),
87                ));
88            }
89        }
90
91        let mut out = ShapeMatch::accept(MatchScore::exact(20));
92        // Walk the cdr chain from the head as runtime values, so the rest walk
93        // starts from the correct node even when there are no leading items.
94        let mut current = Some(head);
95
96        for shape in &self.items {
97            let Some(node_value) = current.clone() else {
98                return Ok(ShapeMatch::reject_with_diagnostic(
99                    expected_shape_diagnostic(
100                        format!("at least {min_len} list items"),
101                        "fewer list items",
102                    ),
103                ));
104            };
105            let Some(node) = node_value.object().as_list() else {
106                return Err(Error::Eval("list cdr did not yield a list".to_owned()));
107            };
108            let Some(item) = node.car(cx)? else {
109                return Ok(ShapeMatch::reject_with_diagnostic(
110                    expected_shape_diagnostic(
111                        format!("at least {min_len} list items"),
112                        "fewer list items",
113                    ),
114                ));
115            };
116            let next = node.cdr(cx)?;
117            let matched = shape.check_value(cx, item)?;
118            if !matched.accepted {
119                return Ok(matched);
120            }
121            out.captures.extend(matched.captures);
122            out.score += matched.score;
123            current = next;
124        }
125
126        let Some(rest) = &self.rest else {
127            return Ok(out);
128        };
129
130        // The expr path walks the tail to collect rest captures; the value path
131        // mirrors it so equivalent list values capture like expression forms.
132        // It still terminates on lazy tails once a total rest accepts an
133        // element and binds nothing, because further total non-binding matches
134        // cannot add captures.
135        while let Some(node_value) = current.clone() {
136            let Some(node) = node_value.object().as_list() else {
137                break;
138            };
139            if node.is_empty(cx)? {
140                break;
141            }
142            let Some(item) = node.car(cx)? else {
143                break;
144            };
145            let next = node.cdr(cx)?;
146            let matched = rest.check_value(cx, item)?;
147            if !matched.accepted {
148                return Ok(matched);
149            }
150            let bound_nothing =
151                matched.captures.values().is_empty() && matched.captures.exprs().is_empty();
152            out.captures.extend(matched.captures);
153            out.score += matched.score;
154            current = next;
155            if rest.is_total() && bound_nothing {
156                break;
157            }
158        }
159
160        Ok(out)
161    }
162}
163
164impl Shape for ListShape {
165    fn is_effectful(&self) -> bool {
166        self.items.iter().any(|shape| shape.is_effectful())
167            || self.rest.as_ref().is_some_and(|shape| shape.is_effectful())
168    }
169
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}