sim_shape/primitives/combinators/
list.rs1use 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
10pub struct ListShape {
31 items: Vec<Arc<dyn Shape>>,
32 rest: Option<Arc<dyn Shape>>,
33}
34
35impl ListShape {
36 pub fn new(items: Vec<Arc<dyn Shape>>) -> Self {
38 Self { items, rest: None }
39 }
40
41 pub fn tuple(items: Vec<Arc<dyn Shape>>) -> Self {
43 Self::new(items)
44 }
45
46 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 pub fn variadic(prefix: Vec<Arc<dyn Shape>>, rest: Arc<dyn Shape>) -> Self {
56 Self::with_rest(prefix, rest)
57 }
58
59 pub fn items(&self) -> &[Arc<dyn Shape>] {
61 &self.items
62 }
63
64 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 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 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 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}