Skip to main content

frazaro_core/emit/
mod.rs

1//! The VBA emitter: VLA.bas's `VlaTranspile`, `EmitTop`, `EmitProc` and
2//! the compile state they carry (PORT.5, slice 5e). The statement,
3//! expression and formula emitters are the submodules, one `match` arm per
4//! `Select Case` arm; the names module holds `SymName` and its kin.
5//!
6//! The state is `VlaFrame.cls`'s snapshot of VLA.bas's compile-time
7//! fields, carried here as one [`Compiler`]: the macro table lives in its
8//! [`Expander`], the head aliases are the head table's, and the rest are
9//! the per-compile fields `VlaTranspile` resets at its top. Generated
10//! names (`vla_tco_N`, `vlaSlabRangeN`, the button helper) come out of the
11//! same counters the VBA keeps, so a compile here and a compile there write
12//! the same text.
13//!
14//! What is not here: `SpliceIncludes`, which reads files. The core is a
15//! pure function over text, so a program whose line is a whole-line
16//! `(include "...")` is refused with the catalogue's own `vla-include-
17//! cannot-read`, naming the file; a door that can read files splices
18//! before it calls [`compile`].
19
20mod expr;
21mod formula;
22mod names;
23mod stmt;
24
25use std::collections::HashMap;
26
27use crate::expand::Expander;
28use crate::form::{str_lit_content, sym_text, Form, List};
29use crate::headtable::resolve_head_alias;
30use crate::intrinsics::fold;
31use crate::messages::{raise, Refusal};
32use crate::reader::{count_lf, parse_all, tokenize};
33
34pub use names::sym_name;
35
36/// `VLA_MAKEBUTTON_SUB`: the compiled module's button helper, appended
37/// once when a program used `make-button`.
38pub const MAKEBUTTON_SUB: &str = "VlaCompiledMakeButton";
39
40/// P.L6: one procedure's docstring record.
41#[derive(Clone, Debug)]
42struct SubDoc {
43    as_written: String,
44    doc: String,
45    kind: String,
46}
47
48/// The compile state: VLA.bas's module-level fields, one compile at a time.
49#[derive(Default)]
50pub struct Compiler {
51    pub expander: Expander,
52    sub_docs: HashMap<String, SubDoc>,
53    func_name: String,
54    in_function: bool,
55    tco_name: String,
56    tco_params: Vec<String>,
57    proc_emitted: bool,
58    emit_line: u32,
59    at_line: u32,
60    gen_row: String,
61    slab_counter: u32,
62    slab_row_var: String,
63    slab_arr_var: String,
64    slab_i_var: String,
65}
66
67/// VBA `VlaTranspile`: the program with the prelude, read, macros defined,
68/// every top-level form expanded and emitted, `Option Explicit` first and
69/// the button helper last when it is used.
70pub fn compile(source: &str, prelude: &str) -> Result<String, Refusal> {
71    Compiler::new().transpile(source, prelude)
72}
73
74impl Compiler {
75    pub fn new() -> Compiler {
76        Compiler::default()
77    }
78
79    /// VBA `VlaTranspile`.
80    pub fn transpile(&mut self, source: &str, prelude: &str) -> Result<String, Refusal> {
81        refuse_includes(source)?;
82        let line_offset = count_lf(prelude) as u32 + 1;
83        self.emit_line = 0;
84        self.at_line = 0;
85        self.gen_row.clear();
86        self.slab_counter = 0;
87        self.slab_row_var.clear();
88        let text = format!("{prelude}\r\n{source}");
89        let forms = parse_all(&tokenize(&text), line_offset)?;
90        self.expander = Expander::new();
91        self.sub_docs.clear();
92        self.proc_emitted = false;
93        self.func_name.clear();
94        self.in_function = false;
95
96        // Pass 1: collect macro definitions (order-independent).
97        let body = self.expander.collect(forms)?;
98
99        // Pass 2: expand, then emit. A refusal names the last mapped
100        // statement line the emitter reached, "near" on purpose.
101        let mut out = String::from("Option Explicit\r\n\r\n");
102        for f in &body {
103            let expanded = match self.expander.expand(f, 0) {
104                Ok(e) => e,
105                Err(e) => return Err(self.emit_fail(e)),
106            };
107            match self.emit_top(&expanded) {
108                Ok(text) => out.push_str(&text),
109                Err(e) => return Err(self.emit_fail(e)),
110            }
111            out.push_str("\r\n");
112        }
113        if out.contains(&format!("Call {MAKEBUTTON_SUB}(")) {
114            out.push_str("\r\n");
115            out.push_str(&make_button_helper_text());
116        }
117        Ok(out)
118    }
119
120    /// `VlaTranspile`'s `emitfail`: ` (near vla line N)` on a refusal that
121    /// does not already name a line, with the source line when an
122    /// `(at-line ...)` encloses the point of failure.
123    fn emit_fail(&self, e: Refusal) -> Refusal {
124        if self.emit_line > 0 && !e.text.contains("vla line") {
125            let suffix = format!(
126                " (near vla line {}{})",
127                self.emit_line,
128                self.at_line_suffix()
129            );
130            return e.with_suffix(&suffix);
131        }
132        e
133    }
134
135    /// VBA `AtLineSuffix`.
136    fn at_line_suffix(&self) -> String {
137        if self.at_line > 0 {
138            format!(", source line {}", self.at_line)
139        } else {
140            String::new()
141        }
142    }
143
144    /// VBA `TopoGuard`: a module-level declaration after a procedure.
145    fn topo_guard(&self, what: &str) -> Result<(), Refusal> {
146        if self.proc_emitted {
147            return Err(raise("vla-module-level-decl-after-proc", &[("what", what)]));
148        }
149        Ok(())
150    }
151
152    /// VBA `RecordSubDoc`: last write wins on a name.
153    fn record_sub_doc(&mut self, as_written: &str, doc: &str, kind: &str) {
154        self.sub_docs.insert(
155            fold(as_written),
156            SubDoc {
157                as_written: as_written.to_string(),
158                doc: doc.to_string(),
159                kind: kind.to_string(),
160            },
161        );
162    }
163
164    /// VBA `VlaSubDoc`: a procedure's docstring, or empty.
165    pub fn sub_doc(&self, name: &str) -> String {
166        self.sub_docs
167            .get(&fold(name))
168            .map(|d| d.doc.clone())
169            .unwrap_or_default()
170    }
171
172    /// VBA `SubExists`: has this compile emitted a procedure by this name?
173    fn sub_exists(&self, as_written: &str) -> bool {
174        self.sub_docs.contains_key(&fold(as_written))
175    }
176
177    /// The recorded procedures, as written, with their kinds: what
178    /// `VlaAproposText` would list.
179    pub fn procedures(&self) -> Vec<(String, String)> {
180        self.sub_docs
181            .values()
182            .map(|d| (d.as_written.clone(), d.kind.clone()))
183            .collect()
184    }
185
186    /// VBA `EmitTop`.
187    fn emit_top(&mut self, f: &Form) -> Result<String, Refusal> {
188        let Form::List(lst) = f else {
189            return Err(raise(
190                "vla-top-level-not-list",
191                &[("value", &f.atom_text().unwrap_or_default())],
192            ));
193        };
194        let h = resolve_head_alias(&fold(&lst.head_sym()?));
195        match h.as_str() {
196            "sub" => {
197                self.proc_emitted = true; // G12.1
198                self.emit_proc(lst, false, "Public")
199            }
200            "function" => {
201                self.proc_emitted = true;
202                self.emit_proc(lst, true, "Public")
203            }
204            "dim" => {
205                self.topo_guard("dim")?;
206                Ok(format!("{}\r\n", self.emit_dim_core(lst)?))
207            }
208            "const" => {
209                self.topo_guard("const")?;
210                Ok(format!("{}\r\n", self.emit_const_core(lst)?))
211            }
212            "raw" => Ok(format!("{}\r\n", str_lit_content(lst.nth(2)?)?)),
213            "type" => self.emit_type_def(lst, ""),
214            "enum" => self.emit_enum_def(lst, ""),
215            "public" | "private" => {
216                // C2: visibility as a wrapper form.
217                let vis = if h == "public" { "Public" } else { "Private" };
218                self.emit_visibility(lst, vis)
219            }
220            "begin" => {
221                let mut out = String::new();
222                for i in 2..=lst.count() {
223                    out.push_str(&self.emit_top(lst.nth(i)?)?);
224                }
225                Ok(out)
226            }
227            "deflambda" => Err(raise("vla-deflambda-runs", &[])),
228            "include" => Err(raise("vla-include-must-stand-alone", &[])),
229            _ => Err(raise("vla-unknown-top-level-form", &[("head", &h)])),
230        }
231    }
232
233    /// VBA `EmitProc`: `(sub name (params) ["doc"] body...)` and
234    /// `(function name (params) [type] ["doc"] body...)`.
235    fn emit_proc(&mut self, lst: &List, is_func: bool, vis: &str) -> Result<String, Refusal> {
236        let raw_name = sym_text(lst.nth(2)?)?;
237        // IN.7: only an on:click: handler may carry a colon into a name.
238        if raw_name.contains(':') && !fold(&raw_name).starts_with("on:click:") {
239            return Err(raise(
240                "vla-interpreter-only-handler",
241                &[("name", &raw_name)],
242            ));
243        }
244        let name = sym_name(&raw_name)?;
245        let params = lst.nth_list(3)?;
246
247        let mut body_idx = 4;
248        let mut ret_type = String::from("Variant");
249        if is_func && lst.count() >= 4 {
250            // P.L6: an atom after the params that is not a string literal
251            // is the return type.
252            if let Form::Sym(s) = lst.nth(4)? {
253                ret_type = sym_name(s)?;
254                body_idx = 5;
255            }
256        }
257
258        // P.L6: an optional docstring, documentation only when at least
259        // one more form follows.
260        let mut pdoc = String::new();
261        if lst.count() > body_idx {
262            if let Form::Str(s) = lst.nth(body_idx)? {
263                pdoc = s.clone();
264                body_idx += 1;
265            }
266        }
267        self.record_sub_doc(&raw_name, &pdoc, if is_func { "function" } else { "sub" });
268
269        // L18: TCO, armed for a function whose every parameter is
270        // (byval name type) and whose body holds a self-return.
271        self.tco_name.clear();
272        self.tco_params.clear();
273        if is_func {
274            let mut all_by_val = true;
275            for pv in &params.items {
276                match pv {
277                    Form::List(pvl) => {
278                        if fold(&pvl.head_sym()?) != "byval" {
279                            all_by_val = false;
280                        }
281                    }
282                    _ => all_by_val = false,
283                }
284            }
285            if all_by_val && tco_scan(lst, body_idx, &name)? {
286                self.tco_name = fold(&name);
287                for pv in &params.items {
288                    let Form::List(pvl) = pv else { unreachable!() };
289                    self.tco_params.push(sym_name(&sym_text(pvl.nth(2)?)?)?);
290                }
291            }
292        }
293
294        self.func_name = name.clone();
295        self.in_function = is_func;
296
297        let kind = if is_func { "Function" } else { "Sub" };
298        let mut r = format!("{vis} {kind} {name}({})", self.emit_params(params)?);
299        if is_func {
300            r.push_str(&format!(" As {ret_type}"));
301        }
302        r.push_str("\r\n");
303        if !self.tco_name.is_empty() {
304            // L18: the loop head; temps only with more than one parameter.
305            if self.tco_params.len() > 1 {
306                for ti in 1..=self.tco_params.len() {
307                    r.push_str(&format!("    Dim vla_tco_{ti} As Variant\r\n"));
308                }
309            }
310            r.push_str("vla_tco:\r\n");
311        }
312        r.push_str(&self.emit_body(lst, body_idx, 1)?);
313        r.push_str(&format!("End {kind}\r\n"));
314        self.tco_name.clear();
315        self.tco_params.clear();
316        Ok(r)
317    }
318
319    /// VBA `EmitParams`: `name | (name type) | (byval name type) |
320    /// (byref name type) | (optional name type [default]) | (paramarray name)`.
321    fn emit_params(&self, params: &List) -> Result<String, Refusal> {
322        let mut parts: Vec<String> = Vec::with_capacity(params.count());
323        for p in &params.items {
324            let one = match p {
325                Form::List(pl) => {
326                    let ph = fold(&pl.head_sym()?);
327                    match ph.as_str() {
328                        "byval" => format!(
329                            "ByVal {} As {}",
330                            sym_name(&sym_text(pl.nth(2)?)?)?,
331                            sym_name(&sym_text(pl.nth(3)?)?)?
332                        ),
333                        "byref" => format!(
334                            "ByRef {} As {}",
335                            sym_name(&sym_text(pl.nth(2)?)?)?,
336                            sym_name(&sym_text(pl.nth(3)?)?)?
337                        ),
338                        "optional" => {
339                            let mut one = format!(
340                                "Optional {} As {}",
341                                sym_name(&sym_text(pl.nth(2)?)?)?,
342                                sym_name(&sym_text(pl.nth(3)?)?)?
343                            );
344                            if pl.count() >= 4 {
345                                one.push_str(&format!(" = {}", self.emit_expr(pl.nth(4)?)?));
346                            }
347                            one
348                        }
349                        "paramarray" => format!(
350                            "ParamArray {}() As Variant",
351                            sym_name(&sym_text(pl.nth(2)?)?)?
352                        ),
353                        _ => format!(
354                            "{} As {}",
355                            sym_name(&sym_text(pl.nth(1)?)?)?,
356                            sym_name(&sym_text(pl.nth(2)?)?)?
357                        ),
358                    }
359                }
360                // A bare name is ByRef Variant.
361                _ => sym_name(&p.atom_text().unwrap_or_default())?,
362            };
363            parts.push(one);
364        }
365        Ok(parts.join(", "))
366    }
367
368    /// VBA `EmitBody`.
369    fn emit_body(&mut self, lst: &List, from_idx: usize, ind: usize) -> Result<String, Refusal> {
370        let mut out = String::new();
371        for i in from_idx..=lst.count() {
372            out.push_str(&self.emit_stmt(lst.nth(i)?, ind)?);
373        }
374        Ok(out)
375    }
376}
377
378/// VBA `TcoScan`: does the body hold a `(return (self ...))`?
379fn tco_scan(lst: &List, from_idx: usize, self_name: &str) -> Result<bool, Refusal> {
380    for i in from_idx..=lst.count() {
381        if tco_scan_form(lst.nth(i)?, self_name)? {
382            return Ok(true);
383        }
384    }
385    Ok(false)
386}
387
388/// VBA `TcoScanForm`: only a return's direct operand counts.
389fn tco_scan_form(v: &Form, self_name: &str) -> Result<bool, Refusal> {
390    let Form::List(lst) = v else {
391        return Ok(false);
392    };
393    let Some(Form::Sym(head)) = lst.items.first() else {
394        return Ok(false);
395    };
396    let h = fold(head);
397    if h == "quote" {
398        return Ok(false); // data, never code
399    }
400    if h == "return" && lst.count() >= 2 {
401        if let Form::List(inner) = lst.nth(2)? {
402            if let Some(Form::Sym(callee)) = inner.items.first() {
403                if fold(&sym_name(callee)?) == fold(self_name) {
404                    return Ok(true);
405                }
406            }
407        }
408    }
409    for e in &lst.items {
410        if tco_scan_form(e, self_name)? {
411            return Ok(true);
412        }
413    }
414    Ok(false)
415}
416
417/// VBA `MakeButtonHelperText`: the compiled module's own button-creation
418/// helper, appended once by `VlaTranspile` when `make-button` was used.
419pub fn make_button_helper_text() -> String {
420    [
421        format!("Private Sub {MAKEBUTTON_SUB}(ByVal vlaCaption As String, ByVal vlaPlace As Range, ByVal vlaProc As String)"),
422        "    Dim vlaWs As Worksheet".to_string(),
423        "    Set vlaWs = vlaPlace.Worksheet".to_string(),
424        "    On Error Resume Next".to_string(),
425        "    vlaWs.Buttons(vlaCaption).Delete".to_string(),
426        "    On Error GoTo 0".to_string(),
427        "    Dim vlaBtn As Button".to_string(),
428        "    Set vlaBtn = vlaWs.Buttons.Add(vlaPlace.Left, vlaPlace.Top, 120, 24)".to_string(),
429        "    vlaBtn.Caption = vlaCaption".to_string(),
430        "    vlaBtn.Name = vlaCaption".to_string(),
431        "    If Len(vlaProc) > 0 Then".to_string(),
432        "        vlaBtn.OnAction = \"'\" & ThisWorkbook.Name & \"'!\" & vlaProc".to_string(),
433        "    End If".to_string(),
434        "End Sub".to_string(),
435        String::new(),
436    ]
437    .join("\r\n")
438}
439
440/// `SpliceIncludes`' recognition of a whole-line include, `IsIncludeLine`:
441/// the core cannot read the file, so it refuses by name where the VBA
442/// would splice; a door that reads files splices first.
443fn refuse_includes(source: &str) -> Result<(), Refusal> {
444    if !fold(source).contains("(include") {
445        return Ok(());
446    }
447    for line in source.split('\n') {
448        if let Some(name) = include_line_file(line) {
449            return Err(raise(
450                "vla-include-cannot-read",
451                &[("name", &name), ("path", &name)],
452            ));
453        }
454    }
455    Ok(())
456}
457
458/// VBA `IsIncludeLine`: `(include "name")` alone on its line.
459fn include_line_file(line: &str) -> Option<String> {
460    let t = line.trim();
461    let head = t.get(..9)?;
462    if fold(head) != "(include " || !t.ends_with(')') {
463        return None;
464    }
465    let inner = t[9..t.len() - 1].trim();
466    if inner.len() < 2 || !inner.starts_with('"') || !inner.ends_with('"') {
467        return None;
468    }
469    let name = &inner[1..inner.len() - 1];
470    if name.is_empty() || name.contains('"') {
471        return None;
472    }
473    Some(name.to_string())
474}
475
476#[cfg(test)]
477mod tests {
478    use super::*;
479
480    const PRELUDE: &str = include_str!("../../../scripts/prelude.vla");
481    const GOLDEN_VLA: &str = include_str!("../../../scripts/instructions_golden.vla");
482    const GOLDEN_VBA: &str = include_str!("../../../scripts/instructions_golden.vba");
483
484    fn vba(src: &str) -> String {
485        compile(src, PRELUDE).unwrap_or_else(|e| panic!("{src}: {e}"))
486    }
487
488    fn refusal(src: &str) -> Refusal {
489        match compile(src, PRELUDE) {
490            Ok(text) => panic!("{src} compiled instead of refusing: {text}"),
491            Err(e) => e,
492        }
493    }
494
495    #[test]
496    fn a_sub_and_a_function_with_their_shapes() {
497        assert_eq!(
498            vba("(sub t () (set! x 1))"),
499            "Option Explicit\r\n\r\nPublic Sub t()\r\n    x = 1 ' vla:1\r\nEnd Sub\r\n\r\n"
500        );
501        let f = vba("(function tax ((amount Variant)) (return (* amount 0.08)))");
502        assert!(f.contains("Public Function tax(amount As Variant) As Variant\r\n"));
503        assert!(f.contains("    tax = (amount * 0.08) ' vla:1\r\n    Exit Function\r\n"));
504        let g = vba("(function f ((byval n Long)) Long \"the doc\" (return (+ n 1)))");
505        assert!(g.contains("Public Function f(ByVal n As Long) As Long\r\n"));
506        assert!(g.contains("    f = (n + 1) ' vla:1\r\n"));
507        let p = vba(
508            "(sub s ((a Long) (byref b String) (optional c Variant 1) (paramarray d) e) (exit-sub))",
509        );
510        assert!(p.contains(
511            "Public Sub s(a As Long, ByRef b As String, Optional c As Variant = 1, ParamArray d() As Variant, e)\r\n"
512        ));
513    }
514
515    #[test]
516    fn module_level_forms_and_the_topology_guard() {
517        let m = vba("(const k 5)\n(dim x Long)\n(private (dim m Long))\n(public (const q \"s\"))\n(type Pt (x Double) y)\n(enum E a (b 2))\n(raw \"' hello\")");
518        assert!(m.contains("Const k = 5\r\n\r\nDim x As Long\r\n\r\nPrivate m As Long\r\n\r\nPublic Const q = \"s\"\r\n\r\n"));
519        assert!(m.contains("Type Pt\r\n    x As Double\r\n    y As Variant\r\nEnd Type\r\n\r\n"));
520        assert!(m.contains("Enum E\r\n    a\r\n    b = 2\r\nEnd Enum\r\n\r\n' hello\r\n"));
521        let r = refusal("(sub t () (debug-print 1))\n(dim late Long)");
522        assert_eq!(r.id, "vla-module-level-decl-after-proc");
523        assert!(r.text.contains("must come before the first"));
524        assert_eq!(refusal("5").id, "vla-top-level-not-list");
525        assert_eq!(refusal("(nonsense 1)").id, "vla-unknown-top-level-form");
526        assert_eq!(refusal("(deflambda f (x) x)").id, "vla-deflambda-runs");
527    }
528
529    #[test]
530    fn a_formula_write_calls_the_runtime_sink() {
531        // SEC.15: exactly (set! (. obj formula) v) is a call to the runtime's
532        // VlaSetFormula; a read of the member, and any other member, are
533        // written as they were.
534        let f = vba("(sub t () (set! (. (range \"b3\") formula) \"=A1\"))");
535        assert!(
536            f.contains("    Call VlaSetFormula(range(\"b3\"), \"=A1\") ' vla:1\r\n"),
537            "{f}"
538        );
539        assert!(!f.contains("Formula2"), "{f}");
540        let g = vba("(sub t () (set! x (. (range \"b3\") formula)))");
541        assert!(
542            g.contains("    x = range(\"b3\").formula ' vla:1\r\n"),
543            "{g}"
544        );
545        let h = vba("(sub t () (set! (. (range \"b3\") value) \"=A1\"))");
546        assert!(
547            h.contains("    range(\"b3\").value = \"=A1\" ' vla:1\r\n"),
548            "{h}"
549        );
550    }
551
552    #[test]
553    fn the_refusal_names_the_nearest_mapped_line() {
554        let r = refusal("(sub t ()\n  (set! x 1)\n  (if))");
555        assert!(r.text.ends_with(" (near vla line 3)"), "{}", r.text);
556        let r = refusal("(sub t () (at-line 40 (set! x 1) (if)))");
557        assert!(
558            r.text.ends_with(" (near vla line 1, source line 40)"),
559            "{}",
560            r.text
561        );
562    }
563
564    #[test]
565    fn tail_calls_rebind_and_jump() {
566        let f = vba("(function count-down ((byval n Long) (byval acc Long)) Long (if (= n 0) (then (return acc)) (else (return (count-down (- n 1) (+ acc n))))))");
567        assert!(f.contains(
568            "    Dim vla_tco_1 As Variant\r\n    Dim vla_tco_2 As Variant\r\nvla_tco:\r\n"
569        ));
570        assert!(f.contains("        vla_tco_1 = (n - 1) ' vla:1\r\n        vla_tco_2 = (acc + n)\r\n        n = vla_tco_1\r\n        acc = vla_tco_2\r\n        GoTo vla_tco\r\n"));
571        let g = vba("(function loop-it ((byval n Long)) Long (if (= n 0) (then (return 0)) (else (return (loop-it (- n 1))))))");
572        assert!(g.contains("vla_tco:\r\n"));
573        assert!(g.contains("        n = (n - 1) ' vla:1\r\n        GoTo vla_tco\r\n"));
574        // A ByRef parameter keeps true recursion: not a byte of TCO.
575        let h = vba("(function r ((n Long)) Long (return (r (- n 1))))");
576        assert!(!h.contains("vla_tco"));
577        assert!(h.contains("    r = r((n - 1)) ' vla:1\r\n"));
578    }
579
580    #[test]
581    fn make_button_appends_its_helper_once() {
582        let t = vba("(sub on:click:go () (debug-print 1))\n(sub main () (make-button \"go\" (range \"A1\")))");
583        assert!(t.contains("Call VlaCompiledMakeButton(\"go\", range(\"A1\"), \"on_click_go\")"));
584        assert!(t.ends_with(&format!("\r\n{}", make_button_helper_text())));
585        assert_eq!(t.matches("Private Sub VlaCompiledMakeButton").count(), 1);
586        let u = vba("(sub main () (make-button \"go\" (range \"A1\")))");
587        assert!(u.contains(", \"\")"));
588        assert_eq!(
589            refusal("(sub on:sheet-change () (debug-print 1))").id,
590            "vla-interpreter-only-handler"
591        );
592    }
593
594    #[test]
595    fn an_include_line_is_refused_by_name_here() {
596        let r = refusal("(include \"lib.vla\")\n(sub t () (debug-print 1))");
597        assert_eq!(r.id, "vla-include-cannot-read");
598        assert!(r.text.contains("lib.vla"));
599        assert_eq!(
600            include_line_file("  (include \"a b.vla\")  "),
601            Some("a b.vla".to_string())
602        );
603        assert_eq!(include_line_file("(include \"a\") x"), None);
604        assert_eq!(include_line_file("(include-all \"a\")"), None);
605        assert_eq!(
606            refusal("(sub t () (include \"x\"))").id,
607            "vla-include-must-stand-alone"
608        );
609    }
610
611    #[test]
612    fn the_golden_compiles_to_the_golden_from_its_second_line_on() {
613        // The treaty's oracle 1b: the stamp line is the writer's, not the
614        // program's; what follows it compiled to instructions_golden.vba.
615        let program = GOLDEN_VLA.split_once('\n').expect("a stamp line").1;
616        let got = compile(program, PRELUDE).unwrap_or_else(|e| panic!("{e}"));
617        let want = GOLDEN_VBA;
618        let norm = |s: &str| s.replace("\r\n", "\n").trim_end_matches('\n').to_string();
619        let (got, want) = (norm(&got), norm(want));
620        if got != want {
621            let at = got
622                .chars()
623                .zip(want.chars())
624                .position(|(a, b)| a != b)
625                .unwrap_or(got.len().min(want.len()));
626            let line = want[..at].matches('\n').count() + 1;
627            let context = |s: &str| {
628                let start = s[..at].rfind('\n').map(|i| i + 1).unwrap_or(0);
629                let end = s[at..].find('\n').map(|i| at + i).unwrap_or(s.len());
630                s[start..end].to_string()
631            };
632            panic!(
633                "differs at char {at} of {} (line {line}):\n  want: {}\n  got:  {}",
634                want.len(),
635                context(&want),
636                context(&got)
637            );
638        }
639    }
640
641    #[test]
642    fn a_program_at_the_language_s_depth_limit_compiles_on_a_small_stack() {
643        // VLA.1: the emitter recurses once a level of a form and is the
644        // shallowest stage (a debug door compiled 80 levels and overflowed
645        // at 96), so a program nested to the language's limit is compiled
646        // on a 1 MiB thread, and one level deeper is refused by name before
647        // the emitter sees it.
648        let program = |n: usize| {
649            format!(
650                "(sub main () (debug-print {}1{}))",
651                "(+ 1 ".repeat(n),
652                ")".repeat(n)
653            )
654        };
655        let limit = crate::reader::FORM_DEPTH_LIMIT as usize;
656        std::thread::Builder::new()
657            .stack_size(1 << 20)
658            .spawn(move || {
659                let vba = compile(&program(limit - 2), "").unwrap_or_else(|e| panic!("{e}"));
660                assert!(vba.contains("Debug.Print (1 + (1 + "));
661                let past = compile(&program(limit - 1), "").unwrap_err();
662                assert_eq!(past.id, "vla-form-too-deep");
663            })
664            .unwrap()
665            .join()
666            .unwrap();
667    }
668}