Skip to main content

cljrs_runtime/builtins/
form.rs

1// ── form_to_value ─────────────────────────────────────────────────────────────
2
3use crate::env::error::{EvalError, EvalResult};
4use cljrs_gc::GcPtr;
5use cljrs_reader::{Form, FormKind};
6use cljrs_value::regex::Pattern;
7use cljrs_value::value::SetValue;
8use cljrs_value::{
9    Keyword, MapValue, PersistentHashSet, PersistentList, PersistentVector, Symbol, Value,
10};
11
12// ── anon fn expansion ─────────────────────────────────────────────────────────
13
14/// Expand `#(...)` to `(fn* [p__1 p__2 ... & rest__] ...)`.
15pub fn expand_anon_fn(body: &[Form], span: cljrs_types::span::Span) -> Form {
16    let mut max_pos: usize = 0;
17    let mut has_rest = false;
18    find_pct_refs(body, &mut max_pos, &mut has_rest);
19
20    let s = &span;
21    let mut params: Vec<Form> = (1..=max_pos)
22        .map(|i| Form::new(FormKind::Symbol(format!("p__{i}")), s.clone()))
23        .collect();
24    if has_rest {
25        params.push(Form::new(FormKind::Symbol("&".into()), s.clone()));
26        params.push(Form::new(FormKind::Symbol("rest__".into()), s.clone()));
27    }
28
29    let new_body = rewrite_pct_refs(body, s.clone());
30
31    // Wrap the rewritten body forms back into a single call expression.
32    // #(f a b) → (fn* [params] (f a b)), not (fn* [params] f a b).
33    let body_expr = Form::new(FormKind::List(new_body), s.clone());
34
35    Form::new(
36        FormKind::List(vec![
37            Form::new(FormKind::Symbol("fn*".into()), s.clone()),
38            Form::new(FormKind::Vector(params), s.clone()),
39            body_expr,
40        ]),
41        span,
42    )
43}
44
45fn find_pct_refs(forms: &[Form], max_pos: &mut usize, has_rest: &mut bool) {
46    for form in forms {
47        find_pct_refs_form(form, max_pos, has_rest);
48    }
49}
50
51fn find_pct_refs_form(form: &Form, max_pos: &mut usize, has_rest: &mut bool) {
52    match &form.kind {
53        FormKind::Symbol(s) if (s == "%" || s == "%1") && *max_pos < 1 => {
54            *max_pos = 1;
55        }
56        FormKind::Symbol(s) if s == "%&" => {
57            *has_rest = true;
58        }
59        FormKind::Symbol(s) if s.starts_with('%') => {
60            if let Ok(n) = s[1..].parse::<usize>()
61                && n > *max_pos
62            {
63                *max_pos = n;
64            }
65        }
66        FormKind::List(c) | FormKind::Vector(c) | FormKind::Set(c) | FormKind::Map(c) => {
67            find_pct_refs(c, max_pos, has_rest);
68        }
69        // Reader-macro sugar (`@%`, `#'%`, `'%`, `` `% ``, `~%`, `~@%`, `#tag %`)
70        // wraps a single inner form; it must be scanned the same as any
71        // other nested form so `%` refs under sugar aren't missed.
72        FormKind::Quote(inner)
73        | FormKind::SyntaxQuote(inner)
74        | FormKind::Unquote(inner)
75        | FormKind::UnquoteSplice(inner)
76        | FormKind::Deref(inner)
77        | FormKind::Var(inner)
78        | FormKind::TaggedLiteral(_, inner) => {
79            find_pct_refs_form(inner, max_pos, has_rest);
80        }
81        FormKind::Meta(meta, inner) => {
82            find_pct_refs_form(meta, max_pos, has_rest);
83            find_pct_refs_form(inner, max_pos, has_rest);
84        }
85        FormKind::ReaderCond { clauses, .. } => {
86            find_pct_refs(clauses, max_pos, has_rest);
87        }
88        _ => {}
89    }
90}
91
92fn rewrite_pct_refs(forms: &[Form], span: cljrs_types::span::Span) -> Vec<Form> {
93    forms
94        .iter()
95        .map(|f| rewrite_pct_form(f, span.clone()))
96        .collect()
97}
98
99fn rewrite_pct_form(form: &Form, span: cljrs_types::span::Span) -> Form {
100    match &form.kind {
101        FormKind::Symbol(s) if s == "%" || s == "%1" => {
102            Form::new(FormKind::Symbol("p__1".into()), span)
103        }
104        FormKind::Symbol(s) if s == "%&" => Form::new(FormKind::Symbol("rest__".into()), span),
105        FormKind::Symbol(s) if s.starts_with('%') => {
106            if let Ok(n) = s[1..].parse::<usize>() {
107                Form::new(FormKind::Symbol(format!("p__{n}")), span)
108            } else {
109                form.clone()
110            }
111        }
112        FormKind::List(c) => {
113            let rewritten = rewrite_pct_refs(c, span.clone());
114            Form::new(FormKind::List(rewritten), span)
115        }
116        FormKind::Vector(c) => {
117            let rewritten = rewrite_pct_refs(c, span.clone());
118            Form::new(FormKind::Vector(rewritten), span)
119        }
120        FormKind::Set(c) => {
121            let rewritten = rewrite_pct_refs(c, span.clone());
122            Form::new(FormKind::Set(rewritten), span)
123        }
124        FormKind::Map(c) => {
125            let rewritten = rewrite_pct_refs(c, span.clone());
126            Form::new(FormKind::Map(rewritten), span)
127        }
128        FormKind::Quote(inner) => Form::new(
129            FormKind::Quote(Box::new(rewrite_pct_form(inner, span.clone()))),
130            span,
131        ),
132        FormKind::SyntaxQuote(inner) => Form::new(
133            FormKind::SyntaxQuote(Box::new(rewrite_pct_form(inner, span.clone()))),
134            span,
135        ),
136        FormKind::Unquote(inner) => Form::new(
137            FormKind::Unquote(Box::new(rewrite_pct_form(inner, span.clone()))),
138            span,
139        ),
140        FormKind::UnquoteSplice(inner) => Form::new(
141            FormKind::UnquoteSplice(Box::new(rewrite_pct_form(inner, span.clone()))),
142            span,
143        ),
144        FormKind::Deref(inner) => Form::new(
145            FormKind::Deref(Box::new(rewrite_pct_form(inner, span.clone()))),
146            span,
147        ),
148        FormKind::Var(inner) => Form::new(
149            FormKind::Var(Box::new(rewrite_pct_form(inner, span.clone()))),
150            span,
151        ),
152        FormKind::TaggedLiteral(tag, inner) => Form::new(
153            FormKind::TaggedLiteral(tag.clone(), Box::new(rewrite_pct_form(inner, span.clone()))),
154            span,
155        ),
156        FormKind::Meta(meta, inner) => Form::new(
157            FormKind::Meta(
158                Box::new(rewrite_pct_form(meta, span.clone())),
159                Box::new(rewrite_pct_form(inner, span.clone())),
160            ),
161            span,
162        ),
163        FormKind::ReaderCond { splicing, clauses } => Form::new(
164            FormKind::ReaderCond {
165                splicing: *splicing,
166                clauses: rewrite_pct_refs(clauses, span.clone()),
167            },
168            span,
169        ),
170        _ => form.clone(),
171    }
172}
173
174// ── auto-resolved identifiers ─────────────────────────────────────────────────
175
176/// Replace every `AutoKeyword` / `AutoSymbol` in a form tree with its
177/// fully-qualified `Keyword` / `Symbol`, resolving an `alias/name` prefix
178/// through `env`'s namespace alias table.
179///
180/// Every boundary that turns a form into data or into IR - `quote`, the `'`
181/// sugar, and `lower_arity` - must call this first, so no tier below sees an
182/// unresolved identifier. Errors when an alias is not registered in `env`.
183pub fn resolve_auto_forms(form: &Form, env: &crate::env::env::Env) -> EvalResult<Form> {
184    let resolve_seq = |items: &Vec<Form>| -> EvalResult<Vec<Form>> {
185        items.iter().map(|f| resolve_auto_forms(f, env)).collect()
186    };
187    let resolve_box =
188        |inner: &Form| -> EvalResult<Box<Form>> { Ok(Box::new(resolve_auto_forms(inner, env)?)) };
189    let qualify = |name: &str| -> EvalResult<String> {
190        env.globals
191            .resolve_auto_keyword(&env.current_ns, name)
192            .map_err(EvalError::Runtime)
193    };
194
195    let kind = match &form.kind {
196        FormKind::AutoKeyword(s) => FormKind::Keyword(qualify(s)?),
197        FormKind::AutoSymbol(s) => FormKind::Symbol(qualify(s)?),
198
199        FormKind::List(items) => FormKind::List(resolve_seq(items)?),
200        FormKind::Vector(items) => FormKind::Vector(resolve_seq(items)?),
201        FormKind::Map(items) => FormKind::Map(resolve_seq(items)?),
202        FormKind::Set(items) => FormKind::Set(resolve_seq(items)?),
203        FormKind::AnonFn(items) => FormKind::AnonFn(resolve_seq(items)?),
204        FormKind::ReaderCond { splicing, clauses } => FormKind::ReaderCond {
205            splicing: *splicing,
206            clauses: resolve_seq(clauses)?,
207        },
208
209        FormKind::Quote(inner) => FormKind::Quote(resolve_box(inner)?),
210        FormKind::SyntaxQuote(inner) => FormKind::SyntaxQuote(resolve_box(inner)?),
211        FormKind::Unquote(inner) => FormKind::Unquote(resolve_box(inner)?),
212        FormKind::UnquoteSplice(inner) => FormKind::UnquoteSplice(resolve_box(inner)?),
213        FormKind::Deref(inner) => FormKind::Deref(resolve_box(inner)?),
214        FormKind::Var(inner) => FormKind::Var(resolve_box(inner)?),
215        FormKind::TaggedLiteral(tag, inner) => {
216            FormKind::TaggedLiteral(tag.clone(), resolve_box(inner)?)
217        }
218        FormKind::Meta(meta, inner) => FormKind::Meta(resolve_box(meta)?, resolve_box(inner)?),
219
220        _ => return Ok(form.clone()),
221    };
222    Ok(Form::new(kind, form.span.clone()))
223}
224
225/// True for values that carry metadata (the JVM's `IObj`).
226///
227/// Scalars silently drop an annotation rather than growing a `WithMeta`
228/// wrapper no other tier expects.
229pub fn supports_meta(value: &Value) -> bool {
230    matches!(
231        value.unwrap_meta(),
232        Value::List(_)
233            | Value::Vector(_)
234            | Value::Map(_)
235            | Value::Set(_)
236            | Value::Queue(_)
237            | Value::Symbol(_)
238            | Value::Cons(_)
239            | Value::LazySeq(_)
240            | Value::Fn(_)
241            | Value::Macro(_)
242            | Value::BoundFn(_)
243            | Value::NativeFunction(_)
244            | Value::TypeInstance(_)
245    )
246}
247
248/// The map a `^meta` annotation denotes, expanding the reader shorthands
249/// (`^:kw` → `{:kw true}`, `^Sym` / `^"Str"` → `{:tag …}`).
250///
251/// `general` resolves the non-shorthand case — a map form, or an expression
252/// yielding one — and is the *only* difference between the two positions an
253/// annotation can appear in: inside `quote` it is [`form_to_value`], so
254/// `'^{:x (+ 1 2)} [1]` keeps the unevaluated list; outside it evaluates, so
255/// `^{:x (+ 1 2)} [1]` carries `{:x 3}`.
256///
257/// Both positions share this one table so they cannot drift apart.
258pub fn expand_meta_annotation(
259    meta: &Form,
260    general: &mut dyn FnMut(&Form) -> EvalResult<Value>,
261) -> EvalResult<Value> {
262    let entry = |k: Value, v: Value| Value::Map(MapValue::empty().assoc(k, v));
263    let expanded = match &meta.kind {
264        // `^:kw` → `{:kw true}`. An auto-keyword resolves against the reading
265        // namespace, which only `general` knows how to do in either position.
266        FormKind::Keyword(_) => entry(form_to_value(meta)?, Value::Bool(true)),
267        FormKind::AutoKeyword(_) => entry(general(meta)?, Value::Bool(true)),
268        // `^Sym` → `{:tag Sym}`, `^"Str"` → `{:tag "Str"}`. The tag is the
269        // symbol/string itself, never the value the symbol resolves to.
270        FormKind::Symbol(_) | FormKind::Str(_) => {
271            entry(Value::keyword(Keyword::parse("tag")), form_to_value(meta)?)
272        }
273        // An annotation the macro expander rebuilt out of a value is already
274        // data: `value_to_form` quotes it so that neither position analyses it
275        // a second time.
276        FormKind::Quote(inner) => form_to_value(inner)?,
277        _ => general(meta)?,
278    };
279    // `^42 [1]` is a malformed annotation, not metadata `42` — the JVM reader
280    // rejects it outright rather than attaching a non-map.
281    match expanded {
282        v @ (Value::Map(_) | Value::Nil) => Ok(v),
283        other => Err(EvalError::Runtime(format!(
284            "Metadata must be Symbol, Keyword, String or Map, got {}",
285            other.type_name()
286        ))),
287    }
288}
289
290/// Assoc every entry of `overlay` onto `base`, as the reader does for stacked
291/// metadata (`^:a ^:b x`): both survive, the outer annotation wins a clash.
292pub fn merge_meta_values(base: &Value, overlay: &Value) -> Value {
293    match (base, overlay) {
294        (Value::Map(base), Value::Map(overlay)) => {
295            let mut merged = base.clone();
296            overlay.for_each(|k, v| merged = merged.assoc(k.clone(), v.clone()));
297            Value::Map(merged)
298        }
299        _ => overlay.clone(),
300    }
301}
302
303/// Attach an expanded `^meta` annotation to the value it annotates.
304///
305/// A value that cannot carry metadata keeps none, and an annotation stacked on
306/// one already present merges with it, the outer one winning a clash. Shared by
307/// the quoted and evaluated positions so they attach identically.
308pub fn attach_meta(value: Value, annotation: Value) -> Value {
309    if matches!(annotation, Value::Nil) || !supports_meta(&value) {
310        return value;
311    }
312    let merged = match value.get_meta() {
313        Some(existing) => merge_meta_values(existing, &annotation),
314        None => annotation,
315    };
316    value.with_meta(merged)
317}
318
319/// Convert a `Form` to its literal `Value` without evaluating.
320/// Used by `quote` and macro expansion.
321///
322/// Every sibling sequence is run through [`expand_reader_conds`] first, so a
323/// `#?@` splice contributes its elements to the enclosing collection rather
324/// than reaching the leaf arm.
325///
326/// Errors when the form cannot denote a value: a map literal whose expansion
327/// has odd length, or a `#?@` splice in a position that has no sibling
328/// sequence to splice into.
329/// The list value `form_to_value` builds for a `FormKind::List`, over elements
330/// that are already converted.
331///
332/// `macroexpand_1` converts a call's parts itself so that `&form` and the
333/// macro's arguments can share one conversion; this keeps how a list form
334/// becomes a value in one place rather than two.
335pub fn values_to_list(items: impl IntoIterator<Item = Value>) -> Value {
336    Value::List(GcPtr::new(PersistentList::from_iter(items)))
337}
338
339pub fn form_to_value(form: &Form) -> EvalResult<Value> {
340    let value = match &form.kind {
341        FormKind::Nil => Value::Nil,
342        FormKind::Bool(b) => Value::Bool(*b),
343        FormKind::Int(n) => Value::Long(*n),
344        FormKind::Float(f) => Value::Double(*f),
345        FormKind::Symbolic(f) => Value::Double(*f),
346        FormKind::Str(s) => Value::string(s.clone()),
347        FormKind::Char(c) => Value::Char(*c),
348        FormKind::BigInt(s) => crate::builtins::parse_bigint(s).unwrap_or(Value::Nil),
349        FormKind::BigDecimal(s) => crate::builtins::parse_bigdecimal(s).unwrap_or(Value::Nil),
350        FormKind::Ratio(s) => crate::builtins::parse_ratio(s).unwrap_or(Value::Nil),
351
352        FormKind::Symbol(s) => Value::symbol(Symbol::parse(s)),
353        FormKind::Keyword(s) => Value::keyword(Keyword::parse(s)),
354        FormKind::AutoKeyword(s) => Value::keyword(Keyword::simple(s.as_str())),
355        FormKind::AutoSymbol(s) => Value::symbol(Symbol::parse(s)),
356        FormKind::Regex(s) => match Pattern::new(s.as_str()) {
357            Ok(pattern) => Value::Pattern(GcPtr::new(pattern)),
358            Err(_) => Value::Nil, // should already have been caught
359        },
360
361        FormKind::List(forms) => {
362            let items = forms_to_values(&expand_reader_conds_cow(forms))?;
363            values_to_list(items)
364        }
365        FormKind::Vector(forms) => {
366            let items = forms_to_values(&expand_reader_conds_cow(forms))?;
367            Value::Vector(GcPtr::new(PersistentVector::from_iter(items)))
368        }
369        FormKind::Map(forms) => {
370            let forms = expand_pairs(forms).map_err(|_| map_literal_arity_error())?;
371            let mut m = MapValue::empty();
372            for [k, v] in forms.as_chunks::<2>().0 {
373                m = m.assoc(form_to_value(k)?, form_to_value(v)?);
374            }
375            Value::Map(m)
376        }
377        FormKind::Set(forms) => {
378            let mut s = PersistentHashSet::empty();
379            for f in expand_reader_conds_cow(forms).iter() {
380                s = s.conj(form_to_value(f)?);
381            }
382            Value::Set(SetValue::Hash(GcPtr::new(s)))
383        }
384
385        // `'x` → the form x as a data value.
386        FormKind::Quote(inner) => reader_form_value("quote", inner)?,
387        FormKind::SyntaxQuote(inner) => reader_form_value("syntax-quote", inner)?,
388        FormKind::Unquote(inner) => reader_form_value("unquote", inner)?,
389        FormKind::UnquoteSplice(inner) => reader_form_value("unquote-splicing", inner)?,
390        FormKind::Deref(inner) => reader_form_value("deref", inner)?,
391        FormKind::Var(inner) => reader_form_value("var", inner)?,
392        FormKind::Meta(meta, inner) => {
393            let value = form_to_value(inner)?;
394            // Inside `quote` the annotation is data: nothing in it is evaluated.
395            let m = expand_meta_annotation(meta, &mut form_to_value)?;
396            attach_meta(value, m)
397        }
398        FormKind::AnonFn(body) => {
399            // Expand #(...) to (fn* [...] ...) so it round-trips correctly through quote.
400            let expanded = expand_anon_fn(body, form.span.clone());
401            form_to_value(&expanded)?
402        }
403        FormKind::TaggedLiteral(tag, inner) => match tag.as_str() {
404            "uuid" => {
405                if let FormKind::Str(s) = &inner.kind
406                    && let Ok(u) = uuid::Uuid::parse_str(s)
407                {
408                    Value::Uuid(u.as_u128())
409                } else {
410                    form_to_value(inner)?
411                }
412            }
413            _ => form_to_value(inner)?,
414        },
415        FormKind::ReaderCond {
416            splicing: false,
417            clauses,
418        } => match select_reader_cond(clauses) {
419            Some(selected) => form_to_value(selected)?,
420            None => Value::Nil,
421        },
422        // Splices are consumed by the enclosing sibling sequence; reaching a
423        // leaf means there was none.
424        FormKind::ReaderCond { splicing: true, .. } => {
425            return Err(EvalError::Runtime(
426                "splicing reader conditional not in a sequence context".into(),
427            ));
428        }
429    };
430    Ok(value)
431}
432
433fn forms_to_values(forms: &[Form]) -> EvalResult<Vec<Value>> {
434    forms.iter().map(form_to_value).collect()
435}
436
437/// `(<sym> <inner>)` — the list encoding of a reader-sugar form as data.
438fn reader_form_value(sym: &str, inner: &Form) -> EvalResult<Value> {
439    Ok(Value::List(GcPtr::new(PersistentList::from_iter([
440        Value::symbol(Symbol::simple(sym)),
441        form_to_value(inner)?,
442    ]))))
443}
444
445fn map_literal_arity_error() -> EvalError {
446    EvalError::Runtime("map literal must have an even number of forms".into())
447}
448
449/// Resolve a `#?(...)` reader conditional to the selected branch form, or
450/// `None` if no `:rust` or `:default` clause is present.
451pub fn select_reader_cond(clauses: &[Form]) -> Option<&Form> {
452    let mut default: Option<&Form> = None;
453    for [feature, branch] in clauses.as_chunks::<2>().0 {
454        match &feature.kind {
455            FormKind::Keyword(k) if k == "rust" => return Some(branch),
456            FormKind::Keyword(k) if k == "default" => default = Some(branch),
457            _ => {}
458        }
459    }
460    default
461}
462
463/// Expand reader conditionals in a flat slice of forms.
464///
465/// - Non-splicing `#?(...)`: replaced by the selected branch (or removed if none).
466/// - Splicing `#?@(...)`: selected branch must be a vector/list; its elements
467///   are inlined.  If no branch matches, the splice is removed (empty).
468pub fn expand_reader_conds(forms: &[Form]) -> Vec<Form> {
469    let mut out = Vec::with_capacity(forms.len());
470    for form in forms {
471        match &form.kind {
472            FormKind::ReaderCond {
473                splicing: true,
474                clauses,
475            } => {
476                if let Some(selected) = select_reader_cond(clauses) {
477                    match &selected.kind {
478                        FormKind::Vector(elems) | FormKind::List(elems) => {
479                            // Recursively expand any nested reader conditionals
480                            // within the spliced elements.
481                            let expanded_elems = expand_reader_conds(elems);
482                            out.extend(expanded_elems);
483                        }
484                        // Non-sequence branch: inline it as a single element.
485                        _ => out.push(selected.clone()),
486                    }
487                }
488                // No matching branch → splice nothing (empty).
489            }
490            FormKind::ReaderCond {
491                splicing: false,
492                clauses,
493            } => {
494                if let Some(selected) = select_reader_cond(clauses) {
495                    out.push(selected.clone());
496                }
497                // No matching branch → omit.
498            }
499            _ => out.push(form.clone()),
500        }
501    }
502    out
503}
504
505/// Expand `#?`/`#?@` reader conditionals in a sibling-form slice, borrowing the
506/// input unchanged when none are present.
507///
508/// Callers that validate element structure (e.g. map key/value parity) must do
509/// so on the returned slice.
510pub fn expand_reader_conds_cow(forms: &[Form]) -> std::borrow::Cow<'_, [Form]> {
511    if forms
512        .iter()
513        .any(|f| matches!(f.kind, FormKind::ReaderCond { .. }))
514    {
515        std::borrow::Cow::Owned(expand_reader_conds(forms))
516    } else {
517        std::borrow::Cow::Borrowed(forms)
518    }
519}
520
521/// A pair-structured sibling slice was left with an odd number of forms.
522/// Carries that length; callers phrase the error for their own construct.
523#[derive(Debug, Clone, Copy, PartialEq, Eq)]
524pub struct OddArity(pub usize);
525
526/// Expand reader conditionals in a slice that must hold key/value or
527/// binding/init *pairs*, then enforce even parity on the expansion.
528///
529/// Callers that chunk siblings by two - map literals, `let*`/`loop*`/`binding`
530/// vectors - resolve them here before chunking.
531pub fn expand_pairs(forms: &[Form]) -> Result<std::borrow::Cow<'_, [Form]>, OddArity> {
532    let expanded = expand_reader_conds_cow(forms);
533    if expanded.len().is_multiple_of(2) {
534        Ok(expanded)
535    } else {
536        Err(OddArity(expanded.len()))
537    }
538}
539
540#[cfg(test)]
541mod tests {
542    use super::*;
543    use cljrs_reader::Parser;
544
545    fn parse_anon_fn(src: &str) -> Form {
546        let mut parser = Parser::new(src.to_string(), "<test>".to_string());
547        let form = parser.parse_one().unwrap().unwrap();
548        let FormKind::AnonFn(body) = form.kind else {
549            panic!("expected AnonFn, got {:?}", form.kind);
550        };
551        expand_anon_fn(&body, form.span)
552    }
553
554    fn arity(expanded: &Form) -> usize {
555        let FormKind::List(parts) = &expanded.kind else {
556            panic!("expected (fn* [...] ...)");
557        };
558        let FormKind::Vector(params) = &parts[1].kind else {
559            panic!("expected param vector");
560        };
561        params.len()
562    }
563
564    #[test]
565    fn deref_sugar_counts_as_one_arg() {
566        // #(:x @%) must expand to (fn* [p__1] (:x (deref p__1))), arity 1 —
567        // not 0, as if % under `@` were invisible to the arg scanner.
568        let expanded = parse_anon_fn("#(:x @%)");
569        assert_eq!(arity(&expanded), 1);
570
571        let FormKind::List(parts) = &expanded.kind else {
572            unreachable!()
573        };
574        let FormKind::List(body) = &parts[2].kind else {
575            panic!("expected body list");
576        };
577        let FormKind::Deref(inner) = &body[1].kind else {
578            panic!("expected deref form, got {:?}", body[1].kind);
579        };
580        assert_eq!(inner.kind, FormKind::Symbol("p__1".to_string()));
581    }
582
583    #[test]
584    fn var_and_meta_sugar_also_scanned() {
585        assert_eq!(arity(&parse_anon_fn("#(#'%)")), 1);
586        assert_eq!(arity(&parse_anon_fn("#(^:x %)")), 1);
587        assert_eq!(arity(&parse_anon_fn("#('%)")), 1);
588    }
589
590    // ── reader-conditional expansion (#?@ splice) ───────────────────────────
591
592    fn parse_one_form(src: &str) -> Form {
593        let mut p = Parser::new(src.to_string(), "<test>".to_string());
594        p.parse_one().unwrap().unwrap()
595    }
596
597    fn vec_elems(src: &str) -> Vec<Form> {
598        match parse_one_form(src).kind {
599            FormKind::Vector(v) => v,
600            other => panic!("expected vector literal, got {other:?}"),
601        }
602    }
603
604    fn kinds(forms: &[Form]) -> Vec<FormKind> {
605        forms.iter().map(|f| f.kind.clone()).collect()
606    }
607
608    /// Expand the elements of a `[...]` literal and return their kinds.
609    fn expand_src(src: &str) -> Vec<FormKind> {
610        kinds(&expand_reader_conds(&vec_elems(src)))
611    }
612
613    #[test]
614    fn splice_rust_branch_flattens_into_parent() {
615        assert_eq!(
616            expand_src("[:x #?@(:rust [:a :b]) :y]"),
617            kinds(&vec_elems("[:x :a :b :y]"))
618        );
619    }
620
621    #[test]
622    fn splice_falls_back_to_default_branch() {
623        assert_eq!(
624            expand_src("[:x #?@(:default [:a :b]) :y]"),
625            kinds(&vec_elems("[:x :a :b :y]"))
626        );
627    }
628
629    #[test]
630    fn splice_with_no_matching_branch_is_removed_not_nil() {
631        // `:clj` does not match under cljrs; the splice contributes nothing.
632        // Regression: eval'ing the conditional on its own used to leave a `nil`.
633        assert_eq!(
634            expand_src("[:x #?@(:clj [:a :b]) :y]"),
635            kinds(&vec_elems("[:x :y]"))
636        );
637    }
638
639    #[test]
640    fn non_splicing_conditional_selects_single_form() {
641        assert_eq!(
642            expand_src("[:x #?(:rust :a :default :b) :y]"),
643            kinds(&vec_elems("[:x :a :y]"))
644        );
645    }
646
647    #[test]
648    fn nested_conditionals_inside_spliced_branch_expand() {
649        assert_eq!(
650            expand_src("[#?@(:rust [#?(:rust :a :default :z) :b])]"),
651            kinds(&vec_elems("[:a :b]"))
652        );
653    }
654
655    #[test]
656    fn cow_borrows_when_no_conditional_present() {
657        let forms = vec_elems("[:x :y :z]");
658        assert!(matches!(
659            expand_reader_conds_cow(&forms),
660            std::borrow::Cow::Borrowed(_)
661        ));
662    }
663
664    #[test]
665    fn cow_owns_when_conditional_present() {
666        let forms = vec_elems("[:x #?@(:rust [:a]) :y]");
667        assert!(matches!(
668            expand_reader_conds_cow(&forms),
669            std::borrow::Cow::Owned(_)
670        ));
671    }
672
673    fn kw_list(names: &[String]) -> String {
674        names
675            .iter()
676            .map(|n| format!(":{n}"))
677            .collect::<Vec<_>>()
678            .join(" ")
679    }
680
681    // Model-based oracle. An `Item` is one position in a form sequence: a plain
682    // keyword, a non-splicing conditional, or a splicing conditional. Each knows
683    // both how to render itself as source and what it must contribute after
684    // expansion under the `:rust` selection rule - an independent reimplementation
685    // of `select_reader_cond` the production code is checked against.
686
687    #[derive(Clone, Debug)]
688    enum Item {
689        Plain(String),
690        NonSplice(Vec<(String, String)>),
691        Splice(Vec<(String, Vec<String>)>),
692    }
693
694    fn select_model<T>(branches: &[(String, T)]) -> Option<&T> {
695        let mut default = None;
696        for (k, v) in branches {
697            if k == "rust" {
698                return Some(v);
699            }
700            if k == "default" {
701                default = Some(v);
702            }
703        }
704        default
705    }
706
707    fn item_src(it: &Item) -> String {
708        match it {
709            Item::Plain(n) => format!(":{n}"),
710            Item::NonSplice(brs) => {
711                let inner = brs
712                    .iter()
713                    .map(|(k, n)| format!(":{k} :{n}"))
714                    .collect::<Vec<_>>()
715                    .join(" ");
716                format!("#?({inner})")
717            }
718            Item::Splice(brs) => {
719                let inner = brs
720                    .iter()
721                    .map(|(k, vs)| format!(":{k} [{}]", kw_list(vs)))
722                    .collect::<Vec<_>>()
723                    .join(" ");
724                format!("#?@({inner})")
725            }
726        }
727    }
728
729    fn item_expected(it: &Item) -> Vec<String> {
730        match it {
731            Item::Plain(n) => vec![n.clone()],
732            Item::NonSplice(brs) => select_model(brs).cloned().into_iter().collect(),
733            Item::Splice(brs) => select_model(brs).cloned().unwrap_or_default(),
734        }
735    }
736
737    fn key_strat() -> impl proptest::strategy::Strategy<Value = String> {
738        proptest::prop_oneof![
739            proptest::strategy::Just("rust".to_string()),
740            proptest::strategy::Just("clj".to_string()),
741            proptest::strategy::Just("cljs".to_string()),
742            proptest::strategy::Just("default".to_string()),
743        ]
744    }
745
746    fn item_strat() -> impl proptest::strategy::Strategy<Value = Item> {
747        use proptest::collection::vec as pvec;
748        use proptest::strategy::Strategy;
749        proptest::prop_oneof![
750            "[a-z]{1,4}".prop_map(Item::Plain),
751            pvec((key_strat(), "[a-z]{1,4}".prop_map(|s| s)), 1..4).prop_map(Item::NonSplice),
752            pvec((key_strat(), pvec("[a-z]{1,4}", 0..3)), 1..4).prop_map(Item::Splice),
753        ]
754    }
755
756    fn seq_src(items: &[Item]) -> String {
757        format!(
758            "[{}]",
759            items.iter().map(item_src).collect::<Vec<_>>().join(" ")
760        )
761    }
762
763    fn seq_expected_src(items: &[Item]) -> String {
764        let names: Vec<String> = items.iter().flat_map(item_expected).collect();
765        format!("[{}]", kw_list(&names))
766    }
767
768    proptest::proptest! {
769        /// Splicing `#?@(:rust xs)` at a position equals inlining `xs` there.
770        #[test]
771        fn prop_splice_equals_inline(
772            pre in proptest::collection::vec("[a-z]{1,4}", 0..4),
773            mid in proptest::collection::vec("[a-z]{1,4}", 0..4),
774            suf in proptest::collection::vec("[a-z]{1,4}", 0..4),
775        ) {
776            let spliced = format!("[{} #?@(:rust [{}]) {}]",
777                                  kw_list(&pre), kw_list(&mid), kw_list(&suf));
778            let inlined = format!("[{} {} {}]",
779                                  kw_list(&pre), kw_list(&mid), kw_list(&suf));
780            proptest::prop_assert_eq!(expand_src(&spliced), kinds(&vec_elems(&inlined)));
781        }
782
783        /// Expansion is idempotent: a second pass changes nothing.
784        #[test]
785        fn prop_expand_idempotent(
786            pre in proptest::collection::vec("[a-z]{1,4}", 0..4),
787            mid in proptest::collection::vec("[a-z]{1,4}", 0..4),
788        ) {
789            let src = format!("[{} #?@(:rust [{}])]", kw_list(&pre), kw_list(&mid));
790            let once = expand_reader_conds(&vec_elems(&src));
791            let twice = expand_reader_conds(&once);
792            proptest::prop_assert_eq!(kinds(&once), kinds(&twice));
793        }
794
795        /// With no reader conditionals, expansion is the identity.
796        #[test]
797        fn prop_no_conditional_is_identity(
798            xs in proptest::collection::vec("[a-z]{1,4}", 0..8),
799        ) {
800            let src = format!("[{}]", kw_list(&xs));
801            proptest::prop_assert_eq!(expand_src(&src), kinds(&vec_elems(&src)));
802        }
803
804        /// Expansion of an arbitrary interleaving of plain / non-splicing /
805        /// splicing conditionals (mixed keys) equals the model's expected output.
806        #[test]
807        fn prop_model_matches_expand(
808            items in proptest::collection::vec(item_strat(), 0..6),
809        ) {
810            proptest::prop_assert_eq!(
811                expand_src(&seq_src(&items)),
812                kinds(&vec_elems(&seq_expected_src(&items)))
813            );
814        }
815
816        /// No `ReaderCond` node survives expansion at the top level.
817        #[test]
818        fn prop_no_reader_cond_survives(
819            items in proptest::collection::vec(item_strat(), 0..6),
820        ) {
821            let expanded = expand_reader_conds(&vec_elems(&seq_src(&items)));
822            let none_survive = expanded
823                .iter()
824                .all(|f| !matches!(f.kind, FormKind::ReaderCond { .. }));
825            proptest::prop_assert!(none_survive);
826        }
827
828        /// The expanded length equals the sum of each item's contributed count.
829        #[test]
830        fn prop_length_law(
831            items in proptest::collection::vec(item_strat(), 0..6),
832        ) {
833            let expected_len: usize = items.iter().map(|it| item_expected(it).len()).sum();
834            let got = expand_reader_conds(&vec_elems(&seq_src(&items)));
835            proptest::prop_assert_eq!(got.len(), expected_len);
836        }
837    }
838
839    // ── quoted containers: splice equals inline ─────────────────────────────
840    //
841    // One generated position in a container. Element names are assigned by
842    // index so every element is distinct, which keeps generated set literals
843    // legal and makes map keys unambiguous.
844
845    #[derive(Clone, Debug)]
846    enum Slot {
847        /// A plain element; contributes 1.
848        Plain,
849        /// `#?(…)`; contributes 1 when the branch key matches, else 0.
850        NonSplice { matches: bool },
851        /// `#?@(…)`; contributes `n` when the branch key matches, else 0.
852        Splice { matches: bool, n: usize },
853    }
854
855    fn slot_strat() -> impl proptest::strategy::Strategy<Value = Slot> {
856        use proptest::strategy::{Just, Strategy};
857        proptest::prop_oneof![
858            Just(Slot::Plain),
859            proptest::bool::ANY.prop_map(|matches| Slot::NonSplice { matches }),
860            (proptest::bool::ANY, 0usize..3).prop_map(|(matches, n)| Slot::Splice { matches, n }),
861        ]
862    }
863
864    /// Render `slots` twice: once with the conditionals written out, once with
865    /// the selected branches already inlined. Both share one name counter, so
866    /// corresponding elements carry identical names.
867    fn render_slots(slots: &[Slot]) -> (String, String) {
868        let mut next = 0usize;
869        let mut name = || {
870            let n = next;
871            next += 1;
872            format!(":k{n}")
873        };
874        let (mut written, mut inlined) = (Vec::new(), Vec::new());
875        for slot in slots {
876            match *slot {
877                Slot::Plain => {
878                    let k = name();
879                    written.push(k.clone());
880                    inlined.push(k);
881                }
882                Slot::NonSplice { matches } => {
883                    let k = name();
884                    let key = if matches { "rust" } else { "clj" };
885                    written.push(format!("#?(:{key} {k})"));
886                    if matches {
887                        inlined.push(k);
888                    }
889                }
890                Slot::Splice { matches, n } => {
891                    let ks: Vec<String> = (0..n).map(|_| name()).collect();
892                    let key = if matches { "rust" } else { "clj" };
893                    written.push(format!("#?@(:{key} [{}])", ks.join(" ")));
894                    if matches {
895                        inlined.extend(ks);
896                    }
897                }
898            }
899        }
900        (written.join(" "), inlined.join(" "))
901    }
902
903    fn quoted_value(src: &str) -> EvalResult<Value> {
904        form_to_value(&parse_one_form(src))
905    }
906
907    /// Read `src` and convert it to a value, collapsing the read-time and
908    /// convert-time rejections into one channel: a map literal is rejected by
909    /// the reader when its parity is statically decidable and by
910    /// `form_to_value` when a reader conditional deferred it.
911    fn read_to_value(src: &str) -> Result<Value, String> {
912        let mut p = Parser::new(src.to_string(), "<test>".to_string());
913        let form = p
914            .parse_one()
915            .map_err(|e| e.to_string())?
916            .ok_or_else(|| "no form".to_string())?;
917        form_to_value(&form).map_err(|e| e.to_string())
918    }
919
920    proptest::proptest! {
921        /// `expand_pairs` is exactly `expand_reader_conds` plus an even-length
922        /// gate: it accepts precisely the expansions that can be chunked by two,
923        /// and never alters what expansion produced.
924        #[test]
925        fn prop_expand_pairs_is_expansion_plus_parity_gate(
926            items in proptest::collection::vec(item_strat(), 0..6),
927        ) {
928            let forms = vec_elems(&seq_src(&items));
929            let expanded = expand_reader_conds(&forms);
930            match expand_pairs(&forms) {
931                Ok(got) => {
932                    proptest::prop_assert!(expanded.len().is_multiple_of(2));
933                    proptest::prop_assert_eq!(kinds(&got), kinds(&expanded));
934                }
935                Err(OddArity(len)) => {
936                    proptest::prop_assert!(!expanded.len().is_multiple_of(2));
937                    proptest::prop_assert_eq!(len, expanded.len());
938                }
939            }
940        }
941
942        /// The regression property for the quoted-data path: in EVERY container,
943        /// a quoted form containing reader conditionals denotes the same value as
944        /// the same container with the selected branches written out by hand.
945        /// A container that drops, reorders, or nil-fills a splice fails here.
946        #[test]
947        fn prop_quoted_splice_equals_inline_in_every_container(
948            slots in proptest::collection::vec(slot_strat(), 0..5),
949        ) {
950            let (written, inlined) = render_slots(&slots);
951            for (open, close) in [("[", "]"), ("#{", "}"), ("(", ")"), ("{", "}")] {
952                // A map needs an even number of forms to denote a value at all;
953                // that case is the subject of its own property below.
954                let expanded_len = expand_reader_conds(&vec_elems(&format!("[{written}]"))).len();
955                if open == "{" && !expanded_len.is_multiple_of(2) {
956                    continue;
957                }
958                let got = quoted_value(&format!("{open}{written}{close}"));
959                let want = quoted_value(&format!("{open}{inlined}{close}"));
960                proptest::prop_assert_eq!(
961                    got.map_err(|e| e.to_string()),
962                    want.map_err(|e| e.to_string()),
963                    "container {}{}", open, close
964                );
965            }
966        }
967
968        /// A map denotes a value exactly when its EXPANSION has even length -
969        /// the written parity says nothing, and it makes no difference whether
970        /// the reader or the evaluator is the layer that rejects it. Truncating
971        /// the odd tail (the old behaviour) would yield a value here instead.
972        #[test]
973        fn prop_map_denotes_a_value_iff_expansion_is_even(
974            slots in proptest::collection::vec(slot_strat(), 0..5),
975        ) {
976            let (written, _) = render_slots(&slots);
977            let expanded_len = expand_reader_conds(&vec_elems(&format!("[{written}]"))).len();
978            let got = read_to_value(&format!("{{{written}}}"));
979            proptest::prop_assert_eq!(got.is_ok(), expanded_len.is_multiple_of(2), "{:?}", got);
980        }
981
982        /// Nesting a container inside a quoted container does not change the
983        /// values the inner splice contributes.
984        #[test]
985        fn prop_quoted_splice_survives_nesting(
986            slots in proptest::collection::vec(slot_strat(), 0..4),
987        ) {
988            let (written, inlined) = render_slots(&slots);
989            let got = quoted_value(&format!("[:head [{written}] :tail]"));
990            let want = quoted_value(&format!("[:head [{inlined}] :tail]"));
991            proptest::prop_assert_eq!(
992                got.map_err(|e| e.to_string()),
993                want.map_err(|e| e.to_string())
994            );
995        }
996    }
997
998    // ── examples from the PR #294 review ────────────────────────────────────
999
1000    #[test]
1001    fn quoted_map_splices_into_key_value_positions() {
1002        assert_eq!(
1003            quoted_value("'{:a 1 #?@(:rust [:b 2]) :c 3}").unwrap(),
1004            quoted_value("'{:a 1 :b 2 :c 3}").unwrap()
1005        );
1006    }
1007
1008    #[test]
1009    fn quoted_set_splices_its_elements() {
1010        assert_eq!(
1011            quoted_value("'#{1 #?@(:rust [2 3]) 4}").unwrap(),
1012            quoted_value("'#{1 2 3 4}").unwrap()
1013        );
1014    }
1015
1016    #[test]
1017    fn quoted_map_with_odd_expansion_errors_rather_than_truncating() {
1018        let err = quoted_value("{:a 1 #?@(:rust [:b]) :c 3}").unwrap_err();
1019        assert!(err.to_string().contains("even number of forms"), "{err}");
1020    }
1021
1022    #[test]
1023    fn uuid_tagged_literal_reads_as_a_uuid_value() {
1024        let got = quoted_value("#uuid \"f81d4fae-7dec-11d0-a765-00a0c91e6bf6\"").unwrap();
1025        assert_eq!(
1026            got,
1027            Value::Uuid(0xf81d4fae_7dec_11d0_a765_00a0c91e6bf6_u128)
1028        );
1029    }
1030
1031    #[test]
1032    fn splice_without_a_sibling_sequence_errors() {
1033        let err = quoted_value("#?@(:rust [1 2])").unwrap_err();
1034        assert!(err.to_string().contains("sequence context"), "{err}");
1035    }
1036}