cljrs_runtime/interp/destructure.rs
1//! Sequential and associative destructuring for `let*`, `fn*`, and `loop*`.
2
3use crate::builtins::form::form_to_value;
4use cljrs_gc::GcPtr;
5use cljrs_reader::Form;
6use cljrs_reader::form::FormKind;
7use cljrs_value::{Keyword, PersistentList, Symbol, Value, ValueError, ValueResult};
8use std::sync::Arc;
9
10use crate::env::env::Env;
11use crate::env::error::{EvalError, EvalResult};
12
13/// Bind a destructuring pattern `pattern` against `val` in `env`.
14///
15/// Supports:
16/// - Plain symbol binding
17/// - `[a b]` sequential destructuring (recursive)
18pub fn bind_pattern(pattern: &Form, val: Value, env: &mut Env) -> EvalResult<()> {
19 match &pattern.kind {
20 FormKind::Symbol(s) => {
21 // Plain binding, including `_`.
22 env.bind(Arc::from(s.as_str()), val);
23 Ok(())
24 }
25 FormKind::Vector(forms) => bind_sequential(forms, &val, env),
26 FormKind::Map(forms) => bind_associative(forms, &val, env),
27 // A `^long x` (etc.) binding carries a primitive type hint that the
28 // tree-walk interpreter ignores; bind the underlying pattern.
29 FormKind::Meta(_, inner) => bind_pattern(inner, val, env),
30 _ => Err(EvalError::Runtime(format!(
31 "unsupported binding pattern: {:?}",
32 pattern.kind
33 ))),
34 }
35}
36
37/// Bind a sequential destructuring pattern against `val`.
38///
39/// Grammar of `pattern`:
40/// ```text
41/// [sym* (& rest)? (:as alias)?]
42/// ```
43pub fn bind_sequential(pattern: &[Form], val: &Value, env: &mut Env) -> EvalResult<()> {
44 let items = value_to_seq_vec(val).map_err(crate::env::error::value_error_to_eval_error)?;
45 let mut idx = 0usize;
46 let mut i = 0usize;
47
48 while i < pattern.len() {
49 let p = &pattern[i];
50
51 // `&` introduces a rest binding.
52 if matches!(&p.kind, FormKind::Symbol(s) if s == "&") {
53 i += 1;
54 let rest_pat = pattern
55 .get(i)
56 .ok_or_else(|| EvalError::Runtime("& in destructuring requires a name".into()))?;
57 let rest_list = if idx < items.len() {
58 let rest_vals: Vec<Value> = items[idx..].to_vec();
59 Value::List(GcPtr::new(PersistentList::from_iter(rest_vals)))
60 } else {
61 Value::Nil
62 };
63 bind_pattern(rest_pat, rest_list, env)?;
64 i += 1;
65 // Skip optional `:as` after rest.
66 if i < pattern.len()
67 && let FormKind::Keyword(k) = &pattern[i].kind
68 && k == "as"
69 {
70 i += 1;
71 let alias = pattern
72 .get(i)
73 .ok_or_else(|| EvalError::Runtime(":as requires a name".into()))?;
74 bind_pattern(alias, val.clone(), env)?;
75 }
76 break;
77 }
78
79 // `:as` alias — must be last.
80 if let FormKind::Keyword(k) = &p.kind
81 && k == "as"
82 {
83 i += 1;
84 let alias = pattern
85 .get(i)
86 .ok_or_else(|| EvalError::Runtime(":as requires a name".into()))?;
87 bind_pattern(alias, val.clone(), env)?;
88 break;
89 }
90
91 // Normal positional binding.
92 let item = items.get(idx).cloned().unwrap_or(Value::Nil);
93 bind_pattern(p, item, env)?;
94 idx += 1;
95 i += 1;
96 }
97
98 Ok(())
99}
100
101/// Convert a seqable Value to its elements, propagating lazy realization errors.
102/// Non-seqable values retain the historical empty-vector result.
103pub fn value_to_seq_vec(val: &Value) -> ValueResult<Vec<Value>> {
104 // Realization can park or collect. The original chain retains both the
105 // accumulated elements and each thunk's cached result throughout the walk.
106 let _root = crate::env::gc_roots::root_value(val);
107 let mut result = Vec::new();
108 let mut current = val.clone();
109 loop {
110 match current.unwrap_meta() {
111 Value::Nil => break,
112 Value::LazySeq(ls) => {
113 current = ls.get().try_realize().map_err(ValueError::Other)?;
114 }
115 Value::Cons(c) => {
116 result.push(c.get().head.clone());
117 current = c.get().tail.clone();
118 }
119 Value::List(l) => {
120 result.extend(l.get().iter().cloned());
121 break;
122 }
123 Value::Vector(v) => {
124 result.extend(v.get().iter().cloned());
125 break;
126 }
127 Value::Set(s) => {
128 result.extend(s.iter().cloned());
129 break;
130 }
131 Value::Map(m) => {
132 m.for_each(|k, v| result.push(Value::map_entry(k.clone(), v.clone())));
133 break;
134 }
135 _ => break,
136 }
137 }
138 Ok(result)
139}
140
141// ── Associative destructuring ─────────────────────────────────────────────────
142
143/// Bind a map destructuring pattern against `val` in `env`.
144///
145/// `pattern` is a flat `[key val key val ...]` slice from `FormKind::Map`.
146///
147/// Supports:
148/// - `:keys [a b c]` — bind symbols from keyword keys `:a`, `:b`, `:c`
149/// - `:strs [a b]` — bind symbols from string keys `"a"`, `"b"`
150/// - `:syms [a b]` — bind symbols from symbol keys `'a`, `'b`
151/// - `:as name` — bind the whole value to `name`
152/// - `:or {a default}` — default value for missing keys
153/// - Regular `{sym :key}` direct bindings
154pub fn bind_associative(pattern: &[Form], val: &Value, env: &mut Env) -> EvalResult<()> {
155 // First pass: collect :or defaults.
156 // The default is an EXPRESSION, held unevaluated until its symbol is bound.
157 // Storing `form_to_value` here is what made `:or {rules default-rules}` bind
158 // the SYMBOL `default-rules`; and evaluating every default up front would
159 // break `{:keys [a b] :or {b (inc a)}}`, which Clojure supports because it
160 // emits one `(get m :k default)` per symbol, in binding order.
161 let mut defaults: std::collections::HashMap<String, Form> = std::collections::HashMap::new();
162 let mut i = 0;
163 while i + 1 < pattern.len() {
164 let k = &pattern[i];
165 let v = &pattern[i + 1];
166 if let FormKind::Keyword(kw) = &k.kind
167 && kw == "or"
168 {
169 // v is a map literal {sym default ...}
170 if let FormKind::Map(or_forms) = &v.kind {
171 let mut j = 0;
172 while j + 1 < or_forms.len() {
173 if let FormKind::Symbol(sym) = &or_forms[j].kind {
174 defaults.insert(sym.clone(), or_forms[j + 1].clone());
175 }
176 j += 2;
177 }
178 }
179 }
180 i += 2;
181 }
182
183 let get_val = |key: &Value| -> Value {
184 match val.unwrap_meta() {
185 Value::Map(m) => m.get(key).unwrap_or(Value::Nil),
186 _ => Value::Nil,
187 }
188 };
189
190 let mut i = 0;
191 while i + 1 < pattern.len() {
192 let k = &pattern[i];
193 let v = &pattern[i + 1];
194 i += 2;
195
196 // A `:keys`/`:strs`/`:syms` keyword may itself be namespace-qualified
197 // (`:person/keys [a b]`), supplying a default namespace for every
198 // unqualified symbol in its vector; an individual symbol's own
199 // namespace (`ui/dest`) takes precedence over that default.
200 let directive_ns = if let FormKind::Keyword(kw) = &k.kind {
201 Keyword::parse(kw).namespace
202 } else {
203 None
204 };
205 let directive_name = if let FormKind::Keyword(kw) = &k.kind {
206 Keyword::parse(kw).name.to_string()
207 } else {
208 String::new()
209 };
210
211 match &k.kind {
212 FormKind::Keyword(_) if directive_name == "keys" => {
213 if let FormKind::Vector(syms) = &v.kind {
214 for sym_form in syms {
215 if let FormKind::Symbol(sym) = &sym_form.kind {
216 let parsed = Symbol::parse(sym);
217 let key =
218 match parsed.namespace.clone().or_else(|| directive_ns.clone()) {
219 Some(ns) => {
220 Value::keyword(Keyword::qualified(ns, parsed.name.clone()))
221 }
222 None => Value::keyword(Keyword::simple(parsed.name.clone())),
223 };
224 let mut bound_val = get_val(&key);
225 if let Some(d) = defaults.get(parsed.name.as_ref()) {
226 // Evaluated whether or not the key was present:
227 // Clojure's `(get m :k default)` evaluates its
228 // third argument eagerly, so a throwing or
229 // side-effecting default must still fire here.
230 let dv = crate::interp::eval::eval(&d.clone(), env)?;
231 if matches!(bound_val, Value::Nil) {
232 bound_val = dv;
233 }
234 }
235 env.bind(parsed.name.clone(), bound_val);
236 }
237 }
238 }
239 }
240 FormKind::Keyword(kw) if kw == "strs" => {
241 if let FormKind::Vector(syms) = &v.kind {
242 for sym_form in syms {
243 if let FormKind::Symbol(sym) = &sym_form.kind {
244 let key = Value::string(sym.clone());
245 let mut bound_val = get_val(&key);
246 if let Some(d) = defaults.get(sym.as_str()) {
247 // Evaluated whether or not the key was present:
248 // Clojure's `(get m :k default)` evaluates its
249 // third argument eagerly, so a throwing or
250 // side-effecting default must still fire here.
251 let dv = crate::interp::eval::eval(&d.clone(), env)?;
252 if matches!(bound_val, Value::Nil) {
253 bound_val = dv;
254 }
255 }
256 env.bind(Arc::from(sym.as_str()), bound_val);
257 }
258 }
259 }
260 }
261 FormKind::Keyword(_) if directive_name == "syms" => {
262 if let FormKind::Vector(syms) = &v.kind {
263 for sym_form in syms {
264 if let FormKind::Symbol(sym) = &sym_form.kind {
265 let parsed = Symbol::parse(sym);
266 let key =
267 match parsed.namespace.clone().or_else(|| directive_ns.clone()) {
268 Some(ns) => {
269 Value::symbol(Symbol::qualified(ns, parsed.name.clone()))
270 }
271 None => Value::symbol(Symbol::simple(parsed.name.clone())),
272 };
273 let mut bound_val = get_val(&key);
274 if let Some(d) = defaults.get(parsed.name.as_ref()) {
275 // Evaluated whether or not the key was present:
276 // Clojure's `(get m :k default)` evaluates its
277 // third argument eagerly, so a throwing or
278 // side-effecting default must still fire here.
279 let dv = crate::interp::eval::eval(&d.clone(), env)?;
280 if matches!(bound_val, Value::Nil) {
281 bound_val = dv;
282 }
283 }
284 env.bind(parsed.name.clone(), bound_val);
285 }
286 }
287 }
288 }
289 FormKind::Keyword(kw) if kw == "as" => {
290 if let FormKind::Symbol(sym) = &v.kind {
291 env.bind(Arc::from(sym.as_str()), val.clone());
292 }
293 }
294 FormKind::Keyword(kw) if kw == "or" => {
295 // Already processed in the first pass.
296 }
297 _ => {
298 // Regular {binding-form lookup-key} pair.
299 // In Clojure map destructuring {a :x}, the key position is the
300 // binding target and the value position is the lookup key.
301 let lookup_key = form_to_value(v)?;
302 let mut bound_val = get_val(&lookup_key);
303 // Apply defaults for simple symbol bindings.
304 if let FormKind::Symbol(sym) = &k.kind
305 && let Some(d) = defaults.get(sym.as_str())
306 {
307 let dv = crate::interp::eval::eval(&d.clone(), env)?;
308 if matches!(bound_val, Value::Nil) {
309 bound_val = dv;
310 }
311 }
312 // Bind via pattern to support nested destructuring.
313 bind_pattern(k, bound_val, env)?;
314 }
315 }
316 }
317 Ok(())
318}