Skip to main content

vla_lang/
expand.rs

1//! The macro system: VLA.bas's `DefineMacro`, `ExpandMacros`, `ExpandOne`,
2//! `Substitute` with the QUASIQUOTE bundle, the twenty-seven LISTOPS
3//! primitives, `ExpandOnePass`, and `VlaExpandText` (PORT.5, slice 5d; the
4//! expand-time object model's ten, VLA.3); and `formula-expand`, the formula
5//! syntax at expand time, an operation of the walk that expands its argument
6//! before writing it (`ExpandFormulaCall`, VLA.4).
7//!
8//! Template-substitution macros: `(defmacro (name p1 p2 & rest) template
9//! ...)`. A parameter is replaced by the form passed at the call site; the
10//! rest parameter splices its forms into whatever list it stands in; several
11//! template forms become one `(begin ...)`. No gensym, no compile-time
12//! computation beyond the expand-time primitives, by design.
13//!
14//! The shape is the VBA's, function for function. `ExpandMacros` is the
15//! hand-made trampoline: a form whose head is a macro is substituted and
16//! looked at again, in a loop, and a `(begin ...)` a macro made (its line
17//! is 0) is unwrapped around its last element, so a self-recursive walker's
18//! chain length never becomes stack depth; the forms it set aside are
19//! combined back into one `begin` at the end. Everything the macro system
20//! makes carries line 0 (S3.1's explicit zero); a user's argument forms
21//! keep their lines as they are carried in.
22//!
23//! The forms are the VBA's too (VLA.2). A [`Form`] is an owned tree, so a
24//! copy of a list copies everything under it; VLA.bas passes a list by
25//! reference and makes `cdr` a view (`VlaSlice`, since P-NTH), so a macro
26//! that walks a list of N rows pays for N rows, not N squared. The expander
27//! holds the forms it works on as nodes of its own, which share as the
28//! reference shares: a parameter's value at each use, a quoted argument's
29//! content, `car`, `cdr` and `cddr`, a call's arguments and the elements a
30//! trampoline sets aside. What the reference copies, the items of `cons`,
31//! `list`, the rest parameter and `unquote-splicing`, are copied here too,
32//! as references to the same items. A text is read into nodes once and the
33//! expansion written out as forms once.
34//!
35//! The refusals are the catalogue's, by id, in the same situations.
36
37use std::collections::HashMap;
38use std::sync::{Arc, OnceLock};
39
40use crate::form::{sym_text, Form, List};
41use crate::formula_syntax;
42use crate::intrinsics::{fold, is_numeric_literal_text, str_, val};
43use crate::messages::{raise, Refusal};
44use crate::printer::write_pretty;
45use crate::reader::{count_lf, parse_all, src_line_tag, tokenize, FORM_DEPTH_LIMIT};
46use crate::refers::letters_to_col;
47use crate::sheet::{column_letters, MAX_COLUMN, MAX_ROW};
48
49/// One macro, as `DefineMacro` records it.
50#[derive(Clone, Debug)]
51pub struct Macro {
52    /// The parameter names, as written.
53    pub params: Vec<String>,
54    /// The rest parameter's name as written, or empty.
55    pub rest_name: String,
56    pub template: Form,
57    /// The docstring (L17), or empty.
58    pub doc: String,
59    /// The name as written.
60    pub name: String,
61}
62
63/// The expand-time names a macro may not take, each with the description
64/// its refusal gives (`DefineMacro`'s list, in its order).
65const RESERVED: &[(&str, &str)] = &[
66    ("quote", "data-literal form"),
67    ("quasiquote", "template-shielding form"),
68    ("unquote", "quasiquote escape form"),
69    ("unquote-splicing", "quasiquote splice form"),
70    ("symbol", "identifier-fusion primitive"),
71    ("car", "list-head primitive"),
72    ("cdr", "list-tail primitive"),
73    ("cddr", "list-tail primitive"),
74    ("cons", "list-construction primitive"),
75    ("list", "list-construction primitive"),
76    ("null?", "empty-list primitive"),
77    ("eq?", "atom-comparison primitive"),
78    ("equal?", "structural-comparison primitive"),
79    ("quote-if", "expand-time conditional"),
80    (
81        "cond",
82        "multi-clause conditional (folds at expand time when possible, defers to runtime otherwise)",
83    ),
84    ("+expand", "expand-time addition primitive"),
85    ("=expand", "expand-time equality primitive"),
86    ("<>expand", "expand-time inequality primitive"),
87    (">expand", "expand-time comparison primitive"),
88    ("<expand", "expand-time comparison primitive"),
89    (">=expand", "expand-time comparison primitive"),
90    ("<=expand", "expand-time comparison primitive"),
91    // VLA.3: the expand-time object model's ten.
92    ("-expand", "expand-time subtraction primitive"),
93    ("*expand", "expand-time multiplication primitive"),
94    ("quotient-expand", "expand-time integer quotient primitive"),
95    ("mod-expand", "expand-time remainder primitive"),
96    ("&expand", "expand-time text-joining primitive"),
97    ("characters-expand", "expand-time text-splitting primitive"),
98    ("address-expand", "expand-time address primitive"),
99    ("row-expand", "expand-time row-number primitive"),
100    ("column-expand", "expand-time column-number primitive"),
101    ("column-letters-expand", "expand-time column-letters primitive"),
102    // VLA.4: the formula syntax at expand time, an operation of the walk.
103    ("formula-expand", "expand-time formula-syntax operation"),
104];
105
106/// The twenty-seven LISTOPS primitives `Substitute` and `ExpandMacros` both
107/// recognise at a form's head: the original nine, LISTOPS-EXPAND's seven,
108/// `cond`, and the expand-time object model's ten (VLA.3).
109const LISTOPS: &[&str] = &[
110    "car",
111    "cdr",
112    "cddr",
113    "cons",
114    "list",
115    "null?",
116    "eq?",
117    "equal?",
118    "quote-if",
119    "cond",
120    "+expand",
121    "=expand",
122    "<>expand",
123    ">expand",
124    "<expand",
125    ">=expand",
126    "<=expand",
127    "-expand",
128    "*expand",
129    "quotient-expand",
130    "mod-expand",
131    "&expand",
132    "characters-expand",
133    "address-expand",
134    "row-expand",
135    "column-expand",
136    "column-letters-expand",
137];
138
139fn is_listops(head_fold: &str) -> bool {
140    LISTOPS.contains(&head_fold)
141}
142
143/// A form as the expander holds it (VLA.2): VLA.bas's own model, where an
144/// atom is a text and a list a `Collection` passed by reference. Cloning a
145/// node clones a reference, never the tree under it.
146#[derive(Clone, Debug)]
147enum Node {
148    Sym(Arc<str>),
149    Str(Arc<str>),
150    List(Seq),
151}
152
153/// A list: the items of a shared [`Run`] from `start` on, under a line of
154/// its own. At `start` 0 it is the reference's `Collection`; past it, its
155/// `VlaSlice`, the view `cdr` makes. Its weight is kept beside it, made
156/// when it is made: `forms`, the forms in its tree as the printer would
157/// write them, itself among them (a list shared twice is counted twice,
158/// saturating), and `depth`, its nesting, itself 1.
159#[derive(Clone, Debug)]
160struct Seq {
161    run: Arc<Run>,
162    start: usize,
163    line: u32,
164    forms: u32,
165    depth: u32,
166}
167
168/// The items a list was made with, shared by every view of them.
169#[derive(Debug)]
170struct Run {
171    items: Vec<Node>,
172    /// For each `k`, the deepest list among `items[k..]` (0 for none),
173    /// made the first time a view of this run is made, so that a view's
174    /// depth is read and not walked.
175    deepest: OnceLock<Vec<u32>>,
176}
177
178impl Run {
179    fn deepest_from(&self, k: usize) -> u32 {
180        let deepest = self.deepest.get_or_init(|| {
181            let mut v = vec![0; self.items.len() + 1];
182            for i in (0..self.items.len()).rev() {
183                v[i] = v[i + 1].max(self.items[i].weight().1);
184            }
185            v
186        });
187        deepest[k]
188    }
189}
190
191impl Seq {
192    fn new(items: Vec<Node>, line: u32) -> Seq {
193        let (mut forms, mut deepest) = (1u32, 0u32);
194        for item in &items {
195            let (f, d) = item.weight();
196            forms = forms.saturating_add(f);
197            deepest = deepest.max(d);
198        }
199        Seq {
200            run: Arc::new(Run {
201                items,
202                deepest: OnceLock::new(),
203            }),
204            start: 0,
205            line,
206            forms,
207            depth: deepest.saturating_add(1),
208        }
209    }
210
211    fn items(&self) -> &[Node] {
212        &self.run.items[self.start..]
213    }
214
215    fn count(&self) -> usize {
216        self.run.items.len() - self.start
217    }
218
219    /// VBA `ListTail`'s view: the items from position `n + 1` on, the same
220    /// items, untagged. Its forms are this list's less the `n` it passes;
221    /// a count that saturated says only "at least", so it stays saturated
222    /// and the walk in [`Expander::weigh`] decides.
223    fn tail(&self, n: usize) -> Seq {
224        let passed = self.items()[..n]
225            .iter()
226            .fold(0u32, |sum, item| sum.saturating_add(item.weight().0));
227        let start = self.start + n;
228        Seq {
229            run: Arc::clone(&self.run),
230            start,
231            line: 0,
232            forms: if self.forms == u32::MAX {
233                u32::MAX
234            } else {
235                self.forms - passed
236            },
237            depth: self.run.deepest_from(start).saturating_add(1),
238        }
239    }
240}
241
242impl Node {
243    fn sym(text: &str) -> Node {
244        Node::Sym(Arc::from(text))
245    }
246
247    /// A list with no line, as every list the macro system makes has none.
248    fn list(items: Vec<Node>) -> Node {
249        Node::List(Seq::new(items, 0))
250    }
251
252    /// A form read into nodes, every list keeping its line.
253    fn of(f: &Form) -> Node {
254        match f {
255            Form::Sym(s) => Node::Sym(Arc::from(s.as_str())),
256            Form::Str(s) => Node::Str(Arc::from(s.as_str())),
257            Form::List(l) => Node::List(Seq::new(l.items.iter().map(Node::of).collect(), l.line)),
258        }
259    }
260
261    /// The node written out as a form: a list shared in two places is two
262    /// lists of the form, as the reference's printer writes it twice.
263    fn form(&self) -> Form {
264        match self {
265            Node::Sym(s) => Form::Sym(s.to_string()),
266            Node::Str(s) => Form::Str(s.to_string()),
267            Node::List(l) => Form::List(List::new(
268                l.items().iter().map(Node::form).collect(),
269                l.line,
270            )),
271        }
272    }
273
274    /// The forms in the node's tree and its nesting: an atom is one form
275    /// and no list.
276    fn weight(&self) -> (u32, u32) {
277        match self {
278            Node::List(l) => (l.forms, l.depth),
279            _ => (1, 0),
280        }
281    }
282
283    fn is_list(&self) -> bool {
284        matches!(self, Node::List(_))
285    }
286
287    /// [`Form::atom_text`]: a symbol's text, or a string literal's content
288    /// behind its quote marker. `None` for a list.
289    fn atom_text(&self) -> Option<String> {
290        match self {
291            Node::Sym(s) => Some(s.to_string()),
292            Node::Str(s) => Some(format!("\"{s}")),
293            Node::List(_) => None,
294        }
295    }
296
297    /// VBA `ListHeadIs`: a list whose first element is a symbol folding
298    /// equal to `name`.
299    fn head_is(&self, name: &str) -> bool {
300        match self {
301            Node::List(l) => {
302                matches!(l.items().first(), Some(Node::Sym(s)) if fold(s) == fold(name))
303            }
304            _ => false,
305        }
306    }
307}
308
309/// VBA `Nth` over a list's items: element `i`, 1-based, refusing past the
310/// end ([`List::nth`]'s refusal).
311fn nth(items: &[Node], i: usize) -> Result<&Node, Refusal> {
312    if i == 0 || i > items.len() {
313        return Err(raise("vla-form-missing-element", &[("n", &i.to_string())]));
314    }
315    Ok(&items[i - 1])
316}
317
318/// VBA `NthList`: element `i` as a list, refusing an atom there
319/// ([`List::nth_list`]'s refusal).
320fn nth_list(items: &[Node], i: usize) -> Result<&Seq, Refusal> {
321    match nth(items, i)? {
322        Node::List(l) => Ok(l),
323        other => Err(raise(
324            "vla-expected-list-at-position",
325            &[
326                ("n", &i.to_string()),
327                ("value", &other.atom_text().unwrap_or_default()),
328            ],
329        )),
330    }
331}
332
333/// The substitution environment of one macro application: `Substitute`'s
334/// `bindings`, `restName` (folded) and `restItems`. Empty for a primitive
335/// met outside any template.
336#[derive(Default)]
337struct Env<'r> {
338    bindings: HashMap<&'r str, Node>,
339    rest_name: &'r str,
340    rest_items: Vec<Node>,
341}
342
343/// A macro as the expander applies it (VLA.2): its parameters folded and
344/// its template read into nodes once, when it is defined.
345struct Rule {
346    params: Vec<String>,
347    rest_name: String,
348    template: Node,
349}
350
351/// The macro table and the application counters: `mMacros`, `mBudgetOn`,
352/// `mExpandBudget` and `mExpandFired`.
353#[derive(Default)]
354pub struct Expander {
355    macros: HashMap<String, Macro>,
356    /// Each macro of `macros` as it is applied, under the same name.
357    rules: HashMap<String, Rule>,
358    /// L15: when on, applications beyond the budget do not fire.
359    pub budget_on: bool,
360    pub expand_budget: u64,
361    /// L1: macro applications since the last reset.
362    pub expand_fired: u64,
363    /// VLA.4: whether the walk stands inside a `formula-expand`'s argument,
364    /// where another one is refused.
365    formula_open: bool,
366}
367
368/// The most forms one macro application's result may hold (VLA.1), counted
369/// as the printer would write them, so a value its template uses twice
370/// counts twice. A macro that doubles its argument at every application is
371/// refused at its nineteenth, long before the chain's 5,000: its value is
372/// shared and cheap to hold (VLA.2), but its expansion is written out as a
373/// tree. One application's result, not the program's sum: a walk over a
374/// long list holds the rest of the list in every result, so a sum grows as
375/// the square of the list. The largest form the corpus expands to is 20,144
376/// forms; the table generator's first application holds 30,041 at 2,499
377/// rows, the most its chain allows at two links a row.
378pub const EXPANSION_SIZE_LIMIT: usize = 1_000_000;
379
380impl Expander {
381    pub fn new() -> Expander {
382        Expander::default()
383    }
384
385    /// VBA `GetMacro`: by name, folded.
386    pub fn get_macro(&self, name: &str) -> Option<&Macro> {
387        self.macros.get(&fold(name))
388    }
389
390    /// VBA `VlaMacroDoc`: a macro's docstring, or empty.
391    pub fn macro_doc(&self, name: &str) -> String {
392        self.get_macro(name)
393            .map(|m| m.doc.clone())
394            .unwrap_or_default()
395    }
396
397    /// VBA `DefineMacro`: `(defmacro (name params... [& rest]) ["doc"]
398    /// template...)`.
399    pub fn define_macro(&mut self, f: &List) -> Result<(), Refusal> {
400        let sig = f.nth_list(2)?;
401        let mname = sig.head_sym()?;
402        let mfold = fold(&mname);
403        if let Some((word, desc)) = RESERVED.iter().find(|(w, _)| *w == mfold) {
404            return Err(raise(
405                "vla-defmacro-reserved-name",
406                &[("word", word), ("desc", desc)],
407            ));
408        }
409
410        let mut params = Vec::new();
411        let mut rest_name = String::new();
412        let mut j = 2;
413        while j <= sig.count() {
414            let t = sym_text(sig.nth(j)?)?;
415            if t == "&" {
416                if j + 1 > sig.count() {
417                    return Err(raise(
418                        "vla-defmacro-rest-param-missing",
419                        &[("name", &mname)],
420                    ));
421                }
422                rest_name = sym_text(sig.nth(j + 1)?)?;
423                if j + 1 != sig.count() {
424                    return Err(raise(
425                        "vla-defmacro-rest-param-not-last",
426                        &[("name", &mname)],
427                    ));
428                }
429                break;
430            }
431            params.push(t);
432            j += 1;
433        }
434
435        // L17: a string at form 3 is documentation only when at least one
436        // more form follows; a lone string is the template.
437        let mut doc = String::new();
438        let mut doc_idx = 3;
439        if f.count() >= 4 {
440            if let Form::Str(s) = f.nth(3)? {
441                doc = s.clone();
442                doc_idx = 4;
443            }
444        }
445
446        let template = if f.count() == doc_idx {
447            f.nth(doc_idx)?.clone()
448        } else if f.count() > doc_idx {
449            let mut items = vec![Form::sym("begin")];
450            items.extend(f.items[doc_idx - 1..].iter().cloned());
451            Form::list(items)
452        } else {
453            return Err(raise("vla-defmacro-missing-template", &[("name", &mname)]));
454        };
455
456        // A redefinition overwrites, as the Dictionary's keyed assignment does.
457        self.rules.insert(
458            mfold.clone(),
459            Rule {
460                params: params.iter().map(|p| fold(p)).collect(),
461                rest_name: fold(&rest_name),
462                template: Node::of(&template),
463            },
464        );
465        self.macros.insert(
466            mfold,
467            Macro {
468                params,
469                rest_name,
470                template,
471                doc,
472                name: mname,
473            },
474        );
475        Ok(())
476    }
477
478    /// VBA `ExpandMacros`: a form to its fixpoint.
479    pub fn expand(&mut self, d: &Form, depth: u32) -> Result<Form, Refusal> {
480        self.expand_at(&Node::of(d), depth, 1)
481    }
482
483    /// [`Expander::expand`] at `walk`, the depth of the list `d` would be
484    /// within its top-level form (VLA.1): a list deeper than the reader's
485    /// limit is refused, whether a text or a macro nested it so.
486    fn expand_at(&mut self, d: &Node, depth: u32, walk: u32) -> Result<Form, Refusal> {
487        if let Node::List(lst) = d {
488            if walk > FORM_DEPTH_LIMIT {
489                return Err(too_deep(lst.line));
490            }
491        }
492        if depth > 200 {
493            return Err(raise("vla-macro-expansion-too-deep", &[]));
494        }
495        let mut cur = d.clone();
496        let mut chain_len: u32 = 0;
497        let mut pending_head: Vec<Node> = Vec::new();
498        loop {
499            let Node::List(lst) = &cur else {
500                return Ok(cur.form());
501            };
502            let items = lst.items();
503            let Some(Node::Sym(head)) = items.first() else {
504                break;
505            };
506            let head_fold = fold(head);
507            // P.L5: quote is a data context; expansion never enters.
508            if head_fold == "quote" {
509                return Ok(cur.form());
510            }
511            // QUASIQUOTE: (symbol ...) needs no ambient bindings.
512            if head_fold == "symbol" {
513                return Ok(fuse_symbol(items, &Env::default())?.form());
514            }
515            // VLA.4: the formula syntax, its argument expanded first.
516            if head_fold == "formula-expand" {
517                return self.formula_expand(items, depth, walk);
518            }
519            if matches!(
520                head_fold.as_str(),
521                "quasiquote" | "unquote" | "unquote-splicing"
522            ) {
523                return Err(raise(
524                    "vla-quasiquote-outside-template",
525                    &[("head", &head_fold)],
526                ));
527            }
528            // LISTOPS: the twenty-seven primitives, standalone.
529            if is_listops(&head_fold) {
530                return Ok(eval_listops_prim(&head_fold, items, &Env::default())?.form());
531            }
532            if head_fold == "begin" && items.len() > 1 && lst.line == 0 {
533                // TABLESPEC-SCALE: a begin a macro made is a tail-position
534                // wrapper; its other elements wait, its last is looked at
535                // again.
536                pending_head.extend(items[1..items.len() - 1].iter().cloned());
537                chain_len += 1;
538                if chain_len > 5000 {
539                    return Err(raise("vla-macro-self-expansion-limit", &[]));
540                }
541                let last = items[items.len() - 1].clone();
542                cur = last;
543            } else if head_fold == "begin" {
544                break;
545            } else {
546                let Some(rule) = self.rules.get(&head_fold) else {
547                    break;
548                };
549                if self.budget_on && self.expand_fired >= self.expand_budget {
550                    break; // L15
551                }
552                chain_len += 1;
553                if chain_len > 5000 {
554                    return Err(raise("vla-macro-self-expansion-limit", &[]));
555                }
556                let r = expand_one(items, rule)?;
557                self.weigh(&r)?;
558                self.expand_fired += 1; // L1: one per application
559                cur = r;
560            }
561        }
562        // No macro at the head: rebuild with expanded elements, keeping the
563        // list's own line (C2).
564        let Node::List(cur_list) = &cur else {
565            unreachable!("the loop leaves only on a list");
566        };
567        let mut out = Vec::with_capacity(cur_list.count());
568        for e in cur_list.items() {
569            out.push(self.expand_at(e, depth, walk + 1)?);
570        }
571        let out = Form::List(List::new(out, cur_list.line));
572        if pending_head.is_empty() {
573            return Ok(out);
574        }
575        let mut final_out = vec![Form::sym("begin")];
576        for e in &pending_head {
577            final_out.push(self.expand_at(e, depth, walk + 1)?);
578        }
579        final_out.push(out);
580        Ok(Form::list(final_out))
581    }
582
583    /// VLA.4: `(formula-expand <expression>)`, VBA `ExpandFormulaCall`: the
584    /// expression expanded to its fixpoint, then written by the formula
585    /// syntax as one text, the formula's with its `=`. Not one of the
586    /// LISTOPS primitives, which take their arguments as data and are
587    /// computed at substitution too: `Substitute` leaves this form alone, so
588    /// a template's parameters are filled in inside it, and the walk expands
589    /// its argument before writing it, so that an author's macros inside the
590    /// expression fire and a macro's call is never written as a function's.
591    /// One inside another's argument is refused, since its text would be a
592    /// text there and not the formula it wrote.
593    fn formula_expand(&mut self, items: &[Node], depth: u32, walk: u32) -> Result<Form, Refusal> {
594        if items.len() != 2 {
595            return Err(raise(
596                "vla-formula-expand-arity",
597                &[("n", &(items.len() - 1).to_string())],
598            ));
599        }
600        if self.formula_open {
601            return Err(raise("vla-formula-expand-nested", &[]));
602        }
603        self.formula_open = true;
604        let expr = self.expand_at(&items[1], depth, walk + 1);
605        self.formula_open = false;
606        Ok(Form::Str(formula_syntax::write(&expr?)?))
607    }
608
609    /// VBA `ExpandOnePass`: top-down; a list whose head is a macro is
610    /// replaced by its substitution and not walked further.
611    pub fn expand_one_pass(&mut self, d: &Form) -> Result<Form, Refusal> {
612        self.expand_one_pass_at(&Node::of(d), 1)
613    }
614
615    /// [`Expander::expand_one_pass`] at `walk`, held to the reader's limit
616    /// as [`Expander::expand`]'s walk is (VLA.1).
617    fn expand_one_pass_at(&mut self, d: &Node, walk: u32) -> Result<Form, Refusal> {
618        let Node::List(lst) = d else {
619            return Ok(d.form());
620        };
621        if walk > FORM_DEPTH_LIMIT {
622            return Err(too_deep(lst.line));
623        }
624        let items = lst.items();
625        if let Some(Node::Sym(head)) = items.first() {
626            let head_fold = fold(head);
627            if head_fold == "quote" {
628                return Ok(d.form());
629            }
630            if let Some(rule) = self.rules.get(&head_fold) {
631                if !(self.budget_on && self.expand_fired >= self.expand_budget) {
632                    let r = expand_one(items, rule)?;
633                    self.weigh(&r)?;
634                    self.expand_fired += 1;
635                    return Ok(r.form());
636                }
637            }
638        }
639        let mut out = Vec::with_capacity(items.len());
640        for e in items {
641            out.push(self.expand_one_pass_at(e, walk + 1)?);
642        }
643        Ok(Form::List(List::new(out, lst.line)))
644    }
645
646    /// VLA.1: one application's result, weighed before it is used: no list
647    /// in it nested past the reader's limit, and no more than
648    /// [`EXPANSION_SIZE_LIMIT`] forms in it. So a macro that deepens its
649    /// argument on every application is refused at the depth the walk would
650    /// refuse it, before a form thousands deep runs the stack out, and one
651    /// that doubles it is refused within twenty applications. The result's
652    /// weight was made with it (VLA.2), so a walk that holds the rest of
653    /// its list in every result does not walk that rest at every step; only
654    /// a result past a limit is walked, a stack of its own since the form
655    /// is what may be deep, and in the order the copies were walked, so
656    /// the refusal and the list it names are the same.
657    fn weigh(&self, r: &Node) -> Result<(), Refusal> {
658        let (forms, depth) = r.weight();
659        if forms as usize <= EXPANSION_SIZE_LIMIT && depth <= FORM_DEPTH_LIMIT {
660            return Ok(());
661        }
662        let mut forms = 0usize;
663        let mut stack = vec![(r, 1u32)];
664        while let Some((f, depth)) = stack.pop() {
665            forms += 1;
666            if forms > EXPANSION_SIZE_LIMIT {
667                return Err(raise(
668                    "vla-macro-expansion-too-large",
669                    &[("limit", "1,000,000")],
670                ));
671            }
672            if let Node::List(l) = f {
673                if depth > FORM_DEPTH_LIMIT {
674                    return Err(too_deep(l.line));
675                }
676                stack.extend(l.items().iter().map(|item| (item, depth + 1)));
677            }
678        }
679        Ok(())
680    }
681
682    /// `VlaTranspile`'s and `VlaExpandText`'s pass 1 over the prelude and
683    /// the source read together: every `defmacro` is defined, in order, and
684    /// the other forms come back as the body.
685    pub fn collect(&mut self, forms: Vec<Form>) -> Result<Vec<Form>, Refusal> {
686        let mut body = Vec::with_capacity(forms.len());
687        for f in forms {
688            if f.head_is("defmacro") {
689                let Form::List(l) = &f else { unreachable!() };
690                self.define_macro(l)?;
691            } else {
692                body.push(f);
693            }
694        }
695        Ok(body)
696    }
697}
698
699/// VBA `VlaExpandText`: the prelude and the source read together, every
700/// `defmacro` defined, every other form expanded (to its fixpoint, or one
701/// pass) and written pretty, one per line. Returns the text and the number
702/// of applications that fired.
703pub fn expand_text(
704    source: &str,
705    prelude: &str,
706    to_fixpoint: bool,
707) -> Result<(String, u64), Refusal> {
708    let line_offset = count_lf(prelude) as u32 + 1;
709    let text = format!("{prelude}\r\n{source}");
710    let forms = parse_all(&tokenize(&text), line_offset)?;
711    let mut ex = Expander::new();
712    let body = ex.collect(forms)?;
713    let mut out = String::new();
714    for f in &body {
715        let expanded = if to_fixpoint {
716            ex.expand(f, 0)?
717        } else {
718            ex.expand_one_pass(f)?
719        };
720        out.push_str(&write_pretty(&expanded, 0));
721        out.push_str("\r\n");
722    }
723    Ok((out, ex.expand_fired))
724}
725
726/// The walk's refusal of a list past the reader's limit, at the list's own
727/// line, or at none for a list a macro made (VLA.1).
728fn too_deep(line: u32) -> Refusal {
729    raise(
730        "vla-form-too-deep",
731        &[
732            ("limit", &FORM_DEPTH_LIMIT.to_string()),
733            ("loc", &src_line_tag(line, 0)),
734        ],
735    )
736}
737
738/// VBA `ExpandOne`: one application of a macro to a call form's items. A
739/// parameter is bound to its argument itself, not a copy of it.
740fn expand_one(call: &[Node], rule: &Rule) -> Result<Node, Refusal> {
741    let argc = call.len() - 1;
742    let name = call[0].atom_text().unwrap_or_default();
743    if rule.rest_name.is_empty() {
744        if argc != rule.params.len() {
745            return Err(raise(
746                "vla-macro-arity",
747                &[
748                    ("name", &name),
749                    ("n", &rule.params.len().to_string()),
750                    ("given", &argc.to_string()),
751                ],
752            ));
753        }
754    } else if argc < rule.params.len() {
755        return Err(raise(
756            "vla-macro-arity-min",
757            &[
758                ("name", &name),
759                ("n", &rule.params.len().to_string()),
760                ("given", &argc.to_string()),
761            ],
762        ));
763    }
764    let mut env = Env {
765        bindings: HashMap::new(),
766        rest_name: &rule.rest_name,
767        rest_items: Vec::new(),
768    };
769    for (j, p) in rule.params.iter().enumerate() {
770        env.bindings.insert(p, call[j + 1].clone());
771    }
772    env.rest_items
773        .extend(call[rule.params.len() + 1..].iter().cloned());
774    substitute(&rule.template, &env, false)
775}
776
777/// VBA `Substitute`: a template with its parameters replaced, each by its
778/// value itself (`Set Substitute = v`). Inside a quasiquote shield
779/// (`in_quasi`) a bare symbol is data, and only an explicit `unquote` or
780/// `unquote-splicing` reaches the bindings.
781fn substitute(t: &Node, env: &Env, in_quasi: bool) -> Result<Node, Refusal> {
782    let lst = match t {
783        Node::List(l) => l,
784        Node::Sym(s) if !in_quasi => {
785            let key = fold(s);
786            if !env.rest_name.is_empty() && key == env.rest_name {
787                // The rest parameter standalone: the whole list.
788                return Ok(Node::list(env.rest_items.clone()));
789            }
790            if let Some(v) = env.bindings.get(key.as_str()) {
791                return Ok(v.clone());
792            }
793            return Ok(t.clone());
794        }
795        _ => return Ok(t.clone()),
796    };
797    let items = lst.items();
798
799    if let Some(Node::Sym(head)) = items.first() {
800        let h = fold(head);
801        match h.as_str() {
802            "symbol" => return fuse_symbol(items, env),
803            "quasiquote" => {
804                if in_quasi {
805                    return Err(raise("vla-quasiquote-nested", &[]));
806                }
807                if items.len() != 2 {
808                    return Err(raise(
809                        "vla-quasiquote-arity",
810                        &[("n", &(items.len() - 1).to_string())],
811                    ));
812                }
813                return substitute(nth(items, 2)?, env, true);
814            }
815            "unquote" => {
816                if !in_quasi {
817                    return Err(raise("vla-unquote-outside-quasiquote", &[]));
818                }
819                if items.len() != 2 {
820                    return Err(raise(
821                        "vla-unquote-arity",
822                        &[("n", &(items.len() - 1).to_string())],
823                    ));
824                }
825                return substitute(nth(items, 2)?, env, false);
826            }
827            "unquote-splicing" => {
828                // A splice in its place is taken by the element walk below;
829                // reaching here it is a bare value, or outside a shield.
830                if !in_quasi {
831                    return Err(raise("vla-unquote-splicing-outside-quasiquote", &[]));
832                }
833                return Err(raise("vla-unquote-splicing-not-element", &[]));
834            }
835            _ if is_listops(&h) && !in_quasi => {
836                return eval_listops_prim(&h, items, env);
837            }
838            _ => {}
839        }
840    }
841
842    let mut out = Vec::with_capacity(items.len());
843    for e in items {
844        let mut spliced = false;
845        if in_quasi && e.head_is("unquote-splicing") {
846            let Node::List(spl) = e else { unreachable!() };
847            let spl = spl.items();
848            if spl.len() != 2 {
849                return Err(raise(
850                    "vla-unquote-splicing-arity",
851                    &[("n", &(spl.len() - 1).to_string())],
852                ));
853            }
854            let val = substitute(nth(spl, 2)?, env, false)?;
855            let Node::List(val) = val else {
856                return Err(raise("vla-unquote-splicing-not-list", &[]));
857            };
858            out.extend(val.items().iter().cloned());
859            spliced = true;
860        } else if !in_quasi {
861            if let Node::Sym(s) = e {
862                if !env.rest_name.is_empty() && fold(s) == env.rest_name {
863                    // The rest parameter as an element: its forms spliced in.
864                    out.extend(env.rest_items.iter().cloned());
865                    spliced = true;
866                }
867            }
868        }
869        if !spliced {
870            out.push(substitute(e, env, in_quasi)?);
871        }
872    }
873    // S3.1: a template copy is explicitly untagged; the user's own forms
874    // spliced in above keep their lines.
875    Ok(Node::list(out))
876}
877
878/// VBA `FuseSymbol`: `(symbol a b ...)` fuses each argument's resolved
879/// text into one new atom. A fused text that begins with a quote mark is
880/// a string literal, as the VBA's leading-quote marker makes it.
881fn fuse_symbol(lst: &[Node], env: &Env) -> Result<Node, Refusal> {
882    if lst.len() < 2 {
883        return Err(raise("vla-symbol-arity", &[]));
884    }
885    let mut fused = String::new();
886    for k in 2..=lst.len() {
887        let piece = substitute(nth(lst, k)?, env, false)?;
888        match piece {
889            Node::List(_) => {
890                return Err(raise(
891                    "vla-symbol-arg-is-list",
892                    &[("n", &(k - 1).to_string())],
893                ));
894            }
895            Node::Str(s) | Node::Sym(s) => fused.push_str(&s),
896        }
897    }
898    Ok(match fused.strip_prefix('"') {
899        Some(rest) => Node::Str(Arc::from(rest)),
900        None => Node::Sym(Arc::from(fused.as_str())),
901    })
902}
903
904/// VBA `EvalListopsPrim`: the dispatcher the two recognition sites share,
905/// over the primitive's call form.
906fn eval_listops_prim(head_fold: &str, lst: &[Node], env: &Env) -> Result<Node, Refusal> {
907    match head_fold {
908        "car" => eval_car(lst, env),
909        "cdr" => eval_cdr(lst, env),
910        "cddr" => eval_cddr(lst, env),
911        "cons" => eval_cons(lst, env),
912        "list" => eval_list(lst, env),
913        "null?" => eval_nullq(lst, env),
914        "eq?" => eval_eq(lst, env),
915        "equal?" => eval_equal(lst, env),
916        "quote-if" => eval_quote_if(lst, env),
917        "cond" => eval_cond_from(lst, 2, env),
918        "&expand" => eval_concat_expand(lst, env),
919        "characters-expand" => eval_characters_expand(lst, env),
920        "address-expand" => eval_address_expand(lst, env),
921        "row-expand" | "column-expand" => eval_address_part_expand(head_fold, lst, env),
922        "column-letters-expand" => eval_column_letters_expand(lst, env),
923        _ => eval_arith_expand(head_fold, lst, env),
924    }
925}
926
927/// VBA `ResolveListopsArg`: the argument through `Substitute`, then one
928/// top-level `(quote X)` unwrapped to its data, X itself.
929fn resolve_listops_arg(arg: &Node, env: &Env) -> Result<Node, Refusal> {
930    let v = substitute(arg, env, false)?;
931    if v.head_is("quote") {
932        let Node::List(qc) = &v else { unreachable!() };
933        let qc = qc.items();
934        if qc.len() != 2 {
935            return Err(raise("vla-quote-arity", &[]));
936        }
937        return Ok(qc[1].clone());
938    }
939    Ok(v)
940}
941
942fn arity_n(lst: &[Node]) -> String {
943    (lst.len() - 1).to_string()
944}
945
946fn eval_car(lst: &[Node], env: &Env) -> Result<Node, Refusal> {
947    if lst.len() != 2 {
948        return Err(raise("vla-car-arity", &[("n", &arity_n(lst))]));
949    }
950    let v = resolve_listops_arg(nth(lst, 2)?, env)?;
951    let Node::List(c) = &v else {
952        return Err(raise(
953            "vla-car-expected-list",
954            &[("value", &v.atom_text().unwrap_or_default())],
955        ));
956    };
957    let Some(first) = c.items().first() else {
958        return Err(raise("vla-car-empty-list", &[]));
959    };
960    Ok(first.clone())
961}
962
963/// VBA `ListTail`: the items from position `n + 1` on, untagged, a view
964/// of the same items (`VlaSlice`).
965fn list_tail(c: &Seq, n: usize, caller: &str) -> Result<Node, Refusal> {
966    if c.count() < n {
967        return Err(raise(
968            "vla-list-too-short",
969            &[
970                ("caller", caller),
971                ("n", &n.to_string()),
972                ("count", &c.count().to_string()),
973            ],
974        ));
975    }
976    Ok(Node::List(c.tail(n)))
977}
978
979fn eval_cdr(lst: &[Node], env: &Env) -> Result<Node, Refusal> {
980    if lst.len() != 2 {
981        return Err(raise("vla-cdr-arity", &[("n", &arity_n(lst))]));
982    }
983    let v = resolve_listops_arg(nth(lst, 2)?, env)?;
984    let Node::List(c) = &v else {
985        return Err(raise(
986            "vla-cdr-expected-list",
987            &[("value", &v.atom_text().unwrap_or_default())],
988        ));
989    };
990    list_tail(c, 1, "cdr")
991}
992
993fn eval_cddr(lst: &[Node], env: &Env) -> Result<Node, Refusal> {
994    if lst.len() != 2 {
995        return Err(raise("vla-cddr-arity", &[("n", &arity_n(lst))]));
996    }
997    let v = resolve_listops_arg(nth(lst, 2)?, env)?;
998    let Node::List(c) = &v else {
999        return Err(raise(
1000            "vla-cddr-expected-list",
1001            &[("value", &v.atom_text().unwrap_or_default())],
1002        ));
1003    };
1004    list_tail(c, 2, "cddr")
1005}
1006
1007/// `(cons item lst)`: a new list of the item and the tail's items, as
1008/// VBA's `EvalCons` makes it, which shares no tail (P-NTH: a flat view
1009/// cannot hold one element ahead of a shared tail). The items themselves
1010/// are shared.
1011fn eval_cons(lst: &[Node], env: &Env) -> Result<Node, Refusal> {
1012    if lst.len() != 3 {
1013        return Err(raise("vla-cons-arity", &[("n", &arity_n(lst))]));
1014    }
1015    let item = resolve_listops_arg(nth(lst, 2)?, env)?;
1016    let tail = resolve_listops_arg(nth(lst, 3)?, env)?;
1017    let Node::List(tail) = &tail else {
1018        return Err(raise(
1019            "vla-cons-second-not-list",
1020            &[("value", &tail.atom_text().unwrap_or_default())],
1021        ));
1022    };
1023    let mut out = Vec::with_capacity(tail.count() + 1);
1024    out.push(item);
1025    out.extend(tail.items().iter().cloned());
1026    Ok(Node::list(out))
1027}
1028
1029fn eval_list(lst: &[Node], env: &Env) -> Result<Node, Refusal> {
1030    let mut out = Vec::with_capacity(lst.len());
1031    for k in 2..=lst.len() {
1032        out.push(resolve_listops_arg(nth(lst, k)?, env)?);
1033    }
1034    Ok(Node::list(out))
1035}
1036
1037fn bool_sym(b: bool) -> Node {
1038    Node::sym(if b { "true" } else { "false" })
1039}
1040
1041/// VBA `AtomEqual`: symbols folded, strings exact, never across the kinds.
1042fn atom_equal(a: &Node, b: &Node) -> bool {
1043    match (a, b) {
1044        (Node::Sym(x), Node::Sym(y)) => fold(x) == fold(y),
1045        (Node::Str(x), Node::Str(y)) => x == y,
1046        _ => false,
1047    }
1048}
1049
1050/// VBA `DeepEqual`.
1051fn deep_equal(a: &Node, b: &Node) -> bool {
1052    match (a, b) {
1053        (Node::List(x), Node::List(y)) => {
1054            x.count() == y.count()
1055                && x.items()
1056                    .iter()
1057                    .zip(y.items())
1058                    .all(|(p, q)| deep_equal(p, q))
1059        }
1060        (Node::List(_), _) | (_, Node::List(_)) => false,
1061        _ => atom_equal(a, b),
1062    }
1063}
1064
1065fn eval_nullq(lst: &[Node], env: &Env) -> Result<Node, Refusal> {
1066    if lst.len() != 2 {
1067        return Err(raise("vla-nullq-arity", &[("n", &arity_n(lst))]));
1068    }
1069    let v = resolve_listops_arg(nth(lst, 2)?, env)?;
1070    Ok(match &v {
1071        Node::List(c) => bool_sym(c.count() == 0),
1072        _ => bool_sym(false),
1073    })
1074}
1075
1076fn eval_eq(lst: &[Node], env: &Env) -> Result<Node, Refusal> {
1077    if lst.len() != 3 {
1078        return Err(raise("vla-eq-arity", &[("n", &arity_n(lst))]));
1079    }
1080    let a = resolve_listops_arg(nth(lst, 2)?, env)?;
1081    let b = resolve_listops_arg(nth(lst, 3)?, env)?;
1082    if a.is_list() || b.is_list() {
1083        return Err(raise("vla-eq-expected-atom", &[]));
1084    }
1085    Ok(bool_sym(atom_equal(&a, &b)))
1086}
1087
1088fn eval_equal(lst: &[Node], env: &Env) -> Result<Node, Refusal> {
1089    if lst.len() != 3 {
1090        return Err(raise("vla-equal-arity", &[("n", &arity_n(lst))]));
1091    }
1092    let a = resolve_listops_arg(nth(lst, 2)?, env)?;
1093    let b = resolve_listops_arg(nth(lst, 3)?, env)?;
1094    Ok(bool_sym(deep_equal(&a, &b)))
1095}
1096
1097/// `(quote-if test then else)`: only the selected branch is ever resolved.
1098fn eval_quote_if(lst: &[Node], env: &Env) -> Result<Node, Refusal> {
1099    if lst.len() != 4 {
1100        return Err(raise("vla-quote-if-arity", &[("n", &arity_n(lst))]));
1101    }
1102    let t = resolve_listops_arg(nth(lst, 2)?, env)?;
1103    let tf = match &t {
1104        Node::List(_) => return Err(raise("vla-quote-if-test-is-list", &[])),
1105        Node::Str(_) => {
1106            return Err(raise(
1107                "vla-quote-if-test-invalid",
1108                &[("value", &t.atom_text().unwrap_or_default())],
1109            ))
1110        }
1111        Node::Sym(s) => fold(s),
1112    };
1113    let branch = match tf.as_str() {
1114        "true" => nth(lst, 3)?,
1115        "false" => nth(lst, 4)?,
1116        _ => {
1117            return Err(raise(
1118                "vla-quote-if-test-invalid",
1119                &[("value", &t.atom_text().unwrap_or_default())],
1120            ))
1121        }
1122    };
1123    substitute(branch, env, false)
1124}
1125
1126/// LISTOPS-EXPAND: `(+expand a b)` and the six comparisons, over literal
1127/// numeric operands only, through `Val` and `Str$`. VLA.3 adds `-expand`
1128/// and `*expand`, and `quotient-expand` and `mod-expand`: Excel's
1129/// `QUOTIENT` (truncated toward zero) and `MOD` (the divisor's sign,
1130/// `a - b * floor(a / b)`), over whole numbers only, a zero divisor
1131/// refused.
1132fn eval_arith_expand(op: &str, lst: &[Node], env: &Env) -> Result<Node, Refusal> {
1133    if lst.len() != 3 {
1134        return Err(raise(
1135            "vla-arith-expand-arity",
1136            &[("op", op), ("n", &arity_n(lst))],
1137        ));
1138    }
1139    let a = resolve_listops_arg(nth(lst, 2)?, env)?;
1140    let b = resolve_listops_arg(nth(lst, 3)?, env)?;
1141    let [ta, tb] = number_texts(op, [&a, &b])?;
1142    let na = val(&ta);
1143    let nb = val(&tb);
1144    let number = |x: f64| Node::sym(str_(x).trim());
1145    Ok(match op {
1146        "+expand" => number(na + nb),
1147        "=expand" => bool_sym(na == nb),
1148        "<>expand" => bool_sym(na != nb),
1149        ">expand" => bool_sym(na > nb),
1150        "<expand" => bool_sym(na < nb),
1151        ">=expand" => bool_sym(na >= nb),
1152        "<=expand" => bool_sym(na <= nb),
1153        "-expand" => number(na - nb),
1154        "*expand" => number(na * nb),
1155        _ => {
1156            for (n, text) in [(na, &ta), (nb, &tb)] {
1157                if n.fract() != 0.0 {
1158                    return Err(raise(
1159                        "vla-arith-expand-not-whole",
1160                        &[("op", op), ("value", text)],
1161                    ));
1162                }
1163            }
1164            if nb == 0.0 {
1165                return Err(raise("vla-arith-expand-divide-by-zero", &[("op", op)]));
1166            }
1167            if op == "quotient-expand" {
1168                number((na / nb).trunc())
1169            } else {
1170                number(na - nb * (na / nb).floor())
1171            }
1172        }
1173    })
1174}
1175
1176/// The seven's gate over two operands, in the reference's order: both
1177/// checked as lists, then both as numeric literals. Their texts.
1178fn number_texts(op: &str, operands: [&Node; 2]) -> Result<[String; 2], Refusal> {
1179    let mut texts = [String::new(), String::new()];
1180    for (k, v) in operands.iter().enumerate() {
1181        let Some(text) = v.atom_text() else {
1182            return Err(raise("vla-arith-expand-not-number-list", &[("op", op)]));
1183        };
1184        texts[k] = text;
1185    }
1186    for text in &texts {
1187        if !is_numeric_literal_text(text) {
1188            return Err(raise(
1189                "vla-arith-expand-not-number-value",
1190                &[("op", op), ("value", text)],
1191            ));
1192        }
1193    }
1194    Ok(texts)
1195}
1196
1197/// The same gate over one operand.
1198fn number_text(op: &str, v: &Node) -> Result<String, Refusal> {
1199    let Some(text) = v.atom_text() else {
1200        return Err(raise("vla-arith-expand-not-number-list", &[("op", op)]));
1201    };
1202    if !is_numeric_literal_text(&text) {
1203        return Err(raise(
1204            "vla-arith-expand-not-number-value",
1205            &[("op", op), ("value", &text)],
1206        ));
1207    }
1208    Ok(text)
1209}
1210
1211/// VLA.3's arity refusal for a primitive taking `want` arguments.
1212fn expand_arity(op: &str, want: usize, lst: &[Node]) -> Refusal {
1213    raise(
1214        "vla-expand-arity",
1215        &[
1216            ("op", op),
1217            ("want", &want.to_string()),
1218            ("n", &arity_n(lst)),
1219        ],
1220    )
1221}
1222
1223/// VLA.3: an atom's text as the object model's primitives read it, a
1224/// string literal's content or a symbol's (a number's) own text.
1225fn atom_text_expand(v: &Node) -> String {
1226    match v {
1227        Node::Str(s) | Node::Sym(s) => s.to_string(),
1228        Node::List(_) => String::new(),
1229    }
1230}
1231
1232/// VLA.3: `(&expand a b ...)`, each argument resolved as every LISTOPS
1233/// argument is and its text joined. Always a string literal, never a
1234/// symbol, so a macro writes an address or a formula's text with no
1235/// leading-quote trick; `symbol` keeps that trick for identifiers. At
1236/// least one argument, as `symbol` takes.
1237fn eval_concat_expand(lst: &[Node], env: &Env) -> Result<Node, Refusal> {
1238    if lst.len() < 2 {
1239        return Err(raise("vla-concat-expand-arity", &[]));
1240    }
1241    let mut joined = String::new();
1242    for k in 2..=lst.len() {
1243        let piece = resolve_listops_arg(nth(lst, k)?, env)?;
1244        if piece.is_list() {
1245            return Err(raise(
1246                "vla-expand-arg-is-list",
1247                &[("op", "&expand"), ("n", &(k - 1).to_string())],
1248            ));
1249        }
1250        joined.push_str(&atom_text_expand(&piece));
1251    }
1252    Ok(Node::Str(Arc::from(joined.as_str())))
1253}
1254
1255/// VLA.3: `(characters-expand text)`, the text split into one-character
1256/// string literals in order. A character is a Unicode scalar value, as
1257/// the reference counts one by joining a UTF-16 surrogate pair. The empty
1258/// text gives the empty list.
1259fn eval_characters_expand(lst: &[Node], env: &Env) -> Result<Node, Refusal> {
1260    if lst.len() != 2 {
1261        return Err(expand_arity("characters-expand", 1, lst));
1262    }
1263    let v = resolve_listops_arg(nth(lst, 2)?, env)?;
1264    if v.is_list() {
1265        return Err(raise(
1266            "vla-expand-arg-is-list",
1267            &[("op", "characters-expand"), ("n", "1")],
1268        ));
1269    }
1270    let items = atom_text_expand(&v)
1271        .chars()
1272        .map(|c| Node::Str(Arc::from(c.to_string().as_str())))
1273        .collect();
1274    Ok(Node::list(items))
1275}
1276
1277/// VLA.3: `(address-expand row column)`, a cell's address, relative, as
1278/// Excel's `ADDRESS(row, column, 4)` writes it: `B3`. Each argument a
1279/// number literal under the seven's gate, then a whole number within
1280/// Excel's bounds. A string literal.
1281fn eval_address_expand(lst: &[Node], env: &Env) -> Result<Node, Refusal> {
1282    const OP: &str = "address-expand";
1283    if lst.len() != 3 {
1284        return Err(expand_arity(OP, 2, lst));
1285    }
1286    let a = resolve_listops_arg(nth(lst, 2)?, env)?;
1287    let b = resolve_listops_arg(nth(lst, 3)?, env)?;
1288    let [ta, tb] = number_texts(OP, [&a, &b])?;
1289    let (row, col) = (val(&ta), val(&tb));
1290    if row.fract() != 0.0 || !(1.0..=f64::from(MAX_ROW)).contains(&row) {
1291        return Err(raise(
1292            "vla-address-expand-row",
1293            &[("op", OP), ("value", &ta)],
1294        ));
1295    }
1296    if col.fract() != 0.0 || !(1.0..=f64::from(MAX_COLUMN)).contains(&col) {
1297        return Err(raise(
1298            "vla-address-expand-column",
1299            &[("op", OP), ("value", &tb)],
1300        ));
1301    }
1302    let address = format!("{}{}", column_letters(col as u32), str_(row).trim());
1303    Ok(Node::Str(Arc::from(address.as_str())))
1304}
1305
1306/// VLA.3: `(row-expand address)` and `(column-expand address)`, an
1307/// address's row and column numbers, as Excel's `ROW` and `COLUMN` of the
1308/// reference; and `(column-expand letters)`, a column's letters alone, the
1309/// reverse of `column-letters-expand`.
1310fn eval_address_part_expand(op: &str, lst: &[Node], env: &Env) -> Result<Node, Refusal> {
1311    if lst.len() != 2 {
1312        return Err(expand_arity(op, 1, lst));
1313    }
1314    let v = resolve_listops_arg(nth(lst, 2)?, env)?;
1315    if v.is_list() {
1316        return Err(raise("vla-expand-arg-is-list", &[("op", op), ("n", "1")]));
1317    }
1318    let text = atom_text_expand(&v);
1319    let Some((row, col)) = parse_address_text(&text, op == "column-expand") else {
1320        return Err(raise(
1321            "vla-address-expand-not-address",
1322            &[("op", op), ("value", &text)],
1323        ));
1324    };
1325    let n = if op == "row-expand" { row } else { col };
1326    Ok(Node::sym(str_(f64::from(n)).trim()))
1327}
1328
1329/// VLA.3: one grammar for an address, the reference's `ParseAddressText`
1330/// rule for rule: an optional `$`, one to three ASCII letters in either
1331/// case, an optional `$`, then one to seven ASCII digits; the column at
1332/// most XFD, the row 1 to 1,048,576. With `letters_alone`, the letters with
1333/// their optional `$` and nothing after them are a column, the row 0.
1334/// Anything else, a sheet's name before it, a range, a space or a `$`
1335/// anywhere else, is not an address. (`sheet.rs`'s `parse_a1_cell` strips
1336/// a `$` wherever it stands, so it cannot be the rule both implementations
1337/// keep.)
1338fn parse_address_text(s: &str, letters_alone: bool) -> Option<(u32, u32)> {
1339    let b = s.as_bytes();
1340    let n = b.len();
1341    let i = usize::from(n >= 1 && b[0] == b'$');
1342    let mut j = i;
1343    while j < n && b[j].is_ascii_alphabetic() {
1344        j += 1;
1345    }
1346    if j == i || j - i > 3 {
1347        return None;
1348    }
1349    let col = letters_to_col(&s[i..j])?;
1350    if j == n {
1351        return letters_alone.then_some((0, col));
1352    }
1353    if b[j] == b'$' {
1354        j += 1;
1355    }
1356    let mut k = j;
1357    while k < n && b[k].is_ascii_digit() {
1358        k += 1;
1359    }
1360    if k < n || k == j || k - j > 7 {
1361        return None;
1362    }
1363    let row: u32 = s[j..k].parse().ok()?;
1364    if row == 0 || row > MAX_ROW {
1365        return None;
1366    }
1367    Some((row, col))
1368}
1369
1370/// VLA.3: `(column-letters-expand column)`, a column's letters as a string
1371/// literal, 1 = A ... 16384 = XFD; the reverse of `column-expand`.
1372fn eval_column_letters_expand(lst: &[Node], env: &Env) -> Result<Node, Refusal> {
1373    const OP: &str = "column-letters-expand";
1374    if lst.len() != 2 {
1375        return Err(expand_arity(OP, 1, lst));
1376    }
1377    let v = resolve_listops_arg(nth(lst, 2)?, env)?;
1378    let text = number_text(OP, &v)?;
1379    let col = val(&text);
1380    if col.fract() != 0.0 || !(1.0..=f64::from(MAX_COLUMN)).contains(&col) {
1381        return Err(raise(
1382            "vla-address-expand-column",
1383            &[("op", OP), ("value", &text)],
1384        ));
1385    }
1386    Ok(Node::Str(Arc::from(column_letters(col as u32).as_str())))
1387}
1388
1389/// VBA `EvalCondFrom`: `(cond (test form) ... (else form))`, one clause at
1390/// a time from `start_idx`; a test that folds to true or false is taken or
1391/// skipped at expand time, any other test defers to a native `if`.
1392fn eval_cond_from(lst: &[Node], start_idx: usize, env: &Env) -> Result<Node, Refusal> {
1393    if start_idx > lst.len() {
1394        return Ok(Node::list(vec![Node::sym("begin")]));
1395    }
1396    let cl = nth_list(lst, start_idx)?.items();
1397    if cl.len() != 2 {
1398        return Err(raise(
1399            "vla-cond-clause-shape",
1400            &[("n", &cl.len().to_string())],
1401        ));
1402    }
1403    if let Node::Sym(s) = nth(cl, 1)? {
1404        if fold(s) == "else" {
1405            if start_idx != lst.len() {
1406                return Err(raise("vla-cond-else-not-last", &[]));
1407            }
1408            return substitute(nth(cl, 2)?, env, false);
1409        }
1410    }
1411    let t = resolve_listops_arg(nth(cl, 1)?, env)?;
1412    let tf = match &t {
1413        Node::Sym(s) => fold(s),
1414        _ => String::new(),
1415    };
1416    if tf == "true" {
1417        return substitute(nth(cl, 2)?, env, false);
1418    }
1419    if tf == "false" {
1420        return eval_cond_from(lst, start_idx + 1, env);
1421    }
1422    let raw_form = substitute(nth(cl, 2)?, env, false)?;
1423    let rest_form = eval_cond_from(lst, start_idx + 1, env)?;
1424    Ok(Node::list(vec![
1425        Node::sym("if"),
1426        t,
1427        Node::list(vec![Node::sym("then"), raw_form]),
1428        Node::list(vec![Node::sym("else"), rest_form]),
1429    ]))
1430}
1431
1432#[cfg(test)]
1433mod tests {
1434    use super::*;
1435    use crate::reader::read_forms;
1436
1437    const PRELUDE: &str = include_str!("../testdata/prelude.vla");
1438
1439    /// `VlaExpandText(src, True, fired)` under the real prelude, normalised
1440    /// to LF for the pins; the VBA pins compare fragments of this text.
1441    fn expand(src: &str) -> String {
1442        let (text, _) = expand_text(src, PRELUDE, true).unwrap_or_else(|e| panic!("{src}: {e}"));
1443        text.replace("\r\n", "\n")
1444    }
1445
1446    /// The pins' `CheckExpandErr`: the refusal's text holds the fragment.
1447    fn expand_err(src: &str) -> Refusal {
1448        match expand_text(src, PRELUDE, true) {
1449            Ok((text, _)) => panic!("{src} expanded instead of refusing: {text}"),
1450            Err(e) => e,
1451        }
1452    }
1453
1454    fn one(src: &str) -> String {
1455        expand(src).trim_end().to_string()
1456    }
1457
1458    #[test]
1459    fn ordinary_substitution_and_the_rest_parameter() {
1460        assert_eq!(
1461            one("(defmacro (make-adder n) (begin (dim total Long) (set! total (+ total n)))) (make-adder 5)"),
1462            "(begin (dim total Long) (set! total (+ total 5)))"
1463        );
1464        assert_eq!(
1465            one("(defmacro (all & xs) (list-of xs)) (all a b c)"),
1466            "(list-of a b c)"
1467        );
1468        // In element position the rest parameter splices; only a template
1469        // that is the bare name gives the whole list.
1470        assert_eq!(
1471            one("(defmacro (whole & xs) (f xs 1)) (whole a b)"),
1472            "(f a b 1)"
1473        );
1474        assert_eq!(one("(defmacro (bare & xs) xs) (bare a b)"), "(a b)");
1475        // Two template forms become one begin; a docstring is not one.
1476        assert_eq!(
1477            one("(defmacro (two x) \"the doc\" (a x) (b x)) (two 1)"),
1478            "(begin (a 1) (b 1))"
1479        );
1480        assert_eq!(
1481            one("(defmacro (lone) \"just this\") (lone)"),
1482            "\"just this\""
1483        );
1484        // A nested macro call inside a substitution expands too.
1485        assert_eq!(
1486            one("(defmacro (inner x) (set! x 1)) (defmacro (outer y) (inner y)) (outer q)"),
1487            "(set! q 1)"
1488        );
1489        assert_eq!(one("(quote (when unless))"), "(quote (when unless))");
1490    }
1491
1492    #[test]
1493    fn the_prelude_macros_fire_and_the_count_is_reported() {
1494        let (text, fired) = expand_text("(when c a b)", PRELUDE, true).unwrap();
1495        assert_eq!(text.trim_end(), "(if c (then a b))");
1496        assert_eq!(fired, 1);
1497        let (text, fired) = expand_text("(when c a b)", PRELUDE, false).unwrap();
1498        assert_eq!(text.trim_end(), "(if c (then a b))");
1499        assert_eq!(fired, 1);
1500        let (_, fired) = expand_text("(set! x 1)", PRELUDE, true).unwrap();
1501        assert_eq!(fired, 0);
1502    }
1503
1504    #[test]
1505    fn arity_refusals_name_the_macro() {
1506        let r = expand_err("(defmacro (two a b) (f a b)) (two 1)");
1507        assert_eq!(r.id, "vla-macro-arity");
1508        assert_eq!(r.text, "macro 'two' expects 2 argument(s), got 1");
1509        let r = expand_err("(defmacro (atleast a & r) (f a r)) (atleast)");
1510        assert_eq!(r.id, "vla-macro-arity-min");
1511        assert_eq!(
1512            r.text,
1513            "macro 'atleast' expects at least 1 argument(s), got 0"
1514        );
1515        assert_eq!(
1516            expand_err("(defmacro (m a &) a)").id,
1517            "vla-defmacro-rest-param-missing"
1518        );
1519        assert_eq!(
1520            expand_err("(defmacro (m & r x) r)").id,
1521            "vla-defmacro-rest-param-not-last"
1522        );
1523        assert_eq!(
1524            expand_err("(defmacro (m x))").id,
1525            "vla-defmacro-missing-template"
1526        );
1527    }
1528
1529    // TestListops (VLA_Tests.bas), the standalone primitives.
1530    #[test]
1531    fn listops_car_cdr_cddr_cons_list() {
1532        assert_eq!(one("(car (quote (1 2 3)))"), "1");
1533        assert_eq!(one("(cdr (quote (1 2 3)))"), "(2 3)");
1534        assert_eq!(one("(cddr (quote (1 2 3 4)))"), "(3 4)");
1535        assert!(expand_err("(car (quote ()))")
1536            .text
1537            .contains("car: expected a non-empty list"));
1538        assert!(expand_err("(cdr (quote ()))")
1539            .text
1540            .contains("cdr: expected a list of at least 1 element(s), got 0"));
1541        assert!(expand_err("(cddr (quote (1)))")
1542            .text
1543            .contains("cddr: expected a list of at least 2 element(s), got 1"));
1544        assert!(expand_err("(car 5)")
1545            .text
1546            .contains("car: expected a list, got '5'"));
1547        assert!(expand_err("(car (quote (1 2)) (quote (3 4)))")
1548            .text
1549            .contains("car expects exactly 1 argument, got 2"));
1550        assert_eq!(one("(cons 1 (quote (2 3)))"), "(1 2 3)");
1551        assert!(expand_err("(cons 1 2)")
1552            .text
1553            .contains("cons: second argument must be a list, got '2'"));
1554        assert_eq!(one("(list 1 2 3)"), "(1 2 3)");
1555        assert_eq!(one("(list)"), "()");
1556    }
1557
1558    #[test]
1559    fn listops_predicates_and_quote_if() {
1560        assert_eq!(one("(null? (quote ()))"), "true");
1561        assert_eq!(one("(null? (quote (1)))"), "false");
1562        assert_eq!(one("(null? 5)"), "false");
1563        assert_eq!(one("(eq? (quote a) (quote a))"), "true");
1564        assert_eq!(one("(eq? (quote a) (quote b))"), "false");
1565        assert_eq!(one("(eq? (quote A) (quote a))"), "true");
1566        assert_eq!(one("(eq? \"a\" (quote a))"), "false");
1567        assert!(expand_err("(eq? (quote (1)) (quote (1)))")
1568            .text
1569            .contains("eq?: expected an atom, got a list"));
1570        assert_eq!(
1571            one("(equal? (quote (1 2 (3 4))) (quote (1 2 (3 4))))"),
1572            "true"
1573        );
1574        assert_eq!(
1575            one("(equal? (quote (1 2 (3 4))) (quote (1 2 (3 5))))"),
1576            "false"
1577        );
1578        assert_eq!(
1579            one("(quote-if (null? (quote ())) (debug-print \"base\") (car (quote ())))"),
1580            "(debug-print \"base\")"
1581        );
1582        assert_eq!(
1583            one("(quote-if (null? (quote (1))) (car (quote ())) (debug-print \"recurse\"))"),
1584            "(debug-print \"recurse\")"
1585        );
1586        assert!(
1587            expand_err("(quote-if 5 (debug-print \"a\") (debug-print \"b\"))")
1588                .text
1589                .contains("quote-if: test must resolve to true or false")
1590        );
1591        assert!(
1592            expand_err("(quote-if (null? (quote ())) (debug-print \"a\"))")
1593                .text
1594                .contains("quote-if expects exactly 3 arguments")
1595        );
1596        for (word, frag) in [
1597            (
1598                "car",
1599                "'car' is the list-head primitive and cannot be a macro name",
1600            ),
1601            ("cond", "'cond' is the multi-clause conditional"),
1602            ("quote", "'quote' is the data-literal form"),
1603            ("+expand", "'+expand' is the expand-time addition primitive"),
1604        ] {
1605            let r = expand_err(&format!("(defmacro ({word} x) x)"));
1606            assert_eq!(r.id, "vla-defmacro-reserved-name");
1607            assert!(r.text.contains(frag), "{word}: {}", r.text);
1608        }
1609        assert_eq!(
1610            one("(defmacro (first-of xs) (car xs)) (first-of (quote (7 8 9)))"),
1611            "7"
1612        );
1613        assert_eq!(
1614            one("(defmacro (demo xs) (quasiquote (unquote (car xs)))) (demo (quote (1 2 3)))"),
1615            "1"
1616        );
1617    }
1618
1619    #[test]
1620    fn a_self_terminating_walker_emits_one_statement_per_row() {
1621        let walker = "(defmacro (walk-rows lst) (quote-if (null? lst) (begin) (begin (debug-print (car lst)) (walk-rows (cdr lst)))))";
1622        let text = one(&format!("{walker} (walk-rows (quote (10 20 30)))"));
1623        // The trampoline flattens the chain into one begin, as the VBA's
1624        // own pendingHead combine does.
1625        assert_eq!(
1626            text,
1627            "(begin (debug-print 10) (debug-print 20) (debug-print 30) (begin))"
1628        );
1629    }
1630
1631    // TestListopsExpand: the seven arithmetic primitives.
1632    #[test]
1633    fn expand_time_arithmetic() {
1634        assert_eq!(one("(+expand 2 3)"), "5");
1635        assert_eq!(one("(+expand -2 3)"), "1");
1636        assert_eq!(one("(+expand 2.5 1.5)"), "4");
1637        assert_eq!(one("(+expand 0.1 0.2)"), ".3");
1638        assert_eq!(one("(=expand 5 5)"), "true");
1639        assert_eq!(one("(=expand 5 6)"), "false");
1640        assert_eq!(one("(<>expand 5 6)"), "true");
1641        assert_eq!(one("(>expand 5 3)"), "true");
1642        assert_eq!(one("(>expand 3 5)"), "false");
1643        assert_eq!(one("(<expand 3 5)"), "true");
1644        assert_eq!(one("(>=expand 5 5)"), "true");
1645        assert_eq!(one("(<=expand 5 5)"), "true");
1646        assert!(expand_err("(+expand 2 (quote x))")
1647            .text
1648            .contains("+expand: expected a number, got 'x'"));
1649        assert!(expand_err("(>expand (quote (1)) 2)")
1650            .text
1651            .contains(">expand: expected a number, got a list"));
1652        assert!(expand_err("(+expand (quote 1,000) 1)")
1653            .text
1654            .contains("+expand: expected a number, got '1,000'"));
1655        assert!(expand_err("(+expand 2)")
1656            .text
1657            .contains("+expand expects exactly 2 arguments, got 1"));
1658        assert_eq!(
1659            one("(quote-if (>expand 5 3) (debug-print \"yes\") (debug-print \"no\"))"),
1660            "(debug-print \"yes\")"
1661        );
1662        assert_eq!(
1663            one("(defmacro (bigger x) (quote-if (>expand x 10) (quote yes) (quote no))) (bigger 20)"),
1664            "(quote yes)"
1665        );
1666    }
1667
1668    // TestListopsObjectModel (VLA.3), pin for pin.
1669    #[test]
1670    fn the_expand_time_object_model_folds_and_refuses() {
1671        let refused = |src: &str, id: &str, text: &str| {
1672            let e = expand_err(src);
1673            assert_eq!((e.id.as_str(), e.text.as_str()), (id, text), "{src}");
1674        };
1675        // Arithmetic: -expand and *expand as +expand; quotient-expand and
1676        // mod-expand Excel's QUOTIENT and MOD over whole numbers.
1677        assert_eq!(one("(-expand 5 2)"), "3");
1678        assert_eq!(one("(-expand 2 5)"), "-3");
1679        assert_eq!(one("(-expand 2.5 1)"), "1.5");
1680        assert_eq!(one("(*expand 6 7)"), "42");
1681        assert_eq!(one("(*expand 1.5 2)"), "3");
1682        assert_eq!(one("(quotient-expand 7 2)"), "3");
1683        assert_eq!(one("(quotient-expand -7 2)"), "-3");
1684        assert_eq!(one("(mod-expand 7 3)"), "1");
1685        assert_eq!(one("(mod-expand -7 2)"), "1");
1686        assert_eq!(one("(mod-expand 7 -2)"), "-1");
1687        refused(
1688            "(quotient-expand 7.5 2)",
1689            "vla-arith-expand-not-whole",
1690            "quotient-expand: expected a whole number, got '7.5'",
1691        );
1692        refused(
1693            "(mod-expand 7 0)",
1694            "vla-arith-expand-divide-by-zero",
1695            "mod-expand: cannot divide by zero",
1696        );
1697        refused(
1698            "(-expand 1)",
1699            "vla-arith-expand-arity",
1700            "-expand expects exactly 2 arguments, got 1",
1701        );
1702        refused(
1703            "(*expand 2 \"3\")",
1704            "vla-arith-expand-not-number-value",
1705            "*expand: expected a number, got '\"3'",
1706        );
1707        // Text: &expand joins atoms into a string; characters-expand splits.
1708        assert_eq!(one("(&expand \"A\" 1)"), "\"A1\"");
1709        assert_eq!(one("(&expand x)"), "\"x\"");
1710        assert_eq!(
1711            one("(&expand \"=SUM(\" (address-expand 1 2) \":\" (address-expand 9 2) \")\")"),
1712            "\"=SUM(B1:B9)\""
1713        );
1714        refused(
1715            "(&expand)",
1716            "vla-concat-expand-arity",
1717            "&expand expects at least 1 argument, got 0",
1718        );
1719        refused(
1720            "(&expand \"a\" (quote (b)))",
1721            "vla-expand-arg-is-list",
1722            "&expand: argument 2 is a list, expected an atom or string literal",
1723        );
1724        assert_eq!(one("(characters-expand \"ab#\")"), "(\"a\" \"b\" \"#\")");
1725        assert_eq!(one("(characters-expand \"\")"), "()");
1726        assert_eq!(one("(car (cdr (characters-expand \"xyz\")))"), "\"y\"");
1727        assert_eq!(
1728            one("(car (cdr (characters-expand \"a\u{1F600}b\")))"),
1729            "\"\u{1F600}\""
1730        );
1731        assert_eq!(
1732            one("(null? (cdr (cdr (cdr (characters-expand \"a\u{1F600}b\")))))"),
1733            "true"
1734        );
1735        refused(
1736            "(characters-expand (quote (a)))",
1737            "vla-expand-arg-is-list",
1738            "characters-expand: argument 1 is a list, expected an atom or string literal",
1739        );
1740        refused(
1741            "(characters-expand \"a\" \"b\")",
1742            "vla-expand-arity",
1743            "characters-expand expects exactly 1 argument(s), got 2",
1744        );
1745        // Addresses, by one grammar, within Excel's bounds.
1746        assert_eq!(one("(address-expand 3 2)"), "\"B3\"");
1747        assert_eq!(one("(address-expand 1 16384)"), "\"XFD1\"");
1748        assert_eq!(one("(address-expand 1048576 27)"), "\"AA1048576\"");
1749        refused(
1750            "(address-expand 0 1)",
1751            "vla-address-expand-row",
1752            "address-expand: a row is a whole number from 1 to 1048576, got '0'",
1753        );
1754        refused(
1755            "(address-expand 1 16385)",
1756            "vla-address-expand-column",
1757            "address-expand: a column is a whole number from 1 to 16384, got '16385'",
1758        );
1759        refused(
1760            "(address-expand 1.5 1)",
1761            "vla-address-expand-row",
1762            "address-expand: a row is a whole number from 1 to 1048576, got '1.5'",
1763        );
1764        assert_eq!(one("(row-expand \"B3\")"), "3");
1765        assert_eq!(one("(column-expand \"$AA$10\")"), "27");
1766        assert_eq!(one("(column-expand \"xfd1\")"), "16384");
1767        assert_eq!(one("(column-expand \"AA\")"), "27");
1768        for (src, value) in [
1769            ("(row-expand \"AA\")", "AA"),
1770            ("(row-expand \"Sheet1!B3\")", "Sheet1!B3"),
1771            ("(column-expand \"B3:C4\")", "B3:C4"),
1772            ("(row-expand \"B0\")", "B0"),
1773        ] {
1774            let op = &src[1..src.find(' ').unwrap()];
1775            refused(
1776                src,
1777                "vla-address-expand-not-address",
1778                &format!("{op}: expected an address such as B3, got '{value}'"),
1779            );
1780        }
1781        assert_eq!(one("(column-letters-expand 28)"), "\"AB\"");
1782        refused(
1783            "(column-letters-expand 0)",
1784            "vla-address-expand-column",
1785            "column-letters-expand: a column is a whole number from 1 to 16384, got '0'",
1786        );
1787        // Inside a macro's template, the other recognition site.
1788        assert_eq!(
1789            one("(defmacro (below a) (address-expand (+expand (row-expand a) 1) (column-expand a))) (below \"C7\")"),
1790            "\"C8\""
1791        );
1792        assert_eq!(
1793            one(concat!(
1794                "(defmacro (picture r txt) (picture-from r 1 (characters-expand txt))) ",
1795                "(defmacro (picture-from r c m) (quote-if (null? m) (begin) (begin ",
1796                "(quote-if (eq? (car m) \"#\") (cell \"S\" (address-expand r c) 1) (begin)) ",
1797                "(picture-from r (+expand c 1) (cdr m))))) (picture 2 \".#.#\")"
1798            )),
1799            "(begin (begin) (cell \"S\" \"B2\" 1) (begin) (cell \"S\" \"D2\" 1) (begin))"
1800        );
1801        // Reserved: none of the ten can be a macro's name.
1802        for (word, desc) in [
1803            ("-expand", "subtraction"),
1804            ("address-expand", "address"),
1805            ("characters-expand", "text-splitting"),
1806        ] {
1807            refused(
1808                &format!("(defmacro ({word} x) x)"),
1809                "vla-defmacro-reserved-name",
1810                &format!("defmacro '{word}': '{word}' is the expand-time {desc} primitive and cannot be a macro name"),
1811            );
1812        }
1813    }
1814
1815    /// Every name the reference reserves is a primitive both recognition
1816    /// sites know, and the other way round, the quote bundle aside.
1817    #[test]
1818    fn every_listops_name_is_reserved() {
1819        let reserved: Vec<&str> = RESERVED.iter().map(|(w, _)| *w).collect();
1820        let quote_bundle = [
1821            "quote",
1822            "quasiquote",
1823            "unquote",
1824            "unquote-splicing",
1825            "symbol",
1826        ];
1827        // VLA.4: an operation of the walk, recognized by `expand_at` alone.
1828        let walk_operations = ["formula-expand"];
1829        for name in LISTOPS {
1830            assert!(
1831                reserved.contains(name),
1832                "{name} is recognised but not reserved"
1833            );
1834        }
1835        for name in reserved {
1836            assert!(
1837                LISTOPS.contains(&name)
1838                    || quote_bundle.contains(&name)
1839                    || walk_operations.contains(&name),
1840                "{name} is reserved but not recognised"
1841            );
1842        }
1843        assert_eq!(LISTOPS.len(), 27);
1844        assert_eq!(one("(formula-expand (+ 1 2))"), "\"=1+2\"");
1845    }
1846
1847    // VLA.4: formula-expand, the formula syntax at expand time.
1848    #[test]
1849    fn formula_expand_expands_its_argument_first() {
1850        // A macro inside the expression fires before the expression is
1851        // written, so it is never written as a function's call.
1852        assert_eq!(
1853            one("(defmacro (edge cell twin) (and (= cell 1) (= twin 0)))\n\
1854                 (formula-expand (if (edge Input!B5 Input.last!B5) 1 0))"),
1855            "\"=IF(AND(Input!B5=1,Input.last!B5=0),1,0)\""
1856        );
1857        // A template's parameters are filled in inside it, and the row it
1858        // stands in is written with the formula's text.
1859        assert_eq!(
1860            one(
1861                "(defmacro (rule sheet range expr) (formula sheet range (formula-expand expr)))\n\
1862                 (rule \"Board\" \"A1:B2\" (+ A1 1))"
1863            ),
1864            "(formula \"Board\" \"A1:B2\" \"=A1+1\")"
1865        );
1866        // An expand-time primitive inside it computes, a reference built
1867        // by `ref` from the address it computes.
1868        assert_eq!(
1869            one("(formula-expand (ref \"Input\" (address-expand 5 2)))"),
1870            "\"=Input!B5\""
1871        );
1872        // A text stays a text.
1873        assert_eq!(one("(formula-expand \"=A1+1\")"), "\"=\\\"=A1+1\\\"\"");
1874        assert_eq!(
1875            expand_err("(formula-expand (+ (formula-expand A1) 1))").id,
1876            "vla-formula-expand-nested"
1877        );
1878        assert_eq!(
1879            expand_err("(formula-expand)").id,
1880            "vla-formula-expand-arity"
1881        );
1882        assert_eq!(
1883            expand_err("(formula-expand 1 2)").id,
1884            "vla-formula-expand-arity"
1885        );
1886        assert_eq!(
1887            expand_err("(&expand (formula-expand A1))").id,
1888            "vla-expand-arg-is-list"
1889        );
1890        assert_eq!(
1891            expand_err("(defmacro (formula-expand x) x)").id,
1892            "vla-defmacro-reserved-name"
1893        );
1894        // After a refusal the walk is outside every formula-expand again.
1895        let mut ex = Expander::new();
1896        let bad = read_forms("(formula-expand (+ (formula-expand A1) 1))").unwrap();
1897        assert!(ex.expand(&bad[0], 0).is_err());
1898        let good = read_forms("(formula-expand A1)").unwrap();
1899        assert_eq!(ex.expand(&good[0], 0).unwrap(), Form::string("=A1"));
1900    }
1901
1902    // TestCond.
1903    #[test]
1904    fn cond_folds_defers_and_refuses() {
1905        assert_eq!(
1906            one("(cond (true (debug-print \"a\")) (false (car (quote ()))))"),
1907            "(debug-print \"a\")"
1908        );
1909        assert_eq!(
1910            one("(cond (false (car (quote ()))) (true (debug-print \"b\")))"),
1911            "(debug-print \"b\")"
1912        );
1913        assert_eq!(
1914            one("(cond ((null? (quote ())) (debug-print \"yes\")) (else (debug-print \"no\")))"),
1915            "(debug-print \"yes\")"
1916        );
1917        assert_eq!(
1918            one("(cond ((null? (quote (1))) (debug-print \"wrong\")) (else (debug-print \"right\")))"),
1919            "(debug-print \"right\")"
1920        );
1921        // A runtime test defers to a native if, the rest in its else.
1922        assert_eq!(
1923            one("(cond ((> x 0) (debug-print \"pos\")) (else (debug-print \"nonpos\")))"),
1924            "(if (> x 0) (then (debug-print \"pos\")) (else (debug-print \"nonpos\")))"
1925        );
1926        assert_eq!(
1927            one("(cond ((> x 0) (debug-print \"pos\")) (true (debug-print \"fallback\")))"),
1928            "(if (> x 0) (then (debug-print \"pos\")) (else (debug-print \"fallback\")))"
1929        );
1930        assert_eq!(one("(cond (false (debug-print \"a\")))"), "(begin)");
1931        assert_eq!(one("(cond)"), "(begin)");
1932        assert!(
1933            expand_err("(cond (else (debug-print \"a\")) (true (debug-print \"b\")))")
1934                .text
1935                .contains("cond: 'else' must be the last clause")
1936        );
1937        assert!(expand_err("(cond ((> 1 0)))")
1938            .text
1939            .contains("cond: each clause must be (test form) or (else form), got 1 element(s)"));
1940        assert_eq!(
1941            one("(defmacro (branch x) (cond ((null? x) (quote empty)) (else (quote nonempty)))) (branch (quote ()))"),
1942            "(quote empty)"
1943        );
1944    }
1945
1946    // TestQuasiquote.
1947    #[test]
1948    fn quasiquote_shields_and_unquote_punches_through() {
1949        assert_eq!(one("(symbol \"make-\" \"bold\")"), "make-bold");
1950        assert_eq!(one("(symbol \"bold\")"), "bold");
1951        assert!(expand_err("(symbol)")
1952            .text
1953            .contains("symbol expects at least 1 argument, got 0"));
1954        assert_eq!(
1955            one("(defmacro (make-getter suffix) (function (symbol \"get-\" suffix) () String (return \"hi\"))) (make-getter widget)"),
1956            "(function get-widget () String (return \"hi\"))"
1957        );
1958        for (word, frag) in [
1959            ("symbol", "'symbol' is the identifier-fusion primitive"),
1960            ("quasiquote", "'quasiquote' is the template-shielding form"),
1961            ("unquote", "'unquote' is the quasiquote escape form"),
1962            (
1963                "unquote-splicing",
1964                "'unquote-splicing' is the quasiquote splice form",
1965            ),
1966        ] {
1967            assert!(expand_err(&format!("(defmacro ({word} x) x)"))
1968                .text
1969                .contains(frag));
1970        }
1971        assert_eq!(
1972            one("(defmacro (demo x) (quasiquote (x (unquote x)))) (demo hello)"),
1973            "(x hello)"
1974        );
1975        assert_eq!(
1976            one("(defmacro (demo & rest) (quasiquote (literal (unquote-splicing rest) end))) (demo a b c)"),
1977            "(literal a b c end)"
1978        );
1979        assert!(expand_err(
1980            "(defmacro (demo x) (quasiquote (a (quasiquote (unquote x))))) (demo hello)"
1981        )
1982        .text
1983        .contains("nested quasiquote is not supported"));
1984        assert!(expand_err("(unquote x)")
1985            .text
1986            .contains("'unquote' only has meaning inside a defmacro's own template"));
1987        assert!(expand_err("(defmacro (demo x) (unquote x)) (demo hello)")
1988            .text
1989            .contains("unquote used outside quasiquote"));
1990        assert!(
1991            expand_err("(defmacro (demo x) (unquote-splicing x)) (demo hello)")
1992                .text
1993                .contains("unquote-splicing used outside quasiquote")
1994        );
1995        assert!(
1996            expand_err("(defmacro (demo x) (quasiquote (unquote-splicing x))) (demo hello)")
1997                .text
1998                .contains("unquote-splicing must appear as a list element, not as a value")
1999        );
2000        assert!(expand_err(
2001            "(defmacro (demo x) (quasiquote (a (unquote-splicing x) b))) (demo hello)"
2002        )
2003        .text
2004        .contains("unquote-splicing: operand did not resolve to a list"));
2005        assert!(
2006            expand_err("(defmacro (demo x) (quasiquote a b)) (demo hello)")
2007                .text
2008                .contains("quasiquote expects exactly 1 argument, got 2")
2009        );
2010        assert!(
2011            expand_err("(defmacro (demo x) (quasiquote (unquote x y))) (demo hello)")
2012                .text
2013                .contains("unquote expects exactly 1 argument, got 2")
2014        );
2015        assert!(expand_err(
2016            "(defmacro (demo x) (quasiquote (a (unquote-splicing x y) b))) (demo hello)"
2017        )
2018        .text
2019        .contains("unquote-splicing expects exactly 1 argument, got 2"));
2020    }
2021
2022    #[test]
2023    fn lines_survive_where_the_vba_keeps_them() {
2024        // A user's argument keeps its line through substitution; the
2025        // template's own lists carry none.
2026        let mut ex = Expander::new();
2027        let defs = read_forms("(defmacro (wrap x) (begin x))").unwrap();
2028        let body = ex.collect(defs).unwrap();
2029        assert!(body.is_empty());
2030        let call = read_forms("\n\n(wrap (f 1))").unwrap().remove(0);
2031        // A one-element begin a macro made is unwrapped by the trampoline;
2032        // the user's own form comes through with its line.
2033        let out = ex.expand(&call, 0).unwrap();
2034        assert_eq!(out, read_forms("(f 1)").unwrap()[0]);
2035        assert_eq!(out.as_list().unwrap().line, 3);
2036        let defs = read_forms("(defmacro (wrap2 x) (begin (g 0) x))").unwrap();
2037        ex.collect(defs).unwrap();
2038        let call = read_forms("\n\n(wrap2 (f 1))").unwrap().remove(0);
2039        let out = ex.expand(&call, 0).unwrap();
2040        let Form::List(l) = &out else { panic!() };
2041        assert_eq!(l.line, 0);
2042        assert_eq!(l.items[1].as_list().unwrap().line, 0);
2043        assert_eq!(l.items[2].as_list().unwrap().line, 3);
2044        // A hand-written begin keeps its line through the rebuild.
2045        let hand = read_forms("\n(begin (f 1) (g 2))").unwrap().remove(0);
2046        let out = ex.expand(&hand, 0).unwrap();
2047        assert_eq!(out.as_list().unwrap().line, 2);
2048    }
2049
2050    #[test]
2051    fn every_golden_form_expands_under_the_prelude() {
2052        // The corpus golden carries its own defmacros at the end; every
2053        // other form expands without a refusal (the compile oracle holds
2054        // the result to the byte in slice 5e).
2055        const GOLDEN: &str = include_str!("../testdata/instructions_golden.vla");
2056        let (text, fired) = expand_text(GOLDEN, PRELUDE, true).unwrap();
2057        assert!(fired > 100, "{fired} applications");
2058        assert!(text.starts_with("(const hot-pink \"#FF69B4\")"));
2059    }
2060
2061    #[test]
2062    fn a_macro_nesting_a_call_past_the_depth_limit_is_refused_by_the_walk() {
2063        // VLA.1, TestFormTooDeep's second pair: a ten-deep macro called
2064        // inside 54 lists expands to exactly 64; inside 55 it would reach
2065        // 65, which the walk refuses though the text is 56 deep. The list
2066        // past the limit is the macro's, so the refusal names no line.
2067        let deep10 = "(defmacro (deep10) (a (a (a (a (a (a (a (a (a (a 1)))))))))))\r\n";
2068        let around = |n: usize| format!("{deep10}{}(deep10){}", "(".repeat(n), ")".repeat(n));
2069        let (_, fired) = expand_text(&around(54), "", true).unwrap();
2070        assert_eq!(fired, 1);
2071        let r = expand_text(&around(55), "", true).unwrap_err();
2072        assert_eq!(r.id, "vla-form-too-deep");
2073        assert_eq!(
2074            r.text,
2075            "lists nested more than 64 deep - no program needs that many"
2076        );
2077        // The one-pass walk holds the same limit over the lists it walks.
2078        let mut ex = Expander::new();
2079        let deep = read_forms(&format!("{}{}", "(".repeat(64), ")".repeat(64))).unwrap();
2080        let wrapped = Form::list(vec![deep[0].clone()]);
2081        assert_eq!(
2082            ex.expand_one_pass(&wrapped).unwrap_err().id,
2083            "vla-form-too-deep"
2084        );
2085        assert_eq!(ex.expand(&wrapped, 0).unwrap_err().id, "vla-form-too-deep");
2086    }
2087
2088    #[test]
2089    fn a_macro_that_deepens_or_doubles_its_argument_is_refused_by_name() {
2090        // VLA.1: each application's result is weighed, so a macro that
2091        // deepens its argument every time is refused once it nests past 64,
2092        // and one that doubles it once one application writes a million
2093        // forms. In the port, which copied arguments until VLA.2, the first
2094        // ran the stack out and the second the memory, before the 5000
2095        // chain limit could stop either; the reference shares arguments,
2096        // builds no copies, and meets the chain limit (TestFormTooDeep's
2097        // last pair). The port shares them now too, and still writes its
2098        // expansion out as a tree, so both stay refused here by name.
2099        let deeper = expand_text("(defmacro (deeper x) (deeper (x))) (deeper 1)", "", true);
2100        assert_eq!(deeper.unwrap_err().id, "vla-form-too-deep");
2101        let dup = expand_text("(defmacro (dup x) (dup (x x))) (dup 1)", "", true).unwrap_err();
2102        assert_eq!(dup.id, "vla-macro-expansion-too-large");
2103        assert_eq!(
2104            dup.text,
2105            "one macro application wrote more than 1,000,000 forms (a macro that doubles its argument?)"
2106        );
2107        // One pass weighs the result of each application it makes too.
2108        let mut ex = Expander::new();
2109        let forms = read_forms("(defmacro (wrap x) (w (w (w x))))").unwrap();
2110        ex.collect(forms).unwrap();
2111        let deep = read_forms(&format!("(wrap {}1{})", "(".repeat(62), ")".repeat(62))).unwrap();
2112        assert_eq!(
2113            ex.expand_one_pass(&deep[0]).unwrap_err().id,
2114            "vla-form-too-deep"
2115        );
2116    }
2117
2118    #[test]
2119    fn a_long_walk_writes_millions_of_forms_but_none_too_many_at_once() {
2120        // VLA.1, found by the owner's run of the reference's scale pins
2121        // (2026-10-10): a walk over a long list writes the rest of the list
2122        // into every application's result, so the results summed grow as
2123        // the square of the list. A budget over the program's sum refused
2124        // the 2,000-row walk TABLESPEC-SCALE expands; the limit is one
2125        // application's result, which a walk keeps to the list's length.
2126        let walker = "(defmacro (walk-rows lst) (quote-if (null? lst) (begin) \
2127                      (begin (debug-print (car lst)) (walk-rows (cdr lst)))))\r\n";
2128        let rows: Vec<String> = (1..=2000).map(|i| i.to_string()).collect();
2129        let program = format!(
2130            "{walker}(sub t () (walk-rows (quote ({}))))",
2131            rows.join(" ")
2132        );
2133        let (text, fired) = expand_text(&program, "", true).unwrap();
2134        assert_eq!(fired, 2001, "one application a row, and the empty end");
2135        assert!(text.contains("(debug-print 2000)"));
2136    }
2137
2138    /// A node's weight found by walking its tree, as `weigh` walked it.
2139    fn walked(n: &Node) -> (u32, u32) {
2140        match n {
2141            Node::List(l) => {
2142                let (mut forms, mut deepest) = (1, 0);
2143                for item in l.items() {
2144                    let (f, d) = walked(item);
2145                    forms += f;
2146                    deepest = deepest.max(d);
2147                }
2148                (forms, deepest + 1)
2149            }
2150            _ => (1, 0),
2151        }
2152    }
2153
2154    #[test]
2155    fn a_list_keeps_the_weight_its_tree_has_and_a_view_its_own() {
2156        // VLA.2: the forms and the depth a list keeps are what a walk of
2157        // its tree finds, for a list as read, every view `cdr` and `cddr`
2158        // make of it from every start, a view of a view, and a list that
2159        // holds views beside the list itself. Every view is of the one run.
2160        for text in [
2161            "(a (b (c (d))) \"s\" (e f) ((g)) h)",
2162            "((((1))) 2 (3 (4 (5 (6)))) () 7)",
2163            "(1 2 3)",
2164            "()",
2165            "(quote (a quote b))",
2166        ] {
2167            let node = Node::of(&read_forms(text).unwrap()[0]);
2168            let Node::List(seq) = &node else { panic!() };
2169            assert_eq!(node.weight(), walked(&node), "{text}");
2170            for n in 1..=seq.count() {
2171                let Node::List(view) = list_tail(seq, n, "cdr").unwrap() else {
2172                    panic!()
2173                };
2174                assert!(Arc::ptr_eq(&view.run, &seq.run), "{text} from {n}");
2175                let once = Node::List(view.clone());
2176                assert_eq!(once.weight(), walked(&once), "{text} from {n}");
2177                for m in 1..=view.count() {
2178                    let twice = Node::List(view.tail(m));
2179                    assert_eq!(twice.weight(), walked(&twice), "{text} from {n}, {m}");
2180                }
2181                let holder = Node::list(vec![once.clone(), node.clone(), once]);
2182                assert_eq!(holder.weight(), walked(&holder), "{text} held from {n}");
2183            }
2184            assert_eq!(
2185                list_tail(seq, seq.count() + 1, "cddr").unwrap_err().id,
2186                "vla-list-too-short"
2187            );
2188        }
2189    }
2190
2191    #[test]
2192    fn a_parameter_s_value_is_the_argument_itself_at_every_use() {
2193        // VLA.2, the reference's `Set Substitute = v`: a value used twice is
2194        // the argument's own list twice, a quoted argument's content is that
2195        // content, and `cdr` of it a view of its items. A list the template
2196        // writes is made anew, untagged.
2197        let mut ex = Expander::new();
2198        ex.collect(read_forms("(defmacro (m x y) (f x x (cdr y) (car y)))").unwrap())
2199            .unwrap();
2200        let call = Node::of(&read_forms("(m (g 1) (quote ((h 2) 3)))").unwrap()[0]);
2201        let Node::List(call) = &call else { panic!() };
2202        let r = expand_one(call.items(), &ex.rules["m"]).unwrap();
2203        let Node::List(r) = &r else { panic!() };
2204        let shared = |n: &Node, with: &Node| match (n, with) {
2205            (Node::List(a), Node::List(b)) => Arc::ptr_eq(&a.run, &b.run),
2206            _ => false,
2207        };
2208        let [_, x, y] = call.items() else { panic!() };
2209        let Node::List(quoted) = y else { panic!() };
2210        let content = &quoted.items()[1];
2211        let Node::List(rows) = content else { panic!() };
2212        let [_, x1, x2, tail, head] = r.items() else {
2213            panic!()
2214        };
2215        assert!(shared(x1, x) && shared(x2, x));
2216        assert!(shared(tail, content));
2217        let Node::List(tail) = tail else { panic!() };
2218        assert_eq!((tail.start, tail.line), (1, 0));
2219        assert!(shared(head, &rows.items()[0]));
2220        assert_eq!(r.line, 0);
2221        assert_eq!(
2222            Node::List(r.clone()).form(),
2223            read_forms("(f (g 1) (g 1) (3) (h 2))").unwrap()[0]
2224        );
2225    }
2226
2227    #[test]
2228    fn a_result_past_a_limit_is_refused_as_the_walk_refused_it() {
2229        // VLA.1's weighing reads a result's two numbers (VLA.2) and walks it
2230        // only past a limit, in the copies' order, so the refusal is the
2231        // walk's: a value doubled refused at its nineteenth doubling, past a
2232        // million forms, a list nested past 64 named at its line, none for a
2233        // list a macro made.
2234        let small = Node::of(&read_forms("(a (b) c)").unwrap()[0]);
2235        assert_eq!(small.weight(), (5, 2));
2236        let ex = Expander::new();
2237        let mut doubled = Node::sym("1");
2238        for k in 1..=19 {
2239            doubled = Node::list(vec![doubled.clone(), doubled.clone()]);
2240            let refused = ex.weigh(&doubled);
2241            assert_eq!(refused.is_err(), k == 19, "{k} doublings");
2242        }
2243        assert_eq!(doubled.weight().0, (1 << 20) - 1);
2244        assert_eq!(
2245            ex.weigh(&doubled).unwrap_err().id,
2246            "vla-macro-expansion-too-large"
2247        );
2248        let mut deep = Node::sym("1");
2249        for k in 1..=65 {
2250            deep = Node::list(vec![deep]);
2251            assert_eq!(ex.weigh(&deep).is_err(), k == 65, "{k} deep");
2252        }
2253        let r = ex.weigh(&deep).unwrap_err();
2254        assert_eq!(r.id, "vla-form-too-deep");
2255        assert_eq!(r.text, too_deep(0).text);
2256        // A count past what a u32 holds saturates and stays saturated in
2257        // every view, so the walk decides.
2258        let mut huge = Node::sym("1");
2259        for _ in 0..40 {
2260            huge = Node::list(vec![huge.clone(), huge.clone(), huge]);
2261        }
2262        let Node::List(seq) = &huge else { panic!() };
2263        assert_eq!(seq.forms, u32::MAX);
2264        assert_eq!(seq.tail(1).forms, u32::MAX);
2265        assert_eq!(
2266            ex.weigh(&huge).unwrap_err().id,
2267            "vla-macro-expansion-too-large"
2268        );
2269    }
2270
2271    #[test]
2272    fn a_quoted_list_and_a_pass_s_arguments_keep_their_lines() {
2273        // VLA.2: a form read into nodes and written out again keeps every
2274        // list's line where the walk does not rebuild it: a quoted list,
2275        // returned as it stands, and a user's argument inside the result of
2276        // one pass. Found by a mutant that wrote every list at line 0.
2277        let mut ex = Expander::new();
2278        ex.collect(read_forms("(defmacro (wrap2 x) (begin (g 0) x))").unwrap())
2279            .unwrap();
2280        let quoted = read_forms("\n\n(quote (a\n(b)))").unwrap().remove(0);
2281        let out = ex.expand(&quoted, 0).unwrap();
2282        let Form::List(q) = &out else { panic!() };
2283        let Form::List(data) = &q.items[1] else {
2284            panic!()
2285        };
2286        let Form::List(b) = &data.items[1] else {
2287            panic!()
2288        };
2289        assert_eq!((q.line, data.line, b.line), (3, 3, 4));
2290        let call = read_forms("\n\n(wrap2\n(f 1))").unwrap().remove(0);
2291        let out = ex.expand_one_pass(&call).unwrap();
2292        let Form::List(r) = &out else { panic!() };
2293        assert_eq!(r.line, 0);
2294        assert_eq!(r.items[2].as_list().unwrap().line, 4);
2295    }
2296
2297    #[test]
2298    fn the_expander_crosses_threads_as_it_did() {
2299        // VLA.2 keeps a macro's template as nodes in the expander; a
2300        // published type's auto traits are part of its interface, so the
2301        // nodes count their references atomically and the expander stays
2302        // Send, Sync and unwind-safe, as `Macro` does.
2303        fn interface<T: Send + Sync + std::panic::UnwindSafe + std::panic::RefUnwindSafe>() {}
2304        interface::<Expander>();
2305        interface::<Macro>();
2306    }
2307}