Skip to main content

tatara_lisp/
spanned_expand.rs

1//! Span-preserving macro expander.
2//!
3//! Mirror of `macro_expand::Expander` that operates on `Spanned` input and
4//! produces `Spanned` output. Preserves source positions through macro
5//! expansion so downstream evaluators can report errors at the exact
6//! subform the user wrote (or, for macro-generated subtrees, at the
7//! macro call site).
8//!
9//! This path is intentionally simpler than the plain `Expander`:
10//!
11//!   * No bytecode template compilation.
12//!   * No expansion cache — args carry spans, so two calls with otherwise-
13//!     identical args may differ by position, making the cache mostly
14//!     useless here.
15//!
16//! The plain `Expander` on `Sexp` remains the fast path for the
17//! `compile_typed` pipeline. This spanned path exists for `tatara-lisp-eval`
18//! REPL + runtime evaluation where good error locations matter more than
19//! throughput.
20//!
21//! ── expander unification (phase 2 step 5c) ────────────────────────────
22//!
23//! Step 5b unified the READER — one tokenizer, one `Atom::from_lexeme`,
24//! two projections. Step 5c unified the DEFINITION and BINDING halves of
25//! the expander. What used to be four duplicate bodies in this file is
26//! now:
27//!
28//!   * `parse_params_spanned` — **deleted**. Lambda lists are pure syntax
29//!     and a `MacroDef` retains no spans, so there is one parser.
30//!   * `spanned_macro_def_from` — a head-keyword pre-check plus a call to
31//!     `macro_expand::macro_def_from`. One recognizer, one error taxonomy.
32//!   * `bind_spanned_args` — a zip of the shared
33//!     `MacroParams::bind_carrier` against `names()`. One binding loop,
34//!     generic over the value carrier via `MacroArgCarrier`.
35//!   * `substitute_spanned` + `template_eval` — still this file's own, and
36//!     deliberately so; see the residue note below.
37//!
38//! Adopting A's `MacroParams` gave this path `&optional` (with per-param
39//! defaults) and the too-many-args rejection it never had, with A's
40//! semantics rather than a second implementation written in passing —
41//! which is exactly the split-brain the pre-5c note refused to create.
42//!
43//! ── `pending-template-eval-unification` (residue) ─────────────────────
44//!
45//! `template_eval` — the `car`/`cdr`/`cons`/`list`/`null?`/`pair?`/
46//! `list?`/`length`/`if` metalanguage that lives inside `,expr` — is
47//! still one-sided: the plain `Expander` does NOT have it, and rejects
48//! any `,expr` whose target is not a bare bound symbol.
49//!
50//! Measured 2026-07-30, so the next attempt does not re-derive it: this
51//! is NOT a copy, it is a semantic ADDITION to the canonical path. The
52//! plain expander's default strategy compiles each template to a linear
53//! bytecode (`TemplateOp::{Literal, Subst(idx), Splice(idx), BeginList,
54//! EndList}`) in which an unquote is an INDEX into the bound-arg vec —
55//! `compile_node` resolves `,x` through `unquote_target_symbol` +
56//! `resolve_param_index`, both of which structurally require a symbol.
57//! A `,(car x)` has no index to compile to. Hoisting `template_eval`
58//! therefore means a new `TemplateOp` variant carrying an unevaluated
59//! `Sexp` plus an evaluator run at apply time, and it widens the accepted
60//! language of every existing consumer of the plain path. That is a
61//! design change to A's canonical semantics, not a consolidation of two
62//! copies of one semantics — so it is deliberately NOT bundled into the
63//! step that removes duplicates.
64//!
65//! The remaining duplication is `substitute_spanned`'s walk, which mirrors
66//! `macro_expand::substitute`. It cannot collapse before `template_eval`
67//! does: the two walks differ precisely at the unquote arm, where this one
68//! calls `template_eval` and the plain one looks up a name.
69
70use std::collections::HashMap;
71
72use crate::ast::Sexp;
73use crate::error::{LispError, MacroDefHead, Result};
74use crate::macro_expand::{macro_def_from, MacroArgCarrier, MacroDef};
75use crate::span::Span;
76use crate::spanned::{Spanned, SpannedForm};
77
78impl MacroArgCarrier for Spanned {
79    /// The macro CALL SITE. A value the call never supplied — an unfilled
80    /// `&optional` slot's default form, or the synthesized `&rest` list —
81    /// has no source position of its own, so it wears the span of the call
82    /// that caused it to exist. That is the position an error about such a
83    /// value should point at.
84    type Site = Span;
85
86    fn lift_default(default: &Sexp, site: Span) -> Self {
87        Spanned::from_sexp_at(default, site)
88    }
89
90    fn collect_rest(items: Vec<Self>, site: Span) -> Self {
91        Spanned::new(site, SpannedForm::List(items))
92    }
93}
94
95/// Span-preserving macro expander.
96#[derive(Clone, Default)]
97pub struct SpannedExpander {
98    macros: HashMap<String, MacroDef>,
99}
100
101impl SpannedExpander {
102    pub fn new() -> Self {
103        Self::default()
104    }
105
106    pub fn has(&self, name: &str) -> bool {
107        self.macros.contains_key(name)
108    }
109
110    pub fn len(&self) -> usize {
111        self.macros.len()
112    }
113
114    pub fn is_empty(&self) -> bool {
115        self.macros.is_empty()
116    }
117
118    /// Look up a registered macro by name. `None` if unknown.
119    pub fn get_macro(&self, name: &str) -> Option<&MacroDef> {
120        self.macros.get(name)
121    }
122
123    /// All registered macro names. Order is unspecified.
124    pub fn macro_names(&self) -> impl Iterator<Item = &str> {
125        self.macros.keys().map(|s| s.as_str())
126    }
127
128    /// Recognize `defmacro` / `defpoint-template` / `defcheck` and register
129    /// the definition. Returns `true` if `form` was a macro definition
130    /// (and was consumed), `false` if it was an ordinary form. Used by
131    /// embedders that interleave registration with evaluation form-by-form
132    /// (e.g. `tatara-lisp-eval`'s REPL).
133    pub fn try_register_macro(&mut self, form: &Spanned) -> Result<bool> {
134        if let Some(def) = spanned_macro_def_from(form)? {
135            self.macros.insert(def.name.clone(), def);
136            Ok(true)
137        } else {
138            Ok(false)
139        }
140    }
141
142    /// Expand a program. `defmacro`-family forms register and are consumed;
143    /// remaining forms are expanded.
144    pub fn expand_program(&mut self, forms: Vec<Spanned>) -> Result<Vec<Spanned>> {
145        let mut out = Vec::new();
146        for form in forms {
147            if self.try_register_macro(&form)? {
148                continue;
149            }
150            out.push(self.expand(&form)?);
151        }
152        Ok(out)
153    }
154
155    /// Expand a single form. Top-level macro calls are rewritten; otherwise
156    /// recurses into list children.
157    pub fn expand(&self, form: &Spanned) -> Result<Spanned> {
158        let SpannedForm::List(list) = &form.form else {
159            return Ok(form.clone());
160        };
161        if let Some(head_name) = list.first().and_then(Spanned::as_symbol) {
162            if let Some(def) = self.macros.get(head_name) {
163                let expanded = self.apply(def, form.span, &list[1..])?;
164                return self.expand(&expanded);
165            }
166        }
167        let mut out_children: Vec<Spanned> = Vec::with_capacity(list.len());
168        for child in list {
169            out_children.push(self.expand(child)?);
170        }
171        Ok(Spanned::new(form.span, SpannedForm::List(out_children)))
172    }
173
174    /// Apply one macro definition at `call_span` to its spanned arguments.
175    fn apply(&self, def: &MacroDef, call_span: Span, args: &[Spanned]) -> Result<Spanned> {
176        let bindings = bind_spanned_args(&def.name, &def.params, args, call_span)?;
177        substitute_spanned(def.template_body(), &bindings, call_span)
178    }
179}
180
181/// Per-call binding from param name to spanned argument tree.
182///
183/// A `&rest` binding is an ordinary `SpannedForm::List` value rather than a
184/// distinguished variant: `template_eval` already projected the old
185/// `Binding::Rest` to exactly that list, and `splice_into` already flattened
186/// any list value. Collapsing the enum is what lets the shared
187/// [`MacroParams::bind_carrier`](crate::macro_expand::MacroParams::bind_carrier)
188/// produce these bindings directly.
189type Bindings = HashMap<String, Spanned>;
190
191/// Bind a macro call's spanned args through the ONE shared positional binder
192/// on [`MacroParams`](crate::macro_expand::MacroParams), then zip the result
193/// against `names()` into the name-keyed map `substitute_spanned` /
194/// `template_eval` look substitutions up in — the span-carrying mirror of
195/// `macro_expand::bind_args`.
196///
197/// The lambda-list semantics (required run, `&optional` run with per-param
198/// defaults, at-most-one `&rest`, too-few and too-many arity rejections) are
199/// no longer restated here; this path and the plain `Sexp` path cannot
200/// disagree about them because they run the same loop.
201fn bind_spanned_args(
202    macro_name: &str,
203    params: &crate::macro_expand::MacroParams,
204    args: &[Spanned],
205    call_span: Span,
206) -> Result<Bindings> {
207    let vals = params.bind_carrier(macro_name, args, call_span)?;
208    Ok(params
209        .names()
210        .into_iter()
211        .map(String::from)
212        .zip(vals)
213        .collect())
214}
215
216/// Walk a plain-Sexp template body, substituting `,name` / `,@name` with
217/// the spanned bindings and stamping literal template content with the
218/// call-site span.
219///
220/// Inside `,expr`, the expression is evaluated at expansion time against
221/// the macro's parameter bindings — a tiny built-in template-time
222/// evaluator handles bare symbols, `car`/`cdr`/`cons`/`list`/`null?`/
223/// `pair?`/`length`/`if`/`quote`, and literal atoms. This is enough
224/// expressive power for the `->` / `->>` / threading macros and other
225/// recursive macro definitions that need to dispatch on rest-arg shape.
226fn substitute_spanned(template: &Sexp, bindings: &Bindings, call_span: Span) -> Result<Spanned> {
227    match template {
228        Sexp::Unquote(inner) => template_eval(inner, bindings, call_span),
229        Sexp::UnquoteSplice(_) => Err(LispError::Compile {
230            form: "unquote-splice".into(),
231            message: "`,@` may only appear inside a list".into(),
232        }),
233        Sexp::List(items) => {
234            let mut out: Vec<Spanned> = Vec::with_capacity(items.len());
235            for item in items {
236                if let Sexp::UnquoteSplice(inner) = item {
237                    let evaluated = template_eval(inner, bindings, call_span)?;
238                    splice_into(&evaluated, &mut out);
239                } else {
240                    out.push(substitute_spanned(item, bindings, call_span)?);
241                }
242            }
243            Ok(Spanned::new(call_span, SpannedForm::List(out)))
244        }
245        Sexp::Quote(inner) => {
246            let inner = substitute_spanned(inner, bindings, call_span)?;
247            Ok(Spanned::new(call_span, SpannedForm::Quote(Box::new(inner))))
248        }
249        Sexp::Quasiquote(inner) => {
250            let inner = substitute_spanned(inner, bindings, call_span)?;
251            Ok(Spanned::new(
252                call_span,
253                SpannedForm::Quasiquote(Box::new(inner)),
254            ))
255        }
256        Sexp::Nil => Ok(Spanned::new(call_span, SpannedForm::Nil)),
257        Sexp::Atom(a) => Ok(Spanned::new(call_span, SpannedForm::Atom(a.clone()))),
258    }
259}
260
261/// Recognize a spanned `(defmacro name (params) body)` / `defpoint-template`
262/// / `defcheck` form and lower it to the plain `MacroDef` the registry
263/// expects. Span information on the definition itself is not retained —
264/// macros are keyed by name.
265///
266/// The recognition itself is delegated to
267/// [`macro_expand::macro_def_from`](crate::macro_expand::macro_def_from), so
268/// the `defmacro`-head set, the lambda-list grammar (including `&optional`
269/// with defaults) and the definition-site error taxonomy are shared with the
270/// plain expander rather than restated here. Since a `MacroDef` retains no
271/// spans, lowering the form loses nothing.
272///
273/// The head-keyword pre-check is not redundant: `try_register_macro` runs on
274/// EVERY top-level form, and it is what keeps the `to_sexp()` lowering off
275/// the path of ordinary (non-definition) forms.
276fn spanned_macro_def_from(form: &Spanned) -> Result<Option<MacroDef>> {
277    let Some(list) = form.as_list() else {
278        return Ok(None);
279    };
280    let Some(head) = list.first().and_then(Spanned::as_symbol) else {
281        return Ok(None);
282    };
283    if MacroDefHead::from_keyword(head).is_none() {
284        return Ok(None);
285    }
286    macro_def_from(&form.to_sexp())
287}
288
289/// Splice `evaluated` into the surrounding list builder. List values
290/// flatten in; nil disappears; everything else is pushed as a single item.
291fn splice_into(evaluated: &Spanned, out: &mut Vec<Spanned>) {
292    match &evaluated.form {
293        SpannedForm::List(children) => out.extend(children.iter().cloned()),
294        SpannedForm::Nil => {}
295        _ => out.push(evaluated.clone()),
296    }
297}
298
299/// Template-time evaluator. Lives inside `,expr` and walks a Sexp
300/// template expression, substituting bindings and computing a result
301/// Spanned tree. Intentionally bounded — supports the operations
302/// needed for self-recursive macros that pattern-match on rest args.
303///
304/// Supports:
305///
306/// * Bare symbols → look up in `bindings` (Single binding returns its
307///   Spanned; Rest returns a Spanned::List of the rest items).
308/// * Atoms (Int / Float / Str / Bool / Keyword) → wrapped with
309///   `call_span`.
310/// * `(quote x)` → x lifted to Spanned without evaluation.
311/// * `(car x)`, `(cdr x)`, `(cons h t)`, `(list ...)` — list ops on
312///   evaluated children.
313/// * `(null? x)`, `(pair? x)`, `(list? x)` — predicates → `Bool` Spanned.
314/// * `(length x)` → integer Spanned.
315/// * `(if c t e)` — picks branch by truthiness of the evaluated cond.
316///
317/// Anything else is rejected with a clear error.
318fn template_eval(expr: &Sexp, bindings: &Bindings, call_span: Span) -> Result<Spanned> {
319    match expr {
320        Sexp::Atom(crate::ast::Atom::Symbol(name)) => {
321            // Bare symbol — look up in bindings. A `&rest` binding is
322            // already a `SpannedForm::List` stamped at the call site by
323            // `MacroArgCarrier::collect_rest`, so there is no rest-specific
324            // arm to take here.
325            match bindings.get(name) {
326                Some(val) => Ok(val.clone()),
327                None => Err(LispError::Compile {
328                    form: format!(",{name}"),
329                    message: "unbound in macro template".into(),
330                }),
331            }
332        }
333        Sexp::Atom(a) => Ok(Spanned::new(call_span, SpannedForm::Atom(a.clone()))),
334        Sexp::Nil => Ok(Spanned::new(call_span, SpannedForm::Nil)),
335        Sexp::Quote(inner) => Ok(Spanned::from_sexp_at(inner, call_span)),
336        // `\`expr` at template-eval time MEANS "produce the substituted
337        // form of expr" — i.e., re-enter substitution. This is how a
338        // recursive macro template reaches its else-branch, e.g.
339        // `(-> ,x ,(if (null? steps) `,result `(-> ,inner ,@rest)))`.
340        Sexp::Quasiquote(inner) => substitute_spanned(inner, bindings, call_span),
341        // `,expr` inside template_eval just unwraps one level — it
342        // identifies an expression to evaluate, which is exactly what
343        // template_eval is doing anyway.
344        Sexp::Unquote(inner) => template_eval(inner, bindings, call_span),
345        Sexp::UnquoteSplice(_) => Err(LispError::Compile {
346            form: "template-eval".into(),
347            message: "`,@` only valid directly inside a list".into(),
348        }),
349        Sexp::List(items) => {
350            if items.is_empty() {
351                return Ok(Spanned::new(call_span, SpannedForm::List(Vec::new())));
352            }
353            let head = items[0].as_symbol().ok_or_else(|| LispError::Compile {
354                form: "template-eval".into(),
355                message: "first element of a template-time list must be a symbol".into(),
356            })?;
357            match head {
358                "quote" => {
359                    let arg = items.get(1).ok_or_else(|| LispError::Compile {
360                        form: "quote".into(),
361                        message: "expected one arg".into(),
362                    })?;
363                    Ok(Spanned::from_sexp_at(arg, call_span))
364                }
365                "car" => {
366                    let xs = template_eval_list(&items[1..], 1, "car", bindings, call_span)?;
367                    let inner = template_eval(&xs[0].1, bindings, call_span)?;
368                    let list = require_spanned_list(&inner, "car")?;
369                    if list.is_empty() {
370                        return Err(LispError::Compile {
371                            form: "car".into(),
372                            message: "car of empty list".into(),
373                        });
374                    }
375                    Ok(list[0].clone())
376                }
377                "cdr" => {
378                    let xs = template_eval_list(&items[1..], 1, "cdr", bindings, call_span)?;
379                    let inner = template_eval(&xs[0].1, bindings, call_span)?;
380                    let list = require_spanned_list(&inner, "cdr")?;
381                    if list.is_empty() {
382                        return Err(LispError::Compile {
383                            form: "cdr".into(),
384                            message: "cdr of empty list".into(),
385                        });
386                    }
387                    Ok(Spanned::new(
388                        call_span,
389                        SpannedForm::List(list[1..].to_vec()),
390                    ))
391                }
392                "cons" => {
393                    let xs = template_eval_list(&items[1..], 2, "cons", bindings, call_span)?;
394                    let h = template_eval(&xs[0].1, bindings, call_span)?;
395                    let t = template_eval(&xs[1].1, bindings, call_span)?;
396                    let mut out = vec![h];
397                    match t.form {
398                        SpannedForm::List(children) => out.extend(children),
399                        SpannedForm::Nil => {}
400                        _ => out.push(t),
401                    }
402                    Ok(Spanned::new(call_span, SpannedForm::List(out)))
403                }
404                "list" => {
405                    let mut out: Vec<Spanned> = Vec::with_capacity(items.len() - 1);
406                    for child in &items[1..] {
407                        out.push(template_eval(child, bindings, call_span)?);
408                    }
409                    Ok(Spanned::new(call_span, SpannedForm::List(out)))
410                }
411                "null?" => {
412                    let xs = template_eval_list(&items[1..], 1, "null?", bindings, call_span)?;
413                    let v = template_eval(&xs[0].1, bindings, call_span)?;
414                    let is_null = matches!(&v.form, SpannedForm::Nil)
415                        || matches!(&v.form, SpannedForm::List(c) if c.is_empty());
416                    Ok(Spanned::new(
417                        call_span,
418                        SpannedForm::Atom(crate::ast::Atom::Bool(is_null)),
419                    ))
420                }
421                "pair?" => {
422                    let xs = template_eval_list(&items[1..], 1, "pair?", bindings, call_span)?;
423                    let v = template_eval(&xs[0].1, bindings, call_span)?;
424                    let ok = matches!(&v.form, SpannedForm::List(c) if !c.is_empty());
425                    Ok(Spanned::new(
426                        call_span,
427                        SpannedForm::Atom(crate::ast::Atom::Bool(ok)),
428                    ))
429                }
430                "list?" => {
431                    let xs = template_eval_list(&items[1..], 1, "list?", bindings, call_span)?;
432                    let v = template_eval(&xs[0].1, bindings, call_span)?;
433                    let ok = matches!(&v.form, SpannedForm::List(_) | SpannedForm::Nil);
434                    Ok(Spanned::new(
435                        call_span,
436                        SpannedForm::Atom(crate::ast::Atom::Bool(ok)),
437                    ))
438                }
439                "length" => {
440                    let xs = template_eval_list(&items[1..], 1, "length", bindings, call_span)?;
441                    let v = template_eval(&xs[0].1, bindings, call_span)?;
442                    let n = match &v.form {
443                        SpannedForm::Nil => 0,
444                        SpannedForm::List(c) => c.len() as i64,
445                        _ => {
446                            return Err(LispError::Compile {
447                                form: "length".into(),
448                                message: "expected a list".into(),
449                            })
450                        }
451                    };
452                    Ok(Spanned::new(
453                        call_span,
454                        SpannedForm::Atom(crate::ast::Atom::Int(n)),
455                    ))
456                }
457                "if" => {
458                    if items.len() != 4 {
459                        return Err(LispError::Compile {
460                            form: "if".into(),
461                            message: "expected (if cond then else)".into(),
462                        });
463                    }
464                    let c = template_eval(&items[1], bindings, call_span)?;
465                    let truthy = !matches!(
466                        &c.form,
467                        SpannedForm::Nil | SpannedForm::Atom(crate::ast::Atom::Bool(false))
468                    );
469                    if truthy {
470                        template_eval(&items[2], bindings, call_span)
471                    } else {
472                        template_eval(&items[3], bindings, call_span)
473                    }
474                }
475                other => Err(LispError::Compile {
476                    form: other.into(),
477                    message: "operation not supported in macro template `,expr`. Supported: \
478                         quote, car, cdr, cons, list, null?, pair?, list?, length, if"
479                        .into(),
480                }),
481            }
482        }
483    }
484}
485
486/// Helper: collect indexed (i, &Sexp) for a template-eval call's args,
487/// checking arity. Lets the call sites get clear error messages.
488fn template_eval_list<'a>(
489    args: &'a [Sexp],
490    expected: usize,
491    fn_name: &'static str,
492    _bindings: &Bindings,
493    _call_span: Span,
494) -> Result<Vec<(usize, &'a Sexp)>> {
495    if args.len() != expected {
496        return Err(LispError::Compile {
497            form: fn_name.into(),
498            message: format!("expected {expected} args, got {}", args.len()),
499        });
500    }
501    Ok(args.iter().enumerate().collect())
502}
503
504fn require_spanned_list<'a>(s: &'a Spanned, fn_name: &'static str) -> Result<&'a [Spanned]> {
505    match &s.form {
506        SpannedForm::List(c) => Ok(c.as_slice()),
507        SpannedForm::Nil => Ok(&[]),
508        _ => Err(LispError::Compile {
509            form: fn_name.into(),
510            message: "expected a list".into(),
511        }),
512    }
513}
514
515#[cfg(test)]
516mod tests {
517    use super::*;
518    use crate::reader::{read, read_spanned};
519
520    fn parse(src: &str) -> Sexp {
521        read(src).unwrap().into_iter().next().unwrap()
522    }
523
524    #[test]
525    fn identity_macro_preserves_arg_span() {
526        let src = "(defmacro id (x) `,x) (id 42)";
527        let forms = read_spanned(src).unwrap();
528        let mut e = SpannedExpander::new();
529        let out = e.expand_program(forms).unwrap();
530        assert_eq!(out.len(), 1);
531        // The result is the literal 42 from the call site.
532        assert_eq!(out[0].to_sexp(), Sexp::int(42));
533        // Its span should point at the "42" in the source, not synthetic.
534        assert!(!out[0].span.is_synthetic());
535        let expected_start = src.find("42").unwrap();
536        assert_eq!(out[0].span, Span::new(expected_start, expected_start + 2));
537    }
538
539    #[test]
540    fn wrap_macro_substitution_preserves_each_arg_span() {
541        let src = "(defmacro wrap (x) `(list ,x ,x)) (wrap hello)";
542        let forms = read_spanned(src).unwrap();
543        let mut e = SpannedExpander::new();
544        let out = e.expand_program(forms).unwrap();
545        assert_eq!(out[0].to_sexp(), parse("(list hello hello)"));
546        // The outer list span should cover the whole call site (wrap hello).
547        let SpannedForm::List(children) = &out[0].form else {
548            panic!()
549        };
550        // Literal `list` is stamped with the call-site span, not synthetic.
551        let list_span = children[0].span;
552        // Both substituted `hello` spans should be equal — they both come
553        // from the same argument in the source.
554        assert_eq!(children[1].span, children[2].span);
555        assert_ne!(children[1].span, list_span);
556        assert!(!children[1].span.is_synthetic());
557    }
558
559    #[test]
560    fn rest_param_splice_preserves_argument_spans() {
561        let src = "(defmacro call (f &rest args) `(,f ,@args)) (call foo a b c)";
562        let forms = read_spanned(src).unwrap();
563        let mut e = SpannedExpander::new();
564        let out = e.expand_program(forms).unwrap();
565        assert_eq!(out[0].to_sexp(), parse("(foo a b c)"));
566        let SpannedForm::List(children) = &out[0].form else {
567            panic!()
568        };
569        // foo, a, b, c should all have non-synthetic spans covering their
570        // positions in the source.
571        for c in children {
572            assert!(!c.span.is_synthetic(), "{:?}", c);
573        }
574    }
575
576    #[test]
577    fn nested_macro_expansion_preserves_original_arg_span() {
578        let src = "(defmacro twice (x) `(list ,x ,x))
579                   (defmacro quad (x) `(twice ,x))
580                   (quad hey)";
581        let forms = read_spanned(src).unwrap();
582        let mut e = SpannedExpander::new();
583        let out = e.expand_program(forms).unwrap();
584        assert_eq!(out[0].to_sexp(), parse("(list hey hey)"));
585        let SpannedForm::List(children) = &out[0].form else {
586            panic!()
587        };
588        // Both `hey` references should carry the argument's original span.
589        assert!(!children[1].span.is_synthetic());
590        assert_eq!(children[1].span, children[2].span);
591    }
592
593    #[test]
594    fn non_macro_form_passes_through_with_original_spans() {
595        let src = "(foo bar baz)";
596        let forms = read_spanned(src).unwrap();
597        let mut e = SpannedExpander::new();
598        let out = e.expand_program(forms).unwrap();
599        assert_eq!(out[0].to_sexp(), parse("(foo bar baz)"));
600        // Outer span covers whole source, children span their identifiers.
601        assert_eq!(out[0].span, Span::new(0, src.len()));
602    }
603
604    #[test]
605    fn unbound_unquote_errors() {
606        let src = "(defmacro bad (x) `(list ,y)) (bad 1)";
607        let forms = read_spanned(src).unwrap();
608        let mut e = SpannedExpander::new();
609        assert!(e.expand_program(forms).is_err());
610    }
611
612    #[test]
613    fn missing_required_arg_errors() {
614        let src = "(defmacro need-two (a b) `(,a ,b)) (need-two 1)";
615        let forms = read_spanned(src).unwrap();
616        let mut e = SpannedExpander::new();
617        assert!(e.expand_program(forms).is_err());
618    }
619
620    #[test]
621    fn empty_rest_splices_nothing() {
622        let src = "(defmacro f (x &rest r) `(list ,x ,@r)) (f 1)";
623        let forms = read_spanned(src).unwrap();
624        let mut e = SpannedExpander::new();
625        let out = e.expand_program(forms).unwrap();
626        assert_eq!(out[0].to_sexp(), parse("(list 1)"));
627    }
628
629    /// `&optional` with a declared default reaches this path at all — it
630    /// could not before step 5c, because `parse_params_spanned` knew only
631    /// required and `&rest` and would have rejected `&optional` as a param
632    /// NAME. Pins both arms of `OptionalParam::resolved_default` through
633    /// `MacroArgCarrier::lift_default`: supplied wins, absent falls back.
634    #[test]
635    fn optional_param_default_agrees_with_plain_expander() {
636        use crate::macro_expand::Expander;
637
638        let src = "
639            (defmacro greet (name &optional (greeting \"hi\") punct)
640              `(list ,greeting ,name ,punct))
641            (greet bob)
642            (greet bob \"yo\")
643            (greet bob \"yo\" bang)
644        ";
645        let plain_out = Expander::new().expand_program(read(src).unwrap()).unwrap();
646        let spanned_out = SpannedExpander::new()
647            .expand_program(read_spanned(src).unwrap())
648            .unwrap();
649
650        assert_eq!(plain_out.len(), 3);
651        assert_eq!(plain_out.len(), spanned_out.len());
652        for (p, s) in plain_out.iter().zip(spanned_out.iter()) {
653            assert_eq!(p, &s.to_sexp());
654        }
655        // The declared default fills the absent slot; the bare optional
656        // falls to the `Sexp::Nil` floor (which is NOT the empty list — an
657        // authored `()` reads as `Sexp::List(vec![])`).
658        let listed = |trailing: Sexp| {
659            Sexp::List(vec![
660                Sexp::symbol("list"),
661                Sexp::string("hi"),
662                Sexp::symbol("bob"),
663                trailing,
664            ])
665        };
666        assert_eq!(plain_out[0], listed(Sexp::Nil));
667        // A supplied arg wins over the declared default.
668        assert_eq!(
669            plain_out[1],
670            Sexp::List(vec![
671                Sexp::symbol("list"),
672                Sexp::string("yo"),
673                Sexp::symbol("bob"),
674                Sexp::Nil,
675            ])
676        );
677        assert_eq!(plain_out[2], parse("(list \"yo\" bob bang)"));
678    }
679
680    /// A value the CALL never supplied has no source position of its own,
681    /// so `MacroArgCarrier::lift_default` stamps it at the call site rather
682    /// than leaving it synthetic. Pins the `Site = Span` choice.
683    #[test]
684    fn absent_optional_default_wears_the_call_site_span() {
685        let src = "(defmacro f (a &optional (b 7)) `(list ,a ,b)) (f 1)";
686        let out = SpannedExpander::new()
687            .expand_program(read_spanned(src).unwrap())
688            .unwrap();
689        let SpannedForm::List(children) = &out[0].form else {
690            panic!("expected a list")
691        };
692        let call_start = src.rfind("(f 1)").unwrap();
693        let call_span = Span::new(call_start, call_start + "(f 1)".len());
694        // `,b` was never supplied — it wears the call span, not a synthetic
695        // one, and not the definition-site span of the `7` literal.
696        assert_eq!(children[2].to_sexp(), Sexp::int(7));
697        assert!(!children[2].span.is_synthetic());
698        assert_eq!(children[2].span, call_span);
699    }
700
701    /// Surplus args against a rest-less param list are a rejection on BOTH
702    /// paths. Before step 5c the spanned binder silently dropped them while
703    /// the plain binder raised `TooManyMacroArgs` — the exact class of
704    /// two-implementations divergence the shared binder removes.
705    #[test]
706    fn surplus_args_rejected_like_plain_expander() {
707        use crate::macro_expand::Expander;
708
709        let src = "(defmacro two (a b) `(list ,a ,b)) (two 1 2 3)";
710        let plain = Expander::new().expand_program(read(src).unwrap());
711        let spanned = SpannedExpander::new().expand_program(read_spanned(src).unwrap());
712        assert!(plain.is_err(), "plain expander accepted a surplus arg");
713        assert!(spanned.is_err(), "spanned expander accepted a surplus arg");
714    }
715
716    /// A malformed lambda list is rejected identically on both paths,
717    /// because there is exactly one `parse_params`.
718    #[test]
719    fn malformed_lambda_list_rejected_like_plain_expander() {
720        use crate::macro_expand::Expander;
721
722        for src in [
723            // `&rest` with no name.
724            "(defmacro f (a &rest) `(list ,a))",
725            // tokens trailing the `&rest` name.
726            "(defmacro f (a &rest r junk) `(list ,a))",
727            // `(name default)` optional spec with no default.
728            "(defmacro f (&optional (b)) `(list ,b))",
729            // non-symbol in the required run.
730            "(defmacro f (1) `(list))",
731        ] {
732            let plain = Expander::new().expand_program(read(src).unwrap());
733            let spanned = SpannedExpander::new().expand_program(read_spanned(src).unwrap());
734            assert!(plain.is_err(), "plain expander accepted: {src}");
735            assert!(spanned.is_err(), "spanned expander accepted: {src}");
736        }
737    }
738
739    #[test]
740    fn agrees_with_plain_expander_on_output() {
741        use crate::macro_expand::Expander;
742
743        let src = "
744            (defmacro wrap (x) `(list ,x ,x))
745            (defmacro call (f &rest args) `(,f ,@args))
746            (defmacro twice (x) `(list ,x ,x))
747            (defmacro quad (x) `(twice ,x))
748            (wrap hello)
749            (call foo a b c)
750            (quad hey)
751            (outer (wrap deep))
752        ";
753        let plain_forms = read(src).unwrap();
754        let spanned_forms = read_spanned(src).unwrap();
755
756        let mut plain = Expander::new();
757        let plain_out = plain.expand_program(plain_forms).unwrap();
758
759        let mut spanned = SpannedExpander::new();
760        let spanned_out = spanned.expand_program(spanned_forms).unwrap();
761
762        assert_eq!(plain_out.len(), spanned_out.len());
763        for (p, s) in plain_out.iter().zip(spanned_out.iter()) {
764            assert_eq!(p, &s.to_sexp());
765        }
766    }
767}