Skip to main content

blue_lang_syntax/
parse.rs

1//! Precedence-climbing parser, lowering straight to the tatara-lisp
2//! quoted form.
3//!
4//! **The load-bearing design decision, made here and once:** the parser's
5//! output IS a `tatara_lisp::Sexp`. There is no private blue AST that later
6//! gets converted. That is Tenet 1 — *blue source parses to tatara-lisp* —
7//! and building it any other way would make homoiconicity a conversion step
8//! rather than an identity, which is the difference between blue's macro
9//! story working and merely being claimed.
10//!
11//! The consequence to keep in view: every surface construct must have a
12//! well-defined s-expression it means. Where the mapping is not obvious it
13//! is written down in the test module, because the tests are the
14//! specification of the surface until the mechanized spec exists.
15//!
16//! ## Spans are built, not bolted on
17//!
18//! Every production returns a [`Spanned`] — tatara-lisp's parallel tree where
19//! **each node** carries the byte range that produced it — and the spanless
20//! [`parse_program`] is a projection of it via `Spanned::to_sexp`.
21//!
22//! That direction is deliberate and it is the second time this file has learned
23//! the lesson: the spanless tree is derivable from the spanned one and the
24//! reverse is not. When the parser produced `Sexp` and callers wanted positions,
25//! the only answers available were *invent one* (a squiggle under unrelated
26//! code) or *use nothing* — and the type checker took the second, so every type
27//! error in the editor was reported at line 0, column 0 regardless of where it
28//! was. A span discarded at the parser cannot be recovered by anything
29//! downstream, exactly as [`crate::lex`] says of trivia.
30//!
31//! Two honest limits, both stated where they are caused rather than inferred:
32//!
33//! - A node blue **synthesizes** (the `define` a `def` lowers to, the `not` an
34//!   `unless` lowers to, the `equal?` a `case` arm compares with) has no source
35//!   text of its own. It is given the span of the token that *caused* it — the
36//!   `def`, the `unless`, the `when` — which is where a reader looks when told
37//!   something about it.
38//! - An interpolation's inner expression is parsed from a **sub-source** (the
39//!   text between `#{` and `}`), so its own offsets are relative to that
40//!   substring and would point into the wrong buffer. Those nodes are stamped
41//!   with the whole string literal's span instead. Sub-span accuracy inside an
42//!   interpolation needs the lexer to record each part's offset in the outer
43//!   source, which it does not.
44
45use tatara_lisp::{Atom, Sexp, Spanned, SpannedForm};
46
47use crate::lex::{lex, Span, Token, TokenKind};
48use crate::spelling::{Note, Spelling};
49
50/// Why a source did not parse, and where.
51///
52/// **Structured, and located at the boundary.** `message` says what was
53/// expected and what was found, in blue's own words; `span` is the byte range
54/// of the offending text; `line` and `col` are that range's start, 1-based,
55/// with the column counted in characters. Every public entry point fills
56/// `line`/`col` from the source it was given, so `Display` reads
57/// `3:7: expected `)`, found `]`` and never a byte offset.
58///
59/// The byte offsets used to BE the display (`… at 10..11`), and a Rust
60/// `Debug` rendering named the token (`found Ident("y")`). Both are facts
61/// about the implementation; neither is something an author can act on.
62/// The span stays, as data, for any consumer that needs to point into the
63/// buffer — an editor squiggle, a diagnostic code attached downstream.
64#[derive(Clone, Debug, PartialEq)]
65pub struct ParseError {
66    pub message: String,
67    pub span: Span,
68    /// 1-based line of `span.start`.
69    pub line: usize,
70    /// 1-based column of `span.start`, in characters.
71    pub col: usize,
72}
73
74impl ParseError {
75    /// An error at `span`, not yet located: the parser sees tokens, not text,
76    /// so the line and column are filled by [`Self::located`] at the entry
77    /// point that holds the source.
78    pub(crate) fn new(message: impl Into<String>, span: Span) -> Self {
79        Self {
80            message: message.into(),
81            span,
82            line: 0,
83            col: 0,
84        }
85    }
86
87    /// Resolve `line`/`col` against the source this error's span indexes.
88    pub(crate) fn located(mut self, src: &str) -> Self {
89        let (line, col) = Span::line_col(src, self.span.start.min(src.len()));
90        self.line = line;
91        self.col = col;
92        self
93    }
94}
95
96impl std::fmt::Display for ParseError {
97    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
98        write!(f, "{}:{}: {}", self.line, self.col, self.message)
99    }
100}
101
102impl std::error::Error for ParseError {}
103
104impl From<crate::lex::LexError> for ParseError {
105    fn from(e: crate::lex::LexError) -> Self {
106        Self::new(e.message, e.span)
107    }
108}
109
110thread_local! {
111    static PARSES: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
112}
113
114/// How many programs this thread has parsed: an independent count for a
115/// caller that promises to parse a text once, read off the parser itself
116/// rather than off the caller's own bookkeeping.
117#[must_use]
118pub fn parses_on_this_thread() -> u64 {
119    PARSES.with(std::cell::Cell::get)
120}
121
122/// Parse a blue program into a sequence of tatara-lisp forms.
123pub fn parse_program(src: &str) -> Result<Vec<Sexp>, ParseError> {
124    parse_program_with_depth(src, MAX_EXPR_DEPTH)
125}
126
127/// Parse a blue program keeping **every node's** source span.
128///
129/// This is the parser's real output; [`parse_program`] projects the spans away.
130/// Reach for this one whenever a position is going to be shown to a human — an
131/// editor squiggle, a `line:col` in a CLI error, a future debugger's breakpoint
132/// table. Reach for [`parse_program`] only when the consumer genuinely does not
133/// care where anything was.
134pub fn parse_program_tree(src: &str) -> Result<Vec<Spanned>, ParseError> {
135    parse_program_tree_with_depth(src, MAX_EXPR_DEPTH)
136}
137
138/// [`parse_program_tree`] with the nesting bound supplied by the caller.
139pub fn parse_program_tree_with_depth(
140    src: &str,
141    max_depth: usize,
142) -> Result<Vec<Spanned>, ParseError> {
143    parse_program_documented_with_depth(src, max_depth).map(crate::doc::strip)
144}
145
146/// Parse keeping the documentation nodes `@doc` and `@moduledoc` lower to
147/// ([`crate::doc`]). Every other `parse_program*` door strips them.
148pub fn parse_program_documented(src: &str) -> Result<Vec<Spanned>, ParseError> {
149    parse_program_documented_with_depth(src, MAX_EXPR_DEPTH)
150}
151
152/// [`parse_program_documented`] with the nesting bound supplied by the caller.
153pub fn parse_program_documented_with_depth(
154    src: &str,
155    max_depth: usize,
156) -> Result<Vec<Spanned>, ParseError> {
157    if let Some(hit) = RECENT.with(|r| {
158        r.borrow()
159            .iter()
160            .find(|(d, s, _)| *d == max_depth && s.as_str() == src)
161            .map(|(_, _, t)| t.clone())
162    }) {
163        return hit;
164    }
165    PARSES.with(|n| n.set(n.get() + 1));
166    let parsed = lex(src)
167        .map_err(|e| ParseError::from(e).located(src))
168        .and_then(|toks| {
169            Parser::over(src, toks, max_depth)
170                .program()
171                .map_err(|e| e.located(src))
172        });
173    RECENT.with(|r| {
174        let mut r = r.borrow_mut();
175        if r.len() == RECENT_TEXTS {
176            drop(r.remove(0));
177        }
178        r.push((max_depth, src.to_string(), parsed.clone()));
179    });
180    parsed
181}
182
183/// A doc example's expression or statement: part of its text's parse, so
184/// neither counted nor kept apart from it.
185pub(crate) fn parse_snippet(src: &str) -> Result<Vec<Spanned>, ParseError> {
186    let toks = lex(src).map_err(|e| ParseError::from(e).located(src))?;
187    Parser::over(src, toks, MAX_EXPR_DEPTH)
188        .program()
189        .map(crate::doc::strip)
190        .map_err(|e| e.located(src))
191}
192
193/// How many recent texts each thread keeps the parse of. One text is read
194/// by several views in one check (the stripped tree, its documentation, its
195/// doctests, the doc rules); they share one parse.
196const RECENT_TEXTS: usize = 4;
197
198type Recent = Vec<(usize, String, Result<Vec<Spanned>, ParseError>)>;
199
200thread_local! {
201    static RECENT: std::cell::RefCell<Recent> = const { std::cell::RefCell::new(Vec::new()) };
202}
203
204/// Whether `x[key]` reads `key` as a map key, `get(x, key)`: a keyword or
205/// string literal. Any other key indexes a list, `nth(key, x)` (B0066).
206#[must_use]
207pub fn is_index_key(key: &Spanned) -> bool {
208    key.as_keyword().is_some() || key.as_string().is_some()
209}
210
211/// The text a constant string form spells, for a reader that takes text from
212/// a tree without evaluating it: a string, `line_break`, or `join_lines([…])` /
213/// `unlines([…])` over constant texts, which is how the parser reads a string
214/// holding a line break (B0065). `None` for anything computed.
215#[must_use]
216pub fn constant_text(s: &Sexp) -> Option<String> {
217    match s {
218        Sexp::Atom(Atom::Str(x)) => Some(x.to_string()),
219        Sexp::Atom(Atom::Symbol(h)) if h.as_str() == "line_break" => Some("\n".to_string()),
220        Sexp::List(items) => match items.as_slice() {
221            [Sexp::Atom(Atom::Symbol(h)), Sexp::List(list)]
222                if h.as_str() == "join_lines" || h.as_str() == "unlines" =>
223            {
224                let (Sexp::Atom(Atom::Symbol(l)), elems) = list.split_first()? else {
225                    return None;
226                };
227                if l.as_str() != "list" {
228                    return None;
229                }
230                let texts = elems
231                    .iter()
232                    .map(constant_text)
233                    .collect::<Option<Vec<_>>>()?;
234                Some(if h.as_str() == "join_lines" {
235                    texts.join("\n")
236                } else {
237                    texts.iter().map(|t| format!("{t}\n")).collect()
238                })
239            }
240            _ => None,
241        },
242        _ => None,
243    }
244}
245
246pub fn spelling_notes(src: &str) -> Result<Vec<Note>, ParseError> {
247    let mut p = Parser::over(
248        src,
249        lex(src).map_err(|e| ParseError::from(e).located(src))?,
250        MAX_EXPR_DEPTH,
251    );
252    p.program().map_err(|e| e.located(src))?;
253    Ok(p.notes)
254}
255
256/// Parse a program written in a [`Yakugo`](crate::yakugo::Yakugo) surface.
257///
258/// The pack is applied to the TOKEN STREAM, between lexing and parsing, so the
259/// parser below is untouched and every keyword site works by construction.
260/// What comes out is the same `Sexp` the English surface produces — that is
261/// the invariant the pack tests assert, and the only thing that makes a
262/// surface a surface rather than a dialect.
263///
264/// # Errors
265///
266/// Lex and parse errors propagate unchanged. A pack cannot repair source that
267/// does not tokenize, and reporting otherwise would name the wrong problem.
268pub fn parse_program_in(src: &str, pack: &crate::yakugo::Yakugo) -> Result<Vec<Sexp>, ParseError> {
269    Ok(parse_program_tree_in(src, pack)?
270        .iter()
271        .map(Spanned::to_sexp)
272        .collect())
273}
274
275/// [`parse_program_in`] keeping **every node's** source span.
276///
277/// The same relation [`parse_program_tree`] has to [`parse_program`], and it
278/// exists for the same reason: a surface is a spelling, so a program written in
279/// one has positions exactly as real as an English-surface program's. Without
280/// this door, `pipeline::run_in_surface` would have to lift a spanless tree back
281/// up and every type error in a `yakugo` program would report `<synthetic>` —
282/// a position the parser HAD and threw away one line earlier, which is the
283/// class `Diagnostic::span`'s own docs name.
284///
285/// [`parse_program_in`] is now its projection, so the two cannot drift.
286///
287/// # Errors
288///
289/// As [`parse_program_in`].
290pub fn parse_program_tree_in(
291    src: &str,
292    pack: &crate::yakugo::Yakugo,
293) -> Result<Vec<Spanned>, ParseError> {
294    let toks =
295        crate::yakugo::canonical_tokens(src, pack).map_err(|e| ParseError::from(e).located(src))?;
296    PARSES.with(|n| n.set(n.get() + 1));
297    let mut p = Parser::over(src, toks, MAX_EXPR_DEPTH);
298    p.program()
299        .map(crate::doc::strip)
300        .map_err(|e| e.located(src))
301}
302
303/// [`parse_program`] with the nesting bound supplied by the caller.
304///
305/// The bound is a **safety limit, not a dialect**: `max_depth` cannot change
306/// what any program means, only whether a pathological one is refused before
307/// the stack is at risk. That is why it is a parameter here and
308/// [`MAX_EXPR_DEPTH`] is only the default — a knob that could alter meaning
309/// would belong nowhere near a config file (see `blue-lang-cli`'s `config`
310/// module for the rule this obeys).
311pub fn parse_program_with_depth(src: &str, max_depth: usize) -> Result<Vec<Sexp>, ParseError> {
312    Ok(parse_program_tree_with_depth(src, max_depth)?
313        .iter()
314        .map(Spanned::to_sexp)
315        .collect())
316}
317
318/// Parse, keeping each top-level form's source span.
319///
320/// The spans are what let the formatter put comments back. A comment is not
321/// part of a program's *meaning*, so it has no place in the `Sexp` tree —
322/// putting it there would break canonicality, since two programs differing only
323/// in a comment would stop formatting identically. Instead the formatter renders
324/// the tree and re-interleaves comments by position, which needs to know where
325/// each form started and ended.
326///
327/// A projection of [`parse_program_tree`], which carries a span on every node
328/// rather than only the top-level ones. This entry point survives because the
329/// formatter wants exactly the top-level pairing and nothing deeper.
330pub fn parse_program_spanned(src: &str) -> Result<Vec<(Sexp, Span)>, ParseError> {
331    parse_program_spanned_with_depth(src, MAX_EXPR_DEPTH)
332}
333
334/// [`parse_program_spanned`] with the nesting bound supplied by the caller.
335pub fn parse_program_spanned_with_depth(
336    src: &str,
337    max_depth: usize,
338) -> Result<Vec<(Sexp, Span)>, ParseError> {
339    Ok(parse_program_tree_with_depth(src, max_depth)?
340        .iter()
341        .map(|f| (f.to_sexp(), f.span))
342        .collect())
343}
344
345/// Every comment in `src`, with its byte span and whether it sits alone on its
346/// line.
347///
348/// `own_line` is the distinction that decides placement: a comment alone on its
349/// line belongs *before* the form that follows, while one after code on the same
350/// line belongs *to* that line. Conflating them moves a trailing note onto the
351/// wrong row.
352pub fn comments(src: &str) -> Vec<Comment> {
353    lex(src)
354        .map(|toks| {
355            toks.iter()
356                .filter_map(|t| match &t.kind {
357                    TokenKind::Comment(text) => Some(Comment {
358                        text: text.clone(),
359                        span: t.span,
360                        own_line: line_before_is_blank(src, t.span.start),
361                    }),
362                    _ => None,
363                })
364                .collect()
365        })
366        .unwrap_or_default()
367}
368
369/// A comment, and where it sat.
370#[derive(Clone, Debug, PartialEq, Eq)]
371pub struct Comment {
372    /// The text, including the leading `#`.
373    pub text: String,
374    pub span: Span,
375    /// Nothing but whitespace precedes it on its line.
376    pub own_line: bool,
377}
378
379fn line_before_is_blank(src: &str, start: usize) -> bool {
380    src[..start.min(src.len())]
381        .rsplit('\n')
382        .next()
383        .is_some_and(|prefix| {
384            prefix
385                .trim_matches(|c: char| c.is_whitespace() || c == '\u{feff}')
386                .is_empty()
387        })
388}
389
390/// Parse a single blue expression. Convenience for tests and the REPL.
391pub fn parse_expr(src: &str) -> Result<Sexp, ParseError> {
392    let forms = parse_program(src)?;
393    match forms.len() {
394        1 => Ok(forms.into_iter().next().expect("checked len")),
395        n => Err(ParseError::new(
396            format!("expected exactly one expression, found {n}"),
397            Span::new(0, src.len()),
398        )
399        .located(src)),
400    }
401}
402
403/// What an infix operator binds like, and what it lowers to.
404///
405/// **One row per operator, carrying both facts.** Precedence and callee
406/// used to live apart, and the cost was immediate: the surface spelling was
407/// emitted verbatim, so `a == b` lowered to `(== a b)` — a symbol no
408/// interpreter binds — and the program died at *runtime* with `unbound
409/// symbol ==`. Splitting the two invites exactly that: add an operator to
410/// the precedence table, forget the lowering, ship a parse that cannot run.
411/// Joined here, "has a precedence" and "has a callee" are the same fact.
412#[derive(Clone, Copy, Debug)]
413pub struct Infix {
414    /// Surface spelling.
415    pub op: &'static str,
416    /// Left and right binding power. Higher binds tighter.
417    pub power: (u8, u8),
418    /// The tatara-lisp callee this lowers to.
419    pub callee: &'static str,
420    /// One line for a reader: what the operator computes, and the trap in it
421    /// when there is one. `blue reference` prints it; a row without one fails
422    /// `every_operator_and_keyword_is_described`.
423    pub doc: &'static str,
424}
425
426/// The complete infix table. Precedence follows Ruby's where Ruby has an
427/// opinion; `|>` (below) sits under everything so `a |> f |> g` chains
428/// without parentheses.
429pub const INFIX: &[Infix] = &[
430    Infix {
431        op: "or",
432        power: (1, 2),
433        callee: "or",
434        doc: "Logical or: the left value if truthy, otherwise the right; the right is evaluated only when needed. `||` is read as `or` and written `or` (B0025).",
435    },
436    Infix {
437        op: "and",
438        power: (3, 4),
439        callee: "and",
440        doc: "Logical and: the right value if the left is truthy, otherwise the left; short-circuits. `&&` is read as `and` and written `and` (B0025).",
441    },
442    // `==` is STRUCTURAL equality, so it lowers to `equal?` and not to `=`.
443    //
444    // tatara's `=` is NUMERIC comparison: `"a" = "a"` is a type error, not
445    // false. Lowering `==` to it meant every string, list or nil comparison
446    // failed with "expected number, got string" — found by blue's own spec
447    // suite the moment a test compared two strings, which is the first thing
448    // anybody does.
449    //
450    // `equal?` is structural and total over the value domain: strings, lists
451    // and nil all compare, and numbers still compare as numbers.
452    Infix {
453        op: "==",
454        power: (5, 6),
455        callee: "equal?",
456        doc: "Structural equality over every value (okite D0001): 4 == 4.0, lists and maps by content, nil only equals nil.",
457    },
458    Infix {
459        op: "!=",
460        power: (5, 6),
461        callee: "not=",
462        doc: "Exactly not(a == b) (okite D0002).",
463    },
464    Infix {
465        op: "<",
466        power: (5, 6),
467        callee: "<",
468        doc: "Numeric less-than. A string operand is a type error: order text with compare(a, b).",
469    },
470    Infix {
471        op: "<=",
472        power: (5, 6),
473        callee: "<=",
474        doc: "Numeric less-than-or-equal. Numbers only, like <.",
475    },
476    Infix {
477        op: ">",
478        power: (5, 6),
479        callee: ">",
480        doc: "Numeric greater-than. Numbers only, like <.",
481    },
482    Infix {
483        op: ">=",
484        power: (5, 6),
485        callee: ">=",
486        doc: "Numeric greater-than-or-equal. Numbers only, like <.",
487    },
488    Infix {
489        op: "+",
490        power: (7, 8),
491        callee: "+",
492        doc: "Numeric addition; an Int and a Float give a Float. Not string concatenation: interpolate instead.",
493    },
494    Infix {
495        op: "-",
496        power: (7, 8),
497        callee: "-",
498        doc: "Numeric subtraction. Written before an operand with nothing on its left, it is unary minus: -n.",
499    },
500    Infix {
501        op: "*",
502        power: (9, 10),
503        callee: "*",
504        doc: "Numeric multiplication.",
505    },
506    Infix {
507        op: "/",
508        power: (9, 10),
509        callee: "/",
510        doc: "Division that never truncates (okite D0008): 7 / 2 is 3.5, while 6 / 2 stays the Int 3. Integer division is floor(a / b).",
511    },
512    Infix {
513        op: "%",
514        power: (9, 10),
515        callee: "mod",
516        doc: "Euclidean remainder: never negative, so (0 - 7) % 3 is 2, not -1. A zero divisor is a runtime error.",
517    },
518];
519
520/// Every surface keyword that BEGINS an expression.
521///
522/// Exists so the formatter's corpus can be checked against it. Three separate
523/// times a form was added to this parser, the formatter was not extended, and
524/// all three formatting laws still passed — because the corpus is
525/// hand-maintained and contained no example of the new form. The annotated
526/// `def` rendered as a method send; `defmacro` rendered as
527/// `defmacro(double, x(), …)`, which does not re-parse at all.
528///
529/// A law cannot notice a case nobody wrote down. `blue-lang-fmt`'s
530/// `every_surface_keyword_appears_in_the_corpus` closes that by making the
531/// omission itself the failure, so adding a keyword here forces the corpus
532/// entry that exercises the other three laws over it.
533///
534/// `do` and `end` are absent deliberately: they are block delimiters, not
535/// expression heads, and have no standalone rendering to test.
536/// The master verbs a bare call dispatches on the subject's space.
537pub const MASTER_VERBS: &[&str] = &[
538    "read", "need", "has?", "put", "update", "delete", "create", "diff", "info", "exists?",
539];
540
541/// The callee `assert e` lowers to.
542///
543/// **Owned here, by the lowering itself, and read by every consumer.** It was
544/// briefly a literal here and a separate `const` in `blue-lang-test`, and the
545/// shadowing gate then could not see a parser that lowered to the wrong name:
546/// the gate checked its own copy. Same duplication class as the operator table.
547pub const LOWERED_ASSERT: &str = "blue-assert";
548
549/// The callee a `{...}` map literal lowers to.
550///
551/// **`hash-map`, not `map`.** tatara binds `map` to the higher-order function,
552/// so a literal lowering to `(map …)` called the HOF with the key/value pairs
553/// as arguments and failed with "expected list, got int". Same shadowing class
554/// as [`LOWERED_ASSERT`], caught by the same gate once the name was listed
555/// there — it was not, which is why this shipped.
556pub const LOWERED_MAP: &str = "hash-map";
557
558/// The callee string interpolation lowers to.
559///
560/// blue's own `concat`, which renders either side through `to_s` — that is what
561/// lets `"n=#{42}"` interpolate a number. Not `+`, which is arithmetic.
562pub const LOWERED_CONCAT: &str = "concat";
563
564pub const OPTIONAL_MARKER: &str = "&optional";
565
566pub const LOWERED_OPTIONAL_ARITY: &str = "blue-optional-arity";
567
568pub const LOWERED_OPTIONAL_ARG: &str = "blue-optional-arg";
569
570pub const LOWERED_FIELD: &str = "blue-field";
571
572pub const LOWERED_MERGE: &str = "blue-merge";
573
574pub const LOWERED_SPLICE: &str = "blue-splice";
575
576pub const LOWERED_KWARGS: &str = "blue-kwargs";
577
578pub const LOWERED_ERROR_MATCH: &str = "blue-rescues?";
579
580pub const LOWERED_ENSURE: &str = "blue-ensure";
581
582pub const LOWERED_RERAISE: &str = "blue-reraise";
583
584pub const LOWERED_ATTEMPTED: &str = "blue-attempted";
585
586pub const LOWERED_FAILED: &str = "blue-failed";
587
588pub const LOWERED_HEADS: &[&str] = &[
589    "list",
590    "concat",
591    "not",
592    "apply",
593    "equal?",
594    "not=",
595    "and",
596    "or",
597    "mod",
598    "join_lines",
599    "unlines",
600    "line_break",
601];
602
603pub const RESCUED: &str = "rescued-error";
604
605pub const SURFACE_KEYWORDS: &[&str] = &[
606    "if",
607    "begin",
608    "unless",
609    "def",
610    "defmacro",
611    "quote",
612    "unquote",
613    "unquote_splice",
614    "test",
615    "assert",
616    "fn",
617    "case",
618];
619
620/// The words that are reserved but are not heads of a `SURFACE_KEYWORDS` form
621/// — block delimiters and the three literals.
622///
623/// Split out as data rather than inlined in a `matches!` because a reserved
624/// word is not only the parser's business: a highlighter has to paint exactly
625/// this set, and one that reads `SURFACE_KEYWORDS` alone silently under-paints
626/// `do` / `end` / `else` / `elsif`. That was not hypothetical — escriba's
627/// `escriba-render/src/langs.rs` hand-transcribed "SURFACE_KEYWORDS plus the
628/// four block words `is_reserved_word` adds", because the union was
629/// unreachable from outside this module. A transcription is a second
630/// definition, and it drifts the first time this list changes.
631pub const BLOCK_KEYWORDS: &[&str] = &[
632    "do", "end", "else", "elsif", "rescue", "ensure", "true", "false", "nil",
633];
634
635pub const OPERATOR_KEYWORDS: &[&str] = &["and", "or", "not"];
636
637/// A surface keyword may not be rebound. `if = 1` is a mistake, not a binding,
638/// and letting it through would shadow the form for the rest of the file.
639///
640/// Public because it is the ONE answer to "is this identifier a keyword" —
641/// the parser's rebinding check and every downstream highlighter read it here
642/// rather than each carrying its own copy of the list.
643pub fn is_reserved_word(name: &str) -> bool {
644    SURFACE_KEYWORDS.contains(&name)
645        || BLOCK_KEYWORDS.contains(&name)
646        || OPERATOR_KEYWORDS.contains(&name)
647}
648
649/// Can `name` be written as the head of a call — `name(…)`?
650///
651/// True exactly when the lexer reads `name` as one identifier and it is not
652/// reserved. Answered BY the lexer rather than by a character class copied
653/// from it, so the two cannot disagree: the formatter asks this before
654/// printing a tree as `name(args)`, and a head such as `hash-map`, `not=` or
655/// `if` — which a tree can carry and a call cannot spell — must get a
656/// different rendering or none.
657pub fn is_callable_name(name: &str) -> bool {
658    // `retsu/first` is written `retsu::first(…)`.
659    if qualified(name).is_some() {
660        return true;
661    }
662    !is_reserved_word(name)
663        && matches!(
664            lex(name).as_deref(),
665            Ok([t, e]) if matches!(&t.kind, TokenKind::Ident(n) if n == name)
666                && e.kind == TokenKind::Eof
667        )
668}
669
670/// The character a qualified name's AST symbol joins its two halves with:
671/// `retsu::first` is the symbol `retsu/first`. The surface never writes it
672/// (`/` is division, and no identifier can contain it), which is what makes
673/// a symbol with text on both sides of one `/` unambiguously qualified.
674pub const QUALIFIER: char = '/';
675
676/// The qualifier that names a builtin: `blue::count` is the interpreter's
677/// `count`, whatever a program defines. No package may be named it.
678pub const BUILTIN_QUALIFIER: &str = "blue";
679
680/// The AST symbol for `pkg::name`.
681#[must_use]
682pub fn qualify(pkg: &str, name: &str) -> String {
683    format!("{pkg}{QUALIFIER}{name}")
684}
685
686/// `retsu/first` → `Some(("retsu", "first"))`: the package and the name of a
687/// qualified symbol. `None` for every other symbol, `/` (division) included.
688///
689/// The ONE reader of the shape, so the formatter, the name table and the
690/// runtime's renderer cannot disagree about what counts as qualified.
691#[must_use]
692pub fn qualified(sym: &str) -> Option<(&str, &str)> {
693    let (pkg, name) = sym.split_once(QUALIFIER)?;
694    let ident = |s: &str| {
695        matches!(
696            lex(s).as_deref(),
697            Ok([t, e]) if matches!(&t.kind, TokenKind::Ident(n) if n == s) && e.kind == TokenKind::Eof
698        )
699    };
700    let named = |s: &str| ident(s) || matches!(s, "true" | "false" | "nil");
701    (ident(pkg) && !is_reserved_word(pkg) && named(name)).then_some((pkg, name))
702}
703
704/// One line for a reader on each reserved word: what it writes, and the shape.
705///
706/// Beside the two tables rather than inside them, because both are `&[&str]`
707/// read by the formatter's corpus gate and by highlighters outside this repo;
708/// changing their type would break every one of those readers.
709/// `every_operator_and_keyword_is_described` makes a word without a line here a
710/// red build, so the omission cannot ship. `blue reference` prints these.
711#[must_use]
712pub fn keyword_doc(word: &str) -> &'static str {
713    match word {
714        "if" => "`if c … elsif d … else … end`: an expression; its value is the branch taken, nil when none is. An `else` that is empty or `nil` is dropped (B0035).",
715        "unless" => "`unless c … end` is read as `if not c` and written that way (B0026).",
716        "begin" => "`begin … rescue :kind as e … rescue e … ensure … end`: run the statements; a `rescue` clause catches an error by kind, binding it with `as`, and `rescue e` catches every error; `ensure` always runs. A def body takes the same clauses without `begin`.",
717        "rescue" => "A clause of `begin` or `def`: `rescue :a, :b as e`, `rescue e` (every error, last). Inline, `x rescue fallback` is fallback when x raises; it binds loosest, below `|>`.",
718        "ensure" => "The last clause of `begin` or `def`: statements that run whether the block returned or raised; the block's value or error is kept.",
719        "and" => "`a and b`: logical and; `&&` is read as `and` and written `and` (B0025).",
720        "or" => "`a or b`: logical or; `||` is read as `or` and written `or` (B0025).",
721        "not" => "`not x`: true when x is false or nil; `!x` is read as `not x` and written `not x` (B0025). `not (a == b)` is written `a != b` (B0034).",
722        "def" => "`def name(a, b) … end`: a named function; its value is the last expression. Parameters may be typed: `def f(n: Int) -> Int`.",
723        "defmacro" => "`defmacro name(x) quote … unquote(x) … end end`: a macro, a rewrite of the unevaluated form.",
724        "quote" => "`quote … end`: the form itself as data, not its value. Used inside `defmacro`.",
725        "unquote" => "`unquote(x)`: inside `quote`, splice in the value of `x`.",
726        "unquote_splice" => "`unquote_splice(xs)`: inside `quote`, splice the items of the form `xs` in place, not the form itself.",
727        "test" => "`test \"what must hold\" … end`: a test block, run by `blue test` and ignored by `blue run`.",
728        "assert" => "`assert expr`: inside a test, fail it unless `expr` is truthy; the failure quotes the expression.",
729        "fn" => "`fn(x) x * 2 end`: an anonymous function, a value. Blue has no brace or do blocks.",
730        "case" => "`case v when 1 … when 2 … else … end`: compares with `==`; it does not destructure.",
731        "do" => "Reserved and refused: blue has no do-blocks. Pass `fn(x) … end` instead.",
732        "end" => "Closes `def`, `defmacro`, `if`, `case`, `fn`, `test` and `quote`.",
733        "else" => "The branch of `if` or `case` taken when nothing before it matched.",
734        "elsif" => "A further condition in an `if` chain; an `else` holding only an `if` is written `elsif` (B0027).",
735        "true" => "The boolean true.",
736        "false" => "The boolean false. Only `false` and `nil` are falsy; 0, \"\" and [] are truthy.",
737        "nil" => "The absent value. It is not `[]` (okite D0004), and only `nil == nil`.",
738        _ => "",
739    }
740}
741
742fn infix(op: &str) -> Option<&'static Infix> {
743    INFIX.iter().find(|i| i.op == op)
744}
745
746pub const SYMBOL_SPELLINGS: &[(&str, &str)] = &[("&&", "and"), ("||", "or"), ("!", "not")];
747
748fn word_for(op: &str) -> &str {
749    SYMBOL_SPELLINGS
750        .iter()
751        .find(|(sym, _)| *sym == op)
752        .map_or(op, |(_, word)| word)
753}
754
755fn infix_of(kind: &TokenKind) -> Option<&'static Infix> {
756    match kind {
757        TokenKind::Op(o) => infix(word_for(o)),
758        TokenKind::Ident(w) if w == "and" || w == "or" => infix(w),
759        _ => None,
760    }
761}
762
763const PIPE_POWER: (u8, u8) = (0, 1);
764const RESCUE_POWER: (u8, u8) = (0, 1);
765
766struct Parser {
767    toks: Vec<Token>,
768    pos: usize,
769    /// Current expression-nesting depth, bounded by [`Self::max_depth`].
770    ///
771    /// Without this the parser does not fail on deep input — it **aborts the
772    /// process** with a stack overflow (SIGABRT), which `catch_unwind` cannot
773    /// catch. Measured 2026-08-01: `"(".repeat(2_000)` killed the test runner
774    /// outright. Every consumer inherited it — an LSP parsing a half-typed
775    /// line, a formatter, and shikumi loading a `.b` config off disk.
776    depth: usize,
777    /// The bound [`Self::depth`] is checked against.
778    ///
779    /// Defaults to [`MAX_EXPR_DEPTH`] on every entry point that does not name
780    /// one; `parse_program_with_depth` exists so an operator can raise it
781    /// without recompiling. It is a **bound**, so raising it changes no
782    /// program's meaning — only which pathological inputs are refused.
783    max_depth: usize,
784    src: String,
785    comment_spans: Vec<Span>,
786    notes: Vec<Note>,
787    /// How many `quote` bodies enclose the parser: a string inside one is
788    /// data, so its `\n` is kept as written rather than lowered (B0065).
789    quoting: usize,
790}
791
792/// Maximum expression nesting before the parser refuses.
793///
794/// Chosen well above anything human-written (blue's own `spec/*.b` peaks in
795/// single digits) and far below the measured overflow point, so the bound is
796/// hit as a typed `Err` long before the stack is at risk. A limit that is
797/// merely *near* the crash point is not a safety bound; it is a race.
798pub const MAX_EXPR_DEPTH: usize = 256;
799
800/// The parser's internal error: a [`ParseError`] behind a pointer.
801///
802/// Every production returns a `Result`, and a debug build keeps several per
803/// frame down the recursion, so the error's size is paid at every level of
804/// nesting whether or not anything fails. Measured 2026-09-29 on a 2 MiB
805/// debug thread, parsing `(((…1…)))` with no bound: the unboxed 40-byte error
806/// overflowed between 255 and 260 levels, barely past [`MAX_EXPR_DEPTH`];
807/// adding `line`/`col` unboxed moved it to 240–250, under the bound, and
808/// `depth_beyond_the_limit_is_an_err_not_an_abort` aborted. Boxed, the
809/// overflow is between 300 and 310. The error path is cold; the pointer is
810/// what keeps the bound a bound.
811type Fail = Box<ParseError>;
812
813fn fail(message: impl Into<String>, span: Span) -> Fail {
814    Box::new(ParseError::new(message, span))
815}
816
817impl Parser {
818    fn over(src: &str, all: Vec<Token>, max_depth: usize) -> Self {
819        let comment_spans = all
820            .iter()
821            .filter(|t| matches!(t.kind, TokenKind::Comment(_)))
822            .map(|t| t.span)
823            .collect();
824        let toks = all
825            .into_iter()
826            .filter(|t| !matches!(t.kind, TokenKind::Comment(_)))
827            .collect();
828        Parser {
829            toks,
830            pos: 0,
831            depth: 0,
832            max_depth,
833            src: src.to_string(),
834            comment_spans,
835            notes: Vec::new(),
836            quoting: 0,
837        }
838    }
839
840    fn note(&mut self, spelling: Spelling, span: Span, written: &str, canonical: &str) {
841        self.notes.push(Note {
842            spelling,
843            span,
844            written: written.to_string(),
845            canonical: canonical.to_string(),
846        });
847    }
848
849    fn text(&self, span: Span) -> &str {
850        self.src.get(span.start..span.end).unwrap_or("")
851    }
852
853    fn comment_within(&self, lo: usize, hi: usize) -> bool {
854        self.comment_spans
855            .iter()
856            .any(|c| c.start >= lo && c.start < hi)
857    }
858
859    fn touches_previous(&self) -> bool {
860        let here = self.peek_span();
861        self.toks
862            .get(self.pos.wrapping_sub(1))
863            .is_some_and(|prev| prev.span.end == here.start)
864    }
865
866    fn peek(&self) -> &TokenKind {
867        &self.toks[self.pos.min(self.toks.len() - 1)].kind
868    }
869
870    fn peek_span(&self) -> Span {
871        self.toks[self.pos.min(self.toks.len() - 1)].span
872    }
873
874    fn bump(&mut self) -> TokenKind {
875        let k = self.toks[self.pos.min(self.toks.len() - 1)].kind.clone();
876        if self.pos < self.toks.len() {
877            self.pos += 1;
878        }
879        k
880    }
881
882    fn at(&self, k: &TokenKind) -> bool {
883        self.peek() == k
884    }
885
886    fn eat(&mut self, k: &TokenKind) -> bool {
887        if self.at(k) {
888            self.bump();
889            true
890        } else {
891            false
892        }
893    }
894
895    fn expect(&mut self, k: &TokenKind, what: &str) -> Result<(), Fail> {
896        if self.eat(k) {
897            Ok(())
898        } else {
899            Err(self.error(format!("expected {what}, found {}", self.peek())))
900        }
901    }
902
903    fn error(&self, message: impl Into<String>) -> Fail {
904        fail(message, self.peek_span())
905    }
906
907    /// Skip statement separators (newlines and semicolon-free layout).
908    fn skip_newlines(&mut self) {
909        while matches!(self.peek(), TokenKind::Newline) {
910            self.bump();
911        }
912    }
913
914    fn at_ident(&self, name: &str) -> bool {
915        matches!(self.peek(), TokenKind::Ident(n) if n == name)
916    }
917
918    /// Byte offset where the next token begins — the anchor a production spans
919    /// *from*. Paired with [`Self::span_since`].
920    fn mark(&self) -> usize {
921        self.peek_span().start
922    }
923
924    /// The span from `start` to the end of the **last consumed** token.
925    ///
926    /// `pos` has already advanced past that token, so this looks one back. Call
927    /// it after a production has consumed everything it owns; calling it early
928    /// yields a span that stops short of the node it describes.
929    fn span_since(&self, start: usize) -> Span {
930        let end = self
931            .toks
932            .get(self.pos.saturating_sub(1))
933            .map_or(start, |t| t.span.end);
934        Span::new(start, end.max(start))
935    }
936
937    fn program(&mut self) -> Result<Vec<Spanned>, Fail> {
938        let mut out = Vec::new();
939        loop {
940            self.skip_newlines();
941            if matches!(self.peek(), TokenKind::Eof) {
942                break;
943            }
944            if let TokenKind::Doc { module, text } = self.peek().clone() {
945                out.push(self.documentation(module, text, out.is_empty())?);
946                continue;
947            }
948            out.push(self.statement()?);
949            self.end_of_statement(&[])?;
950        }
951        Ok(out)
952    }
953
954    /// `@moduledoc """…"""` as the first form, or `@doc """…"""` over the
955    /// definition it documents ([`crate::doc`]).
956    fn documentation(&mut self, module: bool, text: String, first: bool) -> Result<Spanned, Fail> {
957        let span = self.peek_span();
958        self.bump();
959        let word = if module { "@moduledoc" } else { "@doc" };
960        let head = sym_at(
961            Span::new(span.start, span.start + word.len()),
962            if module {
963                crate::doc::MODULEDOC_HEAD
964            } else {
965                crate::doc::DOC_HEAD
966            },
967        );
968        let text = crate::doc::text_node(span, text);
969        self.end_of_statement(&[])?;
970        if module {
971            if !first {
972                return Err(fail(
973                    "`@moduledoc` is the first form of a file: it documents the whole file",
974                    span,
975                ));
976            }
977            return Ok(list_at(span, vec![head, text]));
978        }
979        self.skip_newlines();
980        if matches!(self.peek(), TokenKind::Doc { .. } | TokenKind::Eof) {
981            return Err(fail(
982                "`@doc` documents the definition below it, and none follows",
983                span,
984            ));
985        }
986        let def = self.statement()?;
987        self.end_of_statement(&[])?;
988        if !crate::doc::is_definition(&def) {
989            return Err(fail(
990                "`@doc` documents the definition below it: a `def`, a `defmacro` or a \
991                 top-level `name = value`",
992                span,
993            ));
994        }
995        Ok(list_at(
996            Span::new(span.start, def.span.end),
997            vec![head, text, def],
998        ))
999    }
1000
1001    /// **A statement ends at the end of its line.** Refuse anything else.
1002    ///
1003    /// The lexer has always said newlines "are statement separators", and the
1004    /// parser never asked for one: `program` and `body` looped over
1005    /// `statement()` and started the next one wherever the last stopped. So a
1006    /// form blue does not have became two forms it does, silently —
1007    /// `y = xs[0]` parsed as `y = xs` then the list `[0]`, and Ruby's
1008    /// `a = f x` as `a = f` then `x`. Both ran; both bound the wrong value.
1009    ///
1010    /// **Refused rather than given a meaning**, because each would be a second
1011    /// spelling of a tree blue already spells one way (§0's FORM axis):
1012    /// indexing is the call `nth(i, xs)`, and a call has parentheses. `;` is
1013    /// refused by the lexer for the same reason — one separator, the newline.
1014    ///
1015    /// What may follow a statement: a newline, the end of the file, or — in a
1016    /// body — the keyword that closes it, which is what keeps
1017    /// `fn(x) x + 1 end` a one-line lambda.
1018    fn end_of_statement(&self, terminators: &[&str]) -> Result<(), Fail> {
1019        let next = self.peek();
1020        if matches!(next, TokenKind::Newline | TokenKind::Eof)
1021            || terminators.iter().any(|t| self.at_ident(t))
1022        {
1023            return Ok(());
1024        }
1025        let prev = self.toks.get(self.pos.wrapping_sub(1)).map(|t| &t.kind);
1026        let prev_name = match prev {
1027            Some(TokenKind::Ident(n)) => Some(n.as_str()),
1028            _ => None,
1029        };
1030        let callee = prev_name.unwrap_or("f");
1031        let why = match (prev, next) {
1032            (Some(TokenKind::Ident(t)), _) if t == "then" => {
1033                "blue's `if` has no `then`: put the branch on the next line".to_string()
1034            }
1035            (_, TokenKind::Ident(d)) if d == "do" => "blue has no `do` blocks: pass a function \
1036                 as an argument, `f(xs, fn(x) … end)`"
1037                .to_string(),
1038            (_, TokenKind::LBracket) => {
1039                let index = match (self.toks.get(self.pos + 1), self.toks.get(self.pos + 2)) {
1040                    (Some(i), Some(close)) if close.kind == TokenKind::RBracket => match &i.kind {
1041                        TokenKind::Int(n) => n.to_string(),
1042                        TokenKind::Ident(n) => n.clone(),
1043                        _ => "i".to_string(),
1044                    },
1045                    _ => "i".to_string(),
1046                };
1047                let list = prev_name.unwrap_or("xs");
1048                format!(
1049                    "an index touches what it indexes: write `{list}[{index}]`, with no space \
1050                     before `[`"
1051                )
1052            }
1053            (_, TokenKind::LBrace) => "blue has no brace blocks: pass a function as an \
1054                 argument, `map(xs, fn(x) … end)`"
1055                .to_string(),
1056            (
1057                Some(TokenKind::Ident(_)),
1058                TokenKind::Ident(_)
1059                | TokenKind::Int(_)
1060                | TokenKind::Float(_)
1061                | TokenKind::Str(_)
1062                | TokenKind::InterpolatedStr { .. }
1063                | TokenKind::Sym(_)
1064                | TokenKind::Words { .. }
1065                | TokenKind::True
1066                | TokenKind::False
1067                | TokenKind::Nil,
1068            ) => {
1069                let arg = match next {
1070                    TokenKind::Ident(a) => a.clone(),
1071                    _ => "x".to_string(),
1072                };
1073                format!("a call needs parentheses: write `{callee}({arg})`")
1074            }
1075            _ => "put each statement on its own line".to_string(),
1076        };
1077        Err(self.error(format!("expected the end of the line, found {next}: {why}")))
1078    }
1079
1080    /// A statement: either a binding or an expression.
1081    ///
1082    /// `x = 5` lowers to `(define x 5)`. Blue had NO way to name a value — a
1083    /// capability probe found `x = 5` was a parse error, which makes every
1084    /// program a single expression. That is more fundamental than anything else
1085    /// the probe found.
1086    ///
1087    /// Only at STATEMENT position, never inside an expression, so `f(x = 1)` is
1088    /// still an error rather than a silent binding. Ruby allows assignment as an
1089    /// expression and it is a well-known footgun — `if x = 1` where `==` was
1090    /// meant. Blue declines it, and the cost is only that a walrus-style idiom
1091    /// has to be two lines.
1092    fn statement(&mut self) -> Result<Spanned, Fail> {
1093        // The SECOND recursion cycle, and it needs the same guard as `expr`.
1094        //
1095        // Guarding `expr` alone was not enough — measured 2026-08-01: with the
1096        // expression bound in place, `"def a\n".repeat(2_000)` STILL aborted
1097        // the process. Block nesting (`def` opening a body that contains more
1098        // statements) recurses through here, not through `expr`, so a fix
1099        // applied to one cycle silently left the other reachable. Two paths to
1100        // the same crash; one guard covered one of them.
1101        //
1102        // Shares `self.depth` with `expr` on purpose: what the stack cares
1103        // about is TOTAL nesting, not which grammar production produced it, so
1104        // two independent counters would each permit their own full budget and
1105        // together exceed what the stack can hold.
1106        let max = self.max_depth;
1107        if self.depth >= max {
1108            return Err(self.error(format!(
1109                "statement nests deeper than {max}; refusing to \
1110                 recurse further (this is a limit, not a syntax error)"
1111            )));
1112        }
1113        self.depth += 1;
1114        let r = self.statement_inner();
1115        self.depth -= 1;
1116        r
1117    }
1118
1119    fn statement_inner(&mut self) -> Result<Spanned, Fail> {
1120        if let TokenKind::Ident(name) = self.peek().clone() {
1121            if self.peek_at(1) == "=" && !is_reserved_word(&name) {
1122                let name_span = self.peek_span();
1123                self.bump(); // name
1124                self.bump(); // =
1125                self.skip_newlines();
1126                let value = self.expr(0)?;
1127                // `define` is synthesized — `x = 5` contains no such word — so
1128                // it takes the span of the name being bound, which is what a
1129                // reader looks at when told something about the binding.
1130                return Ok(list_at(
1131                    self.span_since(name_span.start),
1132                    vec![sym_at(name_span, "define"), sym_at(name_span, &name), value],
1133                ));
1134            }
1135        }
1136        let e = self.expr(0)?;
1137        if let SpannedForm::List(items) = &e.form {
1138            if items.len() == 3
1139                && items[0].as_symbol() == Some("define")
1140                && items[1].as_symbol().is_some()
1141                && self.text(items[0].span) == "define"
1142            {
1143                self.note(Spelling::OperatorSpelling, e.span, "define(x, v)", "x = v");
1144            }
1145        }
1146        Ok(e)
1147    }
1148
1149    /// The token `n` positions ahead, for the two-token lookahead a binding
1150    /// needs. Returns `Eof` past the end rather than panicking.
1151    fn peek_at(&self, n: usize) -> String {
1152        match self.toks.get(self.pos + n).map(|t| &t.kind) {
1153            Some(TokenKind::Op(o)) => o.clone(),
1154            _ => String::new(),
1155        }
1156    }
1157
1158    /// Pratt loop.
1159    fn expr(&mut self, min_bp: u8) -> Result<Spanned, Fail> {
1160        // Depth guard at the single recursion cycle (`expr` -> `prefix` ->
1161        // `expr`). Returning an Err here converts an UNRECOVERABLE abort into
1162        // an ordinary parse failure a caller can render — the difference
1163        // between an LSP showing a squiggle and an LSP being gone.
1164        //
1165        // The decrement is deliberately not RAII: every exit from this
1166        // function is via `?` or a normal return, and both are covered by the
1167        // explicit decrements below. A guard object would be tidier but would
1168        // also hide the invariant this comment is here to state.
1169        let max = self.max_depth;
1170        if self.depth >= max {
1171            return Err(self.error(format!(
1172                "expression nests deeper than {max}; refusing to \
1173                 recurse further (this is a limit, not a syntax error)"
1174            )));
1175        }
1176        self.depth += 1;
1177        let r = self.expr_inner(min_bp);
1178        self.depth -= 1;
1179        r
1180    }
1181
1182    #[inline(never)]
1183    fn rescue_node(&self, start: usize, op_span: Span, lhs: Spanned, fallback: Spanned) -> Spanned {
1184        list_at(
1185            self.span_since(start),
1186            vec![
1187                sym_at(op_span, "try"),
1188                lhs,
1189                list_at(
1190                    op_span.merge(fallback.span),
1191                    vec![
1192                        sym_at(op_span, "catch"),
1193                        list_at(op_span, vec![sym_at(op_span, "_")]),
1194                        fallback,
1195                    ],
1196                ),
1197            ],
1198        )
1199    }
1200
1201    #[inline(never)]
1202    fn infix_spelling(&mut self, callee: Option<&str>, op_span: Span) {
1203        let written = self.text(op_span).to_string();
1204        match (callee, self.peek().clone()) {
1205            (None, _) => self.note(Spelling::CallForm, op_span, "|>", "a call"),
1206            (Some(c), TokenKind::Op(o)) if word_for(&o) != o => {
1207                self.note(Spelling::LogicalSymbol, op_span, &written, c);
1208            }
1209            (Some(_), TokenKind::Op(o)) if written != o => {
1210                self.note(Spelling::OperatorSpelling, op_span, &written, &o);
1211            }
1212            _ => {}
1213        }
1214    }
1215
1216    #[inline(never)]
1217    fn nil_test(
1218        &mut self,
1219        start: usize,
1220        op_span: Span,
1221        lhs: &Spanned,
1222        rhs: &Spanned,
1223    ) -> Option<Spanned> {
1224        let (subject, written) = match (&lhs.form, &rhs.form) {
1225            (_, SpannedForm::Nil) if !matches!(lhs.form, SpannedForm::Nil) => (lhs, "x == nil"),
1226            (SpannedForm::Nil, _) if !matches!(rhs.form, SpannedForm::Nil) => (rhs, "nil == x"),
1227            _ => return None,
1228        };
1229        let span = self.span_since(start);
1230        self.note(Spelling::NilTest, span, written, "x.nil?");
1231        Some(list_at(
1232            span,
1233            vec![
1234                sym_at(op_span, LOWERED_FIELD),
1235                subject.clone(),
1236                atom_at(op_span, Atom::Keyword("nil?".to_string())),
1237            ],
1238        ))
1239    }
1240
1241    #[inline(never)]
1242    fn infix_node(
1243        &mut self,
1244        start: usize,
1245        callee: Option<&str>,
1246        op_span: Span,
1247        lhs: Spanned,
1248        rhs: Spanned,
1249    ) -> Spanned {
1250        if callee == Some("-") && matches!(lhs.form, SpannedForm::Atom(Atom::Int(0))) {
1251            self.note(
1252                Spelling::OperatorSpelling,
1253                lhs.span.merge(op_span),
1254                "0 - x",
1255                "-x",
1256            );
1257        }
1258
1259        if callee == Some("equal?") {
1260            if let Some(test) = self.nil_test(start, op_span, &lhs, &rhs) {
1261                return test;
1262            }
1263        }
1264
1265        if callee.is_none() {
1266            // `x |> f`      => (f x)
1267            // `x |> f(a)`   => (f x a)   — the pipeline threads into
1268            //                  the FIRST argument position, as Elixir's
1269            //                  does; that is what makes it composable.
1270            let rhs_span = rhs.span;
1271            let threaded = match rhs.form {
1272                SpannedForm::List(mut items) if !items.is_empty() => {
1273                    items.insert(1, lhs);
1274                    list_at(self.span_since(start), items)
1275                }
1276                callee => list_at(
1277                    self.span_since(start),
1278                    vec![Spanned::new(rhs_span, callee), lhs],
1279                ),
1280            };
1281            match &threaded.form {
1282                SpannedForm::List(items)
1283                    if items.len() == 2 && items[0].as_symbol() == Some("not") =>
1284                {
1285                    let (head, operand) = (items[0].span, items[1].clone());
1286                    self.negation(head, threaded.span, operand)
1287                }
1288                SpannedForm::List(items)
1289                    if items.len() == 4
1290                        && items[0].as_symbol() == Some("if")
1291                        && matches!(items[3].form, SpannedForm::Nil) =>
1292                {
1293                    list_at(threaded.span, items[..3].to_vec())
1294                }
1295                SpannedForm::List(items)
1296                    if items.len() == 3 && items[0].as_symbol() == Some("equal?") =>
1297                {
1298                    let (op, a, b) = (items[0].span, items[1].clone(), items[2].clone());
1299                    match self.nil_test(start, op, &a, &b) {
1300                        Some(test) => test,
1301                        None => threaded,
1302                    }
1303                }
1304                _ => threaded,
1305            }
1306        } else {
1307            // The callee symbol takes the OPERATOR's span, not the whole
1308            // expression's: `a + b` lowers to `(+ a b)` and the `+` node is
1309            // the one byte-range in the source that means it. Handing it the
1310            // whole expression's span would make a diagnostic about the
1311            // operator underline both operands too.
1312            list_at(
1313                self.span_since(start),
1314                vec![sym_at(op_span, callee.unwrap_or_default()), lhs, rhs],
1315            )
1316        }
1317    }
1318
1319    fn expr_inner(&mut self, min_bp: u8) -> Result<Spanned, Fail> {
1320        let start = self.mark();
1321        let mut lhs = self.prefix()?;
1322
1323        loop {
1324            // Postfix: `.name`, `.name(args)`, `(args)`
1325            match self.peek() {
1326                TokenKind::Dot => {
1327                    let dot = self.peek_span();
1328                    self.bump();
1329                    lhs = self.finish_send(start, lhs, dot)?;
1330                    continue;
1331                }
1332                TokenKind::LParen | TokenKind::LBracket
1333                    if self.at(&TokenKind::LParen) || self.touches_previous() =>
1334                {
1335                    lhs = self.applied(start, lhs)?;
1336                    continue;
1337                }
1338                _ => {}
1339            }
1340
1341            // Infix
1342            // `callee == None` marks the pipeline, which is a rewrite rather
1343            // than a call.
1344            let op_span = self.peek_span();
1345            if self.at_ident("rescue") {
1346                if RESCUE_POWER.0 < min_bp {
1347                    break;
1348                }
1349                self.bump();
1350                self.skip_newlines();
1351                let fallback = self.expr(RESCUE_POWER.1)?;
1352                lhs = self.rescue_node(start, op_span, lhs, fallback);
1353                continue;
1354            }
1355            let (callee, (lbp, rbp)) = match self.peek() {
1356                TokenKind::Pipe => (None, PIPE_POWER),
1357                other => match infix_of(other) {
1358                    Some(i) => (Some(i.callee), i.power),
1359                    None => break,
1360                },
1361            };
1362            if lbp < min_bp {
1363                break;
1364            }
1365            self.infix_spelling(callee, op_span);
1366            self.bump();
1367            self.skip_newlines();
1368            let rhs = self.expr(rbp)?;
1369            lhs = self.infix_node(start, callee, op_span, lhs, rhs);
1370        }
1371
1372        Ok(lhs)
1373    }
1374
1375    #[inline(never)]
1376    fn interpolation(span: Span, parts: &[String], exprs: &[String]) -> Result<Spanned, Fail> {
1377        // Every node here carries the WHOLE string literal's span.
1378        //
1379        // The inner expression is parsed from `raw` — a substring — so
1380        // its own offsets index that substring, not `src`. Carrying them
1381        // through would produce spans that look real and point into a
1382        // buffer nobody has: for `"a#{x}"` at offset 40, `x`'s span
1383        // would be 0..1, i.e. the start of the file. The lexer does not
1384        // record where each part sat in the outer source, so the string
1385        // literal is the most precise honest answer available.
1386        let mut acc = atom_at(span, Atom::Str(parts[0].clone()));
1387        for (i, raw) in exprs.iter().enumerate() {
1388            let inner = parse_expr(raw).map_err(|e| {
1389                fail(
1390                    format!("in interpolation `#{{{raw}}}`: {}", e.message),
1391                    span,
1392                )
1393            })?;
1394            let inner = Spanned::from_sexp_at(&inner, span);
1395            acc = list_at(span, vec![sym_at(span, LOWERED_CONCAT), acc, inner]);
1396            // `parts.len() == exprs.len() + 1` by construction, so this
1397            // index is always in range.
1398            acc = list_at(
1399                span,
1400                vec![
1401                    sym_at(span, LOWERED_CONCAT),
1402                    acc,
1403                    atom_at(span, Atom::Str(parts[i + 1].clone())),
1404                ],
1405            );
1406        }
1407        Ok(acc)
1408    }
1409
1410    /// A string literal holding a `\n` is read as the lines it spells
1411    /// (B0065): `"a\nb"` is `join_lines(["a", "b"])`, text ending in a newline
1412    /// is `unlines([…])` without the empty last line, and `"\n"` alone is
1413    /// `line_break`. A line that is one interpolation, `#{x}`, is `x` itself:
1414    /// `join_lines` renders each element as interpolation does. So the escape
1415    /// and the call are one tree, and `blue fmt` writes the call.
1416    #[inline(never)]
1417    fn lines_literal(
1418        &mut self,
1419        span: Span,
1420        parts: Vec<String>,
1421        exprs: Vec<String>,
1422    ) -> Result<Spanned, Fail> {
1423        let mut lines: Vec<(Vec<String>, Vec<String>)> = vec![(vec![String::new()], Vec::new())];
1424        for (i, part) in parts.iter().enumerate() {
1425            let mut pieces = part.split('\n');
1426            if let Some(head) = pieces.next() {
1427                if let Some(last) = lines.last_mut().and_then(|l| l.0.last_mut()) {
1428                    last.push_str(head);
1429                }
1430            }
1431            for piece in pieces {
1432                lines.push((vec![piece.to_string()], Vec::new()));
1433            }
1434            if let Some(raw) = exprs.get(i) {
1435                if let Some(line) = lines.last_mut() {
1436                    line.1.push(raw.clone());
1437                    line.0.push(String::new());
1438                }
1439            }
1440        }
1441        let empty = |l: &(Vec<String>, Vec<String>)| l.1.is_empty() && l.0 == [""];
1442        if lines.len() == 2 && lines.iter().all(empty) {
1443            self.note(Spelling::NewlineEscape, span, "\"\\n\"", "line_break");
1444            return Ok(sym_at(span, "line_break"));
1445        }
1446        let trailing = lines.last().is_some_and(empty);
1447        if trailing {
1448            lines.pop();
1449        }
1450        let head = if trailing { "unlines" } else { "join_lines" };
1451        let mut items = vec![sym_at(span, "list")];
1452        for (lits, raws) in &lines {
1453            let item = if raws.is_empty() {
1454                atom_at(span, Atom::Str(lits[0].clone()))
1455            } else if raws.len() == 1 && lits.iter().all(String::is_empty) {
1456                let inner = parse_expr(&raws[0]).map_err(|e| {
1457                    fail(
1458                        format!("in interpolation `#{{{}}}`: {}", raws[0], e.message),
1459                        span,
1460                    )
1461                })?;
1462                Spanned::from_sexp_at(&inner, span)
1463            } else {
1464                Self::interpolation(span, lits, raws)?
1465            };
1466            items.push(item);
1467        }
1468        self.note(
1469            Spelling::NewlineEscape,
1470            span,
1471            "\"…\\n…\"",
1472            &format!("{head}([…])"),
1473        );
1474        Ok(list_at(
1475            span,
1476            vec![sym_at(span, head), list_at(span, items)],
1477        ))
1478    }
1479
1480    /// `f(args)` or `x[k]` after `lhs`. One out-of-line step for both, so
1481    /// `expr_inner`, whose frame the depth bound is measured against, holds
1482    /// one result slot for them rather than one per form.
1483    #[inline(never)]
1484    fn applied(&mut self, start: usize, lhs: Spanned) -> Result<Spanned, Fail> {
1485        if self.at(&TokenKind::LParen) {
1486            let (args, splat) = self.call_args()?;
1487            let call = self.call_node(self.span_since(start), vec![lhs], args, splat);
1488            Ok(self.call_spelling(call))
1489        } else {
1490            self.index(start, lhs)
1491        }
1492    }
1493
1494    /// `x[k]`: `get(x, k)` for a keyword or string literal key, else
1495    /// `nth(k, x)` (B0066).
1496    #[inline(never)]
1497    fn index(&mut self, start: usize, object: Spanned) -> Result<Spanned, Fail> {
1498        let open = self.peek_span();
1499        self.bump();
1500        self.skip_newlines();
1501        let key = self.expr(0)?;
1502        self.skip_newlines();
1503        self.expect(&TokenKind::RBracket, "`]` closing the index")?;
1504        let span = self.span_since(start);
1505        Ok(if is_index_key(&key) {
1506            list_at(span, vec![sym_at(open, "get"), object, key])
1507        } else {
1508            list_at(span, vec![sym_at(open, "nth"), key, object])
1509        })
1510    }
1511
1512    #[inline(never)]
1513    fn not_prefix(&mut self, span: Span) -> Result<Spanned, Fail> {
1514        if self.at(&TokenKind::LParen) && self.touches_previous() {
1515            self.note(Spelling::OperatorSpelling, span, "not(x)", "not x");
1516            let (args, splat) = self.call_args()?;
1517            if args.len() != 1 || splat.is_some() {
1518                let call = self.call_node(
1519                    self.span_since(span.start),
1520                    vec![sym_at(span, "not")],
1521                    args,
1522                    splat,
1523                );
1524                return Ok(call);
1525            }
1526            let operand = args.into_iter().next().expect("one argument");
1527            return Ok(self.negation(span, self.span_since(span.start), operand));
1528        }
1529        let rhs = self.expr(11)?;
1530        Ok(self.negation(span, self.span_since(span.start), rhs))
1531    }
1532
1533    fn prefix(&mut self) -> Result<Spanned, Fail> {
1534        if self.at(&TokenKind::LParen) {
1535            let span = self.peek_span();
1536            self.bump();
1537            if self.arrow_lambda_ahead() {
1538                return self.arrow_lambda(span);
1539            }
1540            return self.group();
1541        }
1542        self.prefix_token()
1543    }
1544
1545    fn group(&mut self) -> Result<Spanned, Fail> {
1546        self.skip_newlines();
1547        let inner = self.expr(0)?;
1548        self.skip_newlines();
1549        self.expect(&TokenKind::RParen, "`)`")?;
1550        Ok(inner)
1551    }
1552
1553    #[inline(never)]
1554    fn prefix_token(&mut self) -> Result<Spanned, Fail> {
1555        let span = self.peek_span();
1556        match self.bump() {
1557            TokenKind::Int(v) => Ok(atom_at(span, Atom::Int(v))),
1558            TokenKind::Float(v) => Ok(atom_at(span, Atom::Float(v))),
1559            TokenKind::Str(s) if self.quoting == 0 && s.contains('\n') => {
1560                self.lines_literal(span, vec![s], Vec::new())
1561            }
1562            TokenKind::Str(s) => Ok(atom_at(span, Atom::Str(s))),
1563            TokenKind::Doc { module, .. } => Err(fail(
1564                format!(
1565                    "`{}` sits at the top level, above the definition it documents, never \
1566                     inside a form",
1567                    if module { "@moduledoc" } else { "@doc" }
1568                ),
1569                span,
1570            )),
1571
1572            // `"a#{x}b"` → `(concat (concat "a" x) "b")`.
1573            //
1574            // Lowered to `concat`, not to `+`: blue's `+` is arithmetic (see
1575            // the INFIX table), and interpolation must render a value of ANY
1576            // type — `concat` goes through `to_s`, which is what makes
1577            // `"n=#{42}"` work.
1578            //
1579            // The expression source is parsed HERE with the ordinary parser
1580            // rather than lexed inside the string, so an interpolation can hold
1581            // anything an expression can and the two can never drift.
1582            TokenKind::InterpolatedStr { parts, exprs }
1583                if self.quoting == 0 && parts.iter().any(|p| p.contains('\n')) =>
1584            {
1585                self.lines_literal(span, parts, exprs)
1586            }
1587            TokenKind::InterpolatedStr { parts, exprs } => Self::interpolation(span, &parts, &exprs),
1588            TokenKind::Sym(s) => Ok(atom_at(span, Atom::Keyword(s))),
1589            TokenKind::Words { flavor, words } => {
1590                let open = Span::new(span.start, span.start + 3);
1591                let mut items = vec![sym_at(open, "list")];
1592                items.extend(words.into_iter().map(|(w, at)| {
1593                    atom_at(
1594                        at,
1595                        match flavor {
1596                            crate::words::Flavor::Strings => Atom::Str(w),
1597                            crate::words::Flavor::Keywords => Atom::Keyword(w),
1598                        },
1599                    )
1600                }));
1601                Ok(list_at(span, items))
1602            }
1603            TokenKind::True => Ok(atom_at(span, Atom::Bool(true))),
1604            TokenKind::False => Ok(atom_at(span, Atom::Bool(false))),
1605            TokenKind::Nil => Ok(Spanned::new(span, SpannedForm::Nil)),
1606
1607            TokenKind::Op(o) if o == "-" => {
1608                let rhs = self.expr(11)?; // binds tighter than `*`
1609                Ok(list_at(
1610                    self.span_since(span.start),
1611                    vec![sym_at(span, "-"), atom_at(span, Atom::Int(0)), rhs],
1612                ))
1613            }
1614            TokenKind::Op(o) if o == "!" => {
1615                let written = self.text(span).to_string();
1616                self.note(Spelling::LogicalSymbol, span, &written, "not");
1617                let rhs = self.expr(11)?;
1618                Ok(self.negation(span, self.span_since(span.start), rhs))
1619            }
1620            TokenKind::Ident(w) if w == "not" => self.not_prefix(span),
1621            TokenKind::Ident(w) if (w == "and" || w == "or") && self.at(&TokenKind::LParen) => {
1622                Ok(sym_at(span, &w))
1623            }
1624            TokenKind::Ident(w) if w == "and" || w == "or" => Err(fail(
1625                format!("`{w}` joins two expressions: `a {w} b`"),
1626                span,
1627            )),
1628
1629            TokenKind::LParen => {
1630                self.skip_newlines();
1631                let inner = self.expr(0)?;
1632                self.skip_newlines();
1633                self.expect(&TokenKind::RParen, "`)`")?;
1634                // The inner expression's own span, NOT one widened to include
1635                // the parentheses: grouping is not part of the expression's
1636                // meaning, and `(a + b)` should underline `a + b`.
1637                Ok(inner)
1638            }
1639
1640            TokenKind::LBracket => self.list_literal(span),
1641            TokenKind::LBrace => self.map_literal(span),
1642
1643            TokenKind::Ident(name) if self.at(&TokenKind::PathSep) => {
1644                self.qualified_name(span, &name)
1645            }
1646            TokenKind::PathSep => Err(fail(
1647                "a qualified name is `package::name`, with the package written before `::`",
1648                span,
1649            )),
1650            TokenKind::Ident(name) => match name.as_str() {
1651                "if" => self.if_form(span, false),
1652                "begin" if !(self.at(&TokenKind::LParen) && self.touches_previous()) => {
1653                    self.begin_form(span)
1654                }
1655                "rescue" | "ensure" => Err(fail(
1656                    format!("`{name}` belongs to a `begin … end` block or a `def` body, after the statements it protects"),
1657                    span,
1658                )),
1659                "unless" => {
1660                    self.note(Spelling::Unless, span, "unless", "if not");
1661                    self.if_form(span, true)
1662                }
1663                "def" => self.def_form(span),
1664                "defmacro" => self.defmacro_form(span),
1665                "case" => self.case_form(span),
1666                "fn" => self.lambda_form(span),
1667                "test" => self.test_form(span),
1668                "assert" => self.assert_form(span),
1669                "quote" => self.quote_form(span),
1670                "unquote" => self.unquote_form(span, false),
1671                "unquote_splice" => self.unquote_form(span, true),
1672                "do" => Err(fail(
1673                    "blue has no `do` blocks: pass a function as an argument, \
1674                     `f(xs, fn(x) … end)`",
1675                    span,
1676                )),
1677                "end" => Err(fail(
1678                    "unexpected `end`: nothing open here for it to close",
1679                    span,
1680                )),
1681                _ => Ok(sym_at(span, &name)),
1682            },
1683
1684            other => Err(fail(format!("expected an expression, found {other}"), span)),
1685        }
1686    }
1687
1688    /// After `package`, at `::`: `retsu::first` lowers to the ONE symbol
1689    /// `retsu/first` — tatara's `require` shape, so a qualified name means the
1690    /// same thing in both lisps.
1691    ///
1692    /// After `::` any identifier is a name, reserved words included:
1693    /// `kueri::if` names kueri's definition, never the `if` form, because a
1694    /// reserved word is reserved only where it could start a form. The
1695    /// package side is never a reserved word, and a name has exactly one
1696    /// qualifier (`a::b::c` is refused), since a package is one level deep.
1697    fn qualified_name(&mut self, pkg_span: Span, pkg: &str) -> Result<Spanned, Fail> {
1698        if is_reserved_word(pkg) {
1699            return Err(fail(
1700                format!("`{pkg}` is a reserved word and cannot name a package"),
1701                pkg_span,
1702            ));
1703        }
1704        self.bump(); // ::
1705        let name_span = self.peek_span();
1706        let name = match self.bump() {
1707            TokenKind::Ident(n) => n,
1708            // `true`, `false` and `nil` lex as their own tokens.
1709            TokenKind::True => "true".to_string(),
1710            TokenKind::False => "false".to_string(),
1711            TokenKind::Nil => "nil".to_string(),
1712            other => {
1713                return Err(fail(
1714                    format!("expected a name after `{pkg}::`, found {other}"),
1715                    name_span,
1716                ))
1717            }
1718        };
1719        if self.at(&TokenKind::PathSep) {
1720            return Err(self.error(format!(
1721                "a qualified name has one qualifier: `package::name`, and `{pkg}::{name}::…` has two"
1722            )));
1723        }
1724        Ok(sym_at(
1725            self.span_since(pkg_span.start),
1726            &qualify(pkg, &name),
1727        ))
1728    }
1729
1730    /// After a `.`: `recv.name` or `recv.name(args)`.
1731    ///
1732    /// **A bare `recv.name` is a SEND, not a field read.** Blue commits to
1733    /// the uniform access principle here: a structure exposes no public
1734    /// fields, so a field can later become a computed method without
1735    /// breaking a caller.
1736    #[inline(never)]
1737    fn negation(&mut self, head: Span, whole: Span, operand: Spanned) -> Spanned {
1738        if let SpannedForm::List(items) = &operand.form {
1739            let flipped = match items.first().and_then(Spanned::as_symbol) {
1740                Some("equal?") => Some("not="),
1741                Some("not=") => Some("equal?"),
1742                _ => None,
1743            };
1744            if let (Some(to), 3) = (flipped, items.len()) {
1745                let (written, canonical) = if to == "not=" {
1746                    ("not (a == b)", "a != b")
1747                } else {
1748                    ("not (a != b)", "a == b")
1749                };
1750                self.note(Spelling::NegatedComparison, whole, written, canonical);
1751                let mut out = items.clone();
1752                out[0] = sym_at(items[0].span, to);
1753                if to == "equal?" {
1754                    if let Some(test) =
1755                        self.nil_test(whole.start, items[0].span, &items[1], &items[2])
1756                    {
1757                        return test;
1758                    }
1759                }
1760                return list_at(whole, out);
1761            }
1762        }
1763        list_at(whole, vec![sym_at(head, "not"), operand])
1764    }
1765
1766    #[inline(never)]
1767    fn call_spelling(&mut self, call: Spanned) -> Spanned {
1768        let SpannedForm::List(items) = &call.form else {
1769            return call;
1770        };
1771        let Some(head) = items.first().and_then(Spanned::as_symbol) else {
1772            return call;
1773        };
1774        let argc = items.len() - 1;
1775        if head == "get" && argc == 2 && is_index_key(&items[2]) {
1776            self.note(Spelling::Index, call.span, "get(m, :k)", "m[:k]");
1777            return call;
1778        }
1779        if head == "nth" && argc == 2 && !is_index_key(&items[1]) {
1780            self.note(Spelling::Index, call.span, "nth(i, xs)", "xs[i]");
1781            return call;
1782        }
1783        if head == "equal?" && argc == 2 {
1784            let (op_span, start) = (items[0].span, call.span.start);
1785            if let Some(test) = self.nil_test(start, op_span, &items[1], &items[2]) {
1786                return test;
1787            }
1788        }
1789        let operator = match (head, argc) {
1790            ("and" | "or" | "mod" | "equal?", 2) => Some(head),
1791            ("list", _) => Some("list"),
1792            _ => None,
1793        };
1794        if let Some(op) = operator {
1795            let canonical = match op {
1796                "list" => "[…]".to_string(),
1797                "mod" => "a % b".to_string(),
1798                "equal?" => "a == b".to_string(),
1799                other => format!("a {other} b"),
1800            };
1801            self.note(
1802                Spelling::OperatorSpelling,
1803                call.span,
1804                &format!("{head}(…)"),
1805                &canonical,
1806            );
1807            return call;
1808        }
1809        if head == "apply"
1810            && self.text(items[0].span) == "apply"
1811            && argc >= 2
1812            && items[1].as_symbol().is_some_and(is_callable_name)
1813        {
1814            self.note(Spelling::Apply, call.span, "apply(f, …, xs)", "f(…, *xs)");
1815            return call;
1816        }
1817        if head == "try" {
1818            return self.try_spelling(call);
1819        }
1820        call
1821    }
1822
1823    fn try_spelling(&mut self, call: Spanned) -> Spanned {
1824        let SpannedForm::List(items) = &call.form else {
1825            return call;
1826        };
1827        let Some((catch, body)) = items[1..].split_last() else {
1828            return call;
1829        };
1830        let Some(clause) = catch.as_list() else {
1831            return call;
1832        };
1833        let binder = match clause {
1834            [head, binding, handler @ ..]
1835                if head.as_symbol() == Some("catch") && !handler.is_empty() =>
1836            {
1837                match binding.as_list() {
1838                    Some([b]) => b.as_symbol().map(|n| (n.to_string(), b.span)),
1839                    _ => None,
1840                }
1841            }
1842            _ => None,
1843        };
1844        let Some((name, binder_span)) = binder else {
1845            return call;
1846        };
1847        if body.is_empty() {
1848            return call;
1849        }
1850        self.note(Spelling::TryCatch, call.span, "try(…, catch(…))", "rescue");
1851        let handler = &clause[2..];
1852        let rename =
1853            name.starts_with('_') && name != "_" && !handler.iter().any(|h| mentions(h, &name));
1854        let body = if body.len() == 1 {
1855            body[0].clone()
1856        } else {
1857            let span = cover(body, call.span);
1858            let mut list = vec![sym_at(Span::new(span.start, span.start), "begin")];
1859            list.extend(body.iter().cloned());
1860            list_at(span, list)
1861        };
1862        let handler = if handler.len() == 1 {
1863            handler[0].clone()
1864        } else {
1865            let span = cover(handler, catch.span);
1866            let mut list = vec![sym_at(Span::new(span.start, span.start), "begin")];
1867            list.extend(handler.iter().cloned());
1868            list_at(span, list)
1869        };
1870        let binder = sym_at(binder_span, if rename { "_" } else { &name });
1871        list_at(
1872            call.span,
1873            vec![
1874                items[0].clone(),
1875                body,
1876                list_at(
1877                    catch.span,
1878                    vec![
1879                        clause[0].clone(),
1880                        list_at(clause[1].span, vec![binder]),
1881                        handler,
1882                    ],
1883                ),
1884            ],
1885        )
1886    }
1887
1888    fn finish_send(&mut self, start: usize, recv: Spanned, dot: Span) -> Result<Spanned, Fail> {
1889        let name_span = self.peek_span();
1890        let name = match self.bump() {
1891            // A RESERVED WORD is not a method name.
1892            //
1893            // Found by the formatter property suite, minimal input `1.def`.
1894            // The send parsed happily into `(def 1)` — `def` is just an
1895            // identifier to the lexer — and the formatter then rendered that
1896            // as `def(1)`, which the parser rejects. So `format` turned valid
1897            // source into source that does not parse: corruption, not a style
1898            // choice, and silent until a round-trip property looked.
1899            //
1900            // Rejecting here rather than teaching the formatter to quote it
1901            // fixes the class instead of the symptom: `x.if`, `x.end` and
1902            // `x.case` all lower onto special forms the same way, and none of
1903            // them is a method anyone meant to call.
1904            TokenKind::Ident(n) if is_reserved_word(&n) => {
1905                return Err(self.error(format!(
1906                    "`{n}` is a reserved word and cannot be a method name — \
1907                     `recv.{n}` would lower onto the `{n}` form itself"
1908                )))
1909            }
1910            TokenKind::Ident(n) => n,
1911            other => {
1912                return Err(fail(
1913                    format!("expected a method name after `.`, found {other}"),
1914                    name_span,
1915                ))
1916            }
1917        };
1918        if !self.at(&TokenKind::LParen) {
1919            return Ok(list_at(
1920                self.span_since(start),
1921                vec![
1922                    sym_at(dot, LOWERED_FIELD),
1923                    recv,
1924                    atom_at(name_span, Atom::Keyword(name)),
1925                ],
1926            ));
1927        }
1928        self.note(
1929            Spelling::CallForm,
1930            dot.merge(name_span),
1931            &format!("x.{name}(…)"),
1932            &format!("{name}(x, …)"),
1933        );
1934        let (args, splat) = self.call_args()?;
1935        Ok(self.call_node(
1936            self.span_since(start),
1937            vec![sym_at(name_span, &name), recv],
1938            args,
1939            splat,
1940        ))
1941    }
1942
1943    fn call_node(
1944        &self,
1945        span: Span,
1946        head: Vec<Spanned>,
1947        args: Vec<Spanned>,
1948        splat: Option<Span>,
1949    ) -> Spanned {
1950        let mut list = Vec::with_capacity(head.len() + args.len() + 1);
1951        if let Some(at) = splat {
1952            list.push(sym_at(at, "apply"));
1953        }
1954        list.extend(head);
1955        list.extend(args);
1956        list_at(span, list)
1957    }
1958
1959    fn call_args(&mut self) -> Result<(Vec<Spanned>, Option<Span>), Fail> {
1960        self.expect(&TokenKind::LParen, "`(`")?;
1961        let mut args = Vec::new();
1962        let mut splat = None;
1963        self.skip_newlines();
1964        if self.eat(&TokenKind::RParen) {
1965            return Ok((args, splat));
1966        }
1967        loop {
1968            self.skip_newlines();
1969            if splat.is_some() {
1970                return Err(
1971                    self.error("a splat `*xs` passes a list as the LAST arguments: write it last")
1972                );
1973            }
1974            if self.at_spread() || matches!(self.peek(), TokenKind::Label(_)) {
1975                let open = self.peek_span();
1976                let (segments, spread) = self.map_entries(open, &TokenKind::RParen)?;
1977                if !spread {
1978                    return Err(fail(
1979                        "a keyword entry in a call needs a `**` spread: pass a map, f({a: 1})",
1980                        open,
1981                    ));
1982                }
1983                args.push(self.merged(open, LOWERED_KWARGS, segments));
1984                return Ok((args, splat));
1985            }
1986            if matches!(self.peek(), TokenKind::Op(o) if o == "*") {
1987                splat = Some(self.peek_span());
1988                self.bump();
1989            }
1990            args.push(self.expr(0)?);
1991            self.skip_newlines();
1992            if self.eat(&TokenKind::Comma) {
1993                if self.trailing_comma(&TokenKind::RParen) {
1994                    break;
1995                }
1996                continue;
1997            }
1998            self.expect(&TokenKind::RParen, "`,` or `)`")?;
1999            break;
2000        }
2001        Ok((args, splat))
2002    }
2003
2004    fn trailing_comma(&mut self, closer: &TokenKind) -> bool {
2005        let comma = self
2006            .toks
2007            .get(self.pos.wrapping_sub(1))
2008            .map_or(self.peek_span(), |t| t.span);
2009        self.skip_newlines();
2010        if self.at(closer) {
2011            self.note(Spelling::TrailingComma, comma, ",", "");
2012            self.bump();
2013            return true;
2014        }
2015        false
2016    }
2017
2018    fn list_literal(&mut self, open: Span) -> Result<Spanned, Fail> {
2019        let mut items = vec![sym_at(open, "list")];
2020        self.skip_newlines();
2021        if self.eat(&TokenKind::RBracket) {
2022            return Ok(list_at(self.span_since(open.start), items));
2023        }
2024        loop {
2025            self.skip_newlines();
2026            items.push(self.expr(0)?);
2027            self.skip_newlines();
2028            if self.eat(&TokenKind::Comma) {
2029                if self.trailing_comma(&TokenKind::RBracket) {
2030                    break;
2031                }
2032                continue;
2033            }
2034            self.expect(&TokenKind::RBracket, "`,` or `]`")?;
2035            break;
2036        }
2037        Ok(list_at(self.span_since(open.start), items))
2038    }
2039
2040    /// `{a: 1, "k" => v}` — both spellings, one tree.
2041    ///
2042    /// This is §V.13's rendering law at the parser: `a: 1` and `:a => 1`
2043    /// produce the *same* s-expression, which is precisely why the
2044    /// formatter may always choose the shorthand. The rocket survives only
2045    /// where the key is not a plain symbol.
2046    fn map_literal(&mut self, open: Span) -> Result<Spanned, Fail> {
2047        self.skip_newlines();
2048        if self.eat(&TokenKind::RBrace) {
2049            return Ok(list_at(
2050                self.span_since(open.start),
2051                vec![sym_at(open, LOWERED_MAP)],
2052            ));
2053        }
2054        let (mut segments, spread) = self.map_entries(open, &TokenKind::RBrace)?;
2055        if !spread {
2056            return Ok(segments.remove(0));
2057        }
2058        Ok(self.merged(open, LOWERED_MERGE, segments))
2059    }
2060
2061    fn merged(&self, open: Span, head: &str, segments: Vec<Spanned>) -> Spanned {
2062        let mut items = vec![sym_at(open, head)];
2063        items.extend(segments);
2064        list_at(self.span_since(open.start), items)
2065    }
2066
2067    fn at_spread(&self) -> bool {
2068        matches!(self.peek(), TokenKind::Op(o) if o == "**")
2069    }
2070
2071    fn map_entries(
2072        &mut self,
2073        open: Span,
2074        closer: &TokenKind,
2075    ) -> Result<(Vec<Spanned>, bool), Fail> {
2076        let mut segments = Vec::new();
2077        let mut items = vec![sym_at(open, LOWERED_MAP)];
2078        let mut spread = false;
2079        loop {
2080            self.skip_newlines();
2081            if self.at_spread() {
2082                let star = self.peek_span();
2083                self.bump();
2084                let value = self.expr(0)?;
2085                if items.len() > 1 {
2086                    let span = items[1].span.merge(items[items.len() - 1].span);
2087                    items[0] = sym_at(items[1].span, LOWERED_MAP);
2088                    segments.push(list_at(span, std::mem::take(&mut items)));
2089                    items = vec![sym_at(open, LOWERED_MAP)];
2090                }
2091                segments.push(list_at(
2092                    star.merge(value.span),
2093                    vec![sym_at(star, LOWERED_SPLICE), value],
2094                ));
2095                spread = true;
2096            } else {
2097                self.map_entry(&mut items, closer)?;
2098            }
2099            self.skip_newlines();
2100            if self.eat(&TokenKind::Comma) {
2101                if self.trailing_comma(closer) {
2102                    break;
2103                }
2104                continue;
2105            }
2106            self.expect(closer, "`,` or the end of the entries")?;
2107            break;
2108        }
2109        if items.len() > 1 || !spread {
2110            let whole = self.span_since(open.start);
2111            segments.push(list_at(whole, items));
2112        }
2113        Ok((segments, spread))
2114    }
2115
2116    fn map_entry(&mut self, items: &mut Vec<Spanned>, closer: &TokenKind) -> Result<(), Fail> {
2117        match self.peek().clone() {
2118            TokenKind::Label(name) => {
2119                let label_span = self.peek_span();
2120                self.bump();
2121                self.skip_newlines();
2122                items.push(atom_at(label_span, Atom::Keyword(name.clone())));
2123                if matches!(self.peek(), TokenKind::Comma) || self.at(closer) {
2124                    items.push(sym_at(label_span, &name));
2125                } else {
2126                    let value = self.expr(0)?;
2127                    if matches!(&value.form, SpannedForm::Atom(Atom::Symbol(s)) if *s == name)
2128                        && label_name(&name)
2129                    {
2130                        let written = format!("{name}: {name}");
2131                        let canonical = format!("{name}:");
2132                        self.note(
2133                            Spelling::MapPun,
2134                            label_span.merge(value.span),
2135                            &written,
2136                            &canonical,
2137                        );
2138                    }
2139                    items.push(value);
2140                }
2141            }
2142            _ => {
2143                let k = self.expr(0)?;
2144                self.skip_newlines();
2145                if let SpannedForm::Atom(Atom::Keyword(name)) = &k.form {
2146                    if blue_lang_name(name) {
2147                        let written = format!(":{name} =>");
2148                        let canonical = format!("{name}:");
2149                        self.note(Spelling::KeywordRocket, k.span, &written, &canonical);
2150                    }
2151                }
2152                self.expect(&TokenKind::Rocket, "`=>` in a map literal")?;
2153                self.skip_newlines();
2154                items.push(k);
2155                items.push(self.expr(0)?);
2156            }
2157        }
2158        Ok(())
2159    }
2160
2161    /// `if c ... [else ...] end`, and `unless` as its negation.
2162    ///
2163    /// `unless` lowers to `(if (not c) ...)` rather than to a distinct
2164    /// form: one tree per meaning, so the formatter and every downstream
2165    /// tool see exactly one shape.
2166    /// An `elsif` arm: an `if` that does NOT consume the closing `end`.
2167    ///
2168    /// `if / elsif / elsif / else / end` is one `end` for the whole chain, so
2169    /// every arm but the first must leave it for its parent.
2170    fn if_chain(&mut self, head: Span) -> Result<Spanned, Fail> {
2171        let cond = self.expr(0)?;
2172        let then = self.body(&["elsif", "else", "end"])?;
2173        let els = self.else_part()?;
2174        let mut out = vec![sym_at(head, "if"), cond, then];
2175        if let Some(e) = els {
2176            out.push(e);
2177        }
2178        Ok(list_at(self.span_since(head.start), out))
2179    }
2180
2181    fn else_part(&mut self) -> Result<Option<Spanned>, Fail> {
2182        if self.at_ident("elsif") {
2183            let elsif = self.peek_span();
2184            self.bump();
2185            return Ok(Some(self.if_chain(elsif)?));
2186        }
2187        if !self.at_ident("else") {
2188            self.expect_ident("end")?;
2189            return Ok(None);
2190        }
2191        let else_span = self.peek_span();
2192        self.bump();
2193        let save = self.pos;
2194        self.skip_newlines();
2195        let leads_with_if = self.at_ident("if") || self.at_ident("unless");
2196        self.pos = save;
2197        let e = self.body(&["end"])?;
2198        let end_at = self.peek_span().start;
2199        self.expect_ident("end")?;
2200        if matches!(e.form, SpannedForm::Nil) && !self.comment_within(else_span.start, end_at) {
2201            self.note(Spelling::ElseNil, else_span, "else nil", "no else");
2202            return Ok(None);
2203        }
2204        let single_if = matches!(&e.form, SpannedForm::List(items)
2205            if (items.len() == 3 || items.len() == 4) && items[0].as_symbol() == Some("if"));
2206        if leads_with_if && single_if {
2207            self.note(Spelling::ElseIf, else_span, "else if", "elsif");
2208        }
2209        Ok(Some(e))
2210    }
2211
2212    fn if_form(&mut self, head: Span, negate: bool) -> Result<Spanned, Fail> {
2213        let cond = self.expr(0)?;
2214        let cond = if negate {
2215            // `unless c` lowers to `(if (not c) …)`. The synthesized `not` takes
2216            // the `unless` keyword's span — that word is what put it there.
2217            let inner = cond.span;
2218            self.negation(head, head.merge(inner), cond)
2219        } else {
2220            cond
2221        };
2222        // `elsif` terminates the then-body too, or the chain is swallowed.
2223        //
2224        // It used to be absent entirely — not a keyword, not reserved, nowhere
2225        // in this file — so `elsif cond` parsed as a stray expression inside
2226        // the then-body and the branch it guarded VANISHED. No error: the
2227        // program ran and returned the `else` value. Measured:
2228        //
2229        //   if a < 1 then 1 elsif a < 5 then 2 else 3 end   with a = 3
2230        //     wanted 2, returned 3
2231        //
2232        // A silent wrong answer in the most-used control form in the language,
2233        // reachable by writing the Ruby every blue user already knows.
2234        let then = self.body(&["elsif", "else", "end"])?;
2235        let els = self.else_part()?;
2236        let mut out = vec![sym_at(head, "if"), cond, then];
2237        if let Some(e) = els {
2238            out.push(e);
2239        }
2240        Ok(list_at(self.span_since(head.start), out))
2241    }
2242
2243    /// `def name(a, b) ... end`            => `(define (name a b) body)`
2244    /// `def name(a: T, b: T) -> R ... end`  => `(define-typed (name (a T) (b T)) R body)`
2245    ///
2246    /// **The two shapes are deliberately different heads.** §0 says an
2247    /// unannotated program gets ZERO analysis, and the cleanest way to
2248    /// mean that is for untyped code not to reach the typing machinery at
2249    /// all — not to reach it and be waved through. A checker that must
2250    /// walk every node to discover there is nothing to check has already
2251    /// paid the cost the ladder exists to avoid.
2252    ///
2253    /// Annotations are per-parameter, so a signature may be partially
2254    /// annotated. That is the ladder at its finest grain: `a: Int` is
2255    /// checked and a bare `b` stays `dyn`, in the same signature.
2256    /// `case subject / when a / … / else / … / end` => a `cond` over equality.
2257    ///
2258    /// **Value matching, not destructuring.** Elixir's `case` binds pattern
2259    /// variables; blue's compares with the same `equal?` the `==` operator uses,
2260    /// so `when [1, 2]` matches a list by value. Destructuring needs a pattern
2261    /// language and a binder, which blue does not have — and a `case` that
2262    /// *looked* like Elixir's while silently only comparing would be worse than
2263    /// one that plainly compares.
2264    ///
2265    /// The subject is evaluated ONCE, into a binding, so `case expensive()` does
2266    /// not re-run per arm. That is a correctness property, not an optimisation:
2267    /// a subject with a side effect would fire once per `when`.
2268    fn case_form(&mut self, head: Span) -> Result<Spanned, Fail> {
2269        let subject = self.expr(0)?;
2270        self.skip_newlines();
2271
2272        // A fresh name the surface cannot spell, so it cannot capture a user
2273        // binding of the same name.
2274        let subject_var = "case-subject";
2275        let mut arms: Vec<Spanned> = Vec::new();
2276        let mut otherwise: Option<Spanned> = None;
2277        // The `else` keyword's own span, so the synthesized `(else body)` clause
2278        // sits where the author wrote it rather than back at `case`.
2279        let mut else_kw: Option<Span> = None;
2280
2281        loop {
2282            self.skip_newlines();
2283            if self.at_ident("end") {
2284                break;
2285            }
2286            let maybe_else = self.peek_span();
2287            if self.eat_ident("else") {
2288                else_kw = Some(maybe_else);
2289                otherwise = Some(self.body(&["end"])?);
2290                continue;
2291            }
2292            let when = self.peek_span();
2293            if !self.eat_ident("when") {
2294                return Err(self.error(format!(
2295                    "expected `when`, `else` or `end` in a case, found {}",
2296                    self.peek()
2297                )));
2298            }
2299            self.skip_newlines();
2300            let pattern = self.expr(0)?;
2301            let body = self.body(&["when", "else", "end"])?;
2302            // The synthesized comparison takes the arm's `when` span: that word
2303            // is what put an `equal?` there, and it is where a reader looks when
2304            // told an arm cannot match.
2305            let test = list_at(
2306                when.merge(pattern.span),
2307                vec![sym_at(when, "equal?"), sym_at(when, subject_var), pattern],
2308            );
2309            arms.push(list_at(when.merge(body.span), vec![test, body]));
2310        }
2311        self.expect_ident("end")?;
2312
2313        if arms.is_empty() && otherwise.is_none() {
2314            return Err(self.error("a case needs at least one `when` or an `else`".to_string()));
2315        }
2316
2317        // A case with no matching arm and no else is NIL, matching Ruby. Elixir
2318        // raises CaseClauseError; blue follows Ruby because its `if` without an
2319        // else is already nil, and having two different answers to "no branch
2320        // taken" in one language is the inconsistency.
2321        let mut cond = vec![sym_at(head, "cond")];
2322        cond.extend(arms);
2323        // The `else` clause spans its own keyword through its body. Anchoring it
2324        // at `head` — the `case` keyword — put a node whose subtree sits at the
2325        // END of the form at a span near its START, so the else body escaped its
2326        // parent's range entirely. `every_node_sits_inside_its_parents_span`
2327        // caught that; it is the kind of mistake no output-shape test can see,
2328        // because the tree is correct and only the positions are wrong.
2329        let else_span = else_kw.unwrap_or(head);
2330        let else_body = otherwise.unwrap_or_else(|| Spanned::new(else_span, SpannedForm::Nil));
2331        cond.push(list_at(
2332            else_span.merge(else_body.span),
2333            vec![sym_at(else_span, "else"), else_body],
2334        ));
2335
2336        let whole = self.span_since(head.start);
2337        let binding = list_at(
2338            head.merge(subject.span),
2339            vec![sym_at(head, subject_var), subject],
2340        );
2341        Ok(list_at(
2342            whole,
2343            vec![
2344                sym_at(head, "let"),
2345                list_at(binding.span, vec![binding]),
2346                list_at(whole, cond),
2347            ],
2348        ))
2349    }
2350
2351    /// Is the `(` just consumed the head of `(a, b) -> expr`? Only bare names
2352    /// and commas between the parens, then `->`, all on one line.
2353    fn arrow_lambda_ahead(&self) -> bool {
2354        let mut i = self.pos;
2355        let kind = |i: usize| self.toks.get(i).map(|t| &t.kind);
2356        if !matches!(kind(i), Some(TokenKind::RParen)) {
2357            loop {
2358                if matches!(kind(i), Some(TokenKind::Op(o)) if o == "*") {
2359                    i += 1;
2360                }
2361                if !matches!(kind(i), Some(TokenKind::Ident(_))) {
2362                    return false;
2363                }
2364                i += 1;
2365                match kind(i) {
2366                    Some(TokenKind::Comma) => i += 1,
2367                    Some(TokenKind::RParen) => break,
2368                    _ => return false,
2369                }
2370            }
2371        }
2372        matches!(kind(i + 1), Some(TokenKind::Op(o)) if o == "->")
2373    }
2374
2375    /// `(a, b) -> expr` => `(lambda (a b) expr)`: the inline spelling of a
2376    /// one-expression `fn`. `->` is unambiguous here: the return-type arrow
2377    /// only follows a `def` header, never a parenthesised name list in
2378    /// expression position. The formatter spells a lambda this way exactly
2379    /// when its body is one expression that fits the line, and as a broken
2380    /// `fn … end` otherwise.
2381    fn arrow_lambda(&mut self, head: Span) -> Result<Spanned, Fail> {
2382        let mut params: Vec<Spanned> = Vec::new();
2383        while !self.at(&TokenKind::RParen) {
2384            let p_span = self.peek_span();
2385            if self.at_star() {
2386                params.extend(self.rest_parameter()?);
2387                break;
2388            }
2389            match self.bump() {
2390                TokenKind::Ident(p) => params.push(sym_at(p_span, &p)),
2391                other => {
2392                    return Err(fail(
2393                        format!("expected a parameter name, found {other}"),
2394                        p_span,
2395                    ))
2396                }
2397            }
2398            self.eat(&TokenKind::Comma);
2399        }
2400        self.bump();
2401        self.bump();
2402        self.skip_newlines();
2403        let params_span = cover(&params, head);
2404        let body = self.expr(0)?;
2405        Ok(list_at(
2406            self.span_since(head.start),
2407            vec![sym_at(head, "lambda"), list_at(params_span, params), body],
2408        ))
2409    }
2410
2411    fn rest_parameter(&mut self) -> Result<[Spanned; 2], Fail> {
2412        let star = self.peek_span();
2413        self.bump();
2414        let name_span = self.peek_span();
2415        match self.bump() {
2416            TokenKind::Ident(p) => {
2417                let whole = star.merge(name_span);
2418                Ok([sym_at(whole, "&rest"), sym_at(whole, &p)])
2419            }
2420            other => Err(fail(
2421                format!("expected a parameter name after `*`, found {other}"),
2422                name_span,
2423            )),
2424        }
2425    }
2426
2427    fn at_star(&self) -> bool {
2428        matches!(self.peek(), TokenKind::Op(o) if o == "*")
2429    }
2430
2431    fn optional_mark(
2432        &self,
2433        name: String,
2434        span: Span,
2435        optional_from: &mut Option<usize>,
2436        index: usize,
2437    ) -> Result<String, Fail> {
2438        match name.strip_suffix('?').filter(|n| !n.is_empty()) {
2439            Some(bare) => {
2440                optional_from.get_or_insert(index);
2441                Ok(bare.to_string())
2442            }
2443            None if optional_from.is_some() => Err(fail(
2444                format!("the required parameter `{name}` cannot follow an optional one"),
2445                span,
2446            )),
2447            None => Ok(name),
2448        }
2449    }
2450
2451    fn rest_is_last(&mut self) -> Result<(), Fail> {
2452        self.skip_newlines();
2453        if self.at(&TokenKind::Comma) {
2454            return Err(self.error("a rest parameter `*name` comes last".to_string()));
2455        }
2456        Ok(())
2457    }
2458
2459    /// `fn(a, b) ... end` => `(lambda (a b) body)`
2460    ///
2461    /// Without this the higher-order functions are unreachable in practice:
2462    /// `map(inc, xs)` works only because `inc` happens to be a named stdlib
2463    /// function, and there was no way to write the one-off the call site
2464    /// actually wants.
2465    ///
2466    /// `fn … end` is the block spelling and always breaks over lines; the
2467    /// inline spelling is `(x) -> expr` ([`Self::arrow_lambda`]).
2468    fn lambda_form(&mut self, head: Span) -> Result<Spanned, Fail> {
2469        let mut params: Vec<Spanned> = Vec::new();
2470        if !self.at(&TokenKind::LParen) {
2471            self.note(Spelling::ParameterList, head, "fn", "fn()");
2472        }
2473        if self.at(&TokenKind::LParen) {
2474            self.bump();
2475            self.skip_newlines();
2476            if !self.eat(&TokenKind::RParen) {
2477                loop {
2478                    self.skip_newlines();
2479                    let p_span = self.peek_span();
2480                    if self.at_star() {
2481                        params.extend(self.rest_parameter()?);
2482                        self.rest_is_last()?;
2483                        self.expect(&TokenKind::RParen, "`)` after the rest parameter")?;
2484                        break;
2485                    }
2486                    match self.bump() {
2487                        TokenKind::Ident(p) => params.push(sym_at(p_span, &p)),
2488                        other => {
2489                            return Err(fail(
2490                                format!("expected a parameter name, found {other}"),
2491                                p_span,
2492                            ))
2493                        }
2494                    }
2495                    self.skip_newlines();
2496                    if self.eat(&TokenKind::Comma) {
2497                        if self.trailing_comma(&TokenKind::RParen) {
2498                            break;
2499                        }
2500                        continue;
2501                    }
2502                    self.expect(&TokenKind::RParen, "`,` or `)`")?;
2503                    break;
2504                }
2505            }
2506        }
2507        let params_span = cover(&params, head);
2508        let body = self.body(&["end"])?;
2509        self.expect_ident("end")?;
2510        Ok(list_at(
2511            self.span_since(head.start),
2512            vec![sym_at(head, "lambda"), list_at(params_span, params), body],
2513        ))
2514    }
2515
2516    /// `test "name" ... end` => `(deftest "name" body)`
2517    ///
2518    /// A string, not an identifier: a test name is prose for a human report,
2519    /// and forcing it into an identifier is how test names become
2520    /// `test_adds_two_numbers_correctly`.
2521    fn test_form(&mut self, head: Span) -> Result<Spanned, Fail> {
2522        let name_span = self.peek_span();
2523        let name = match self.bump() {
2524            TokenKind::Str(s) => s,
2525            other => {
2526                return Err(fail(
2527                    format!("expected a string name after `test`, found {other}"),
2528                    name_span,
2529                ))
2530            }
2531        };
2532        let body = self.body(&["end"])?;
2533        self.expect_ident("end")?;
2534        Ok(list_at(
2535            self.span_since(head.start),
2536            vec![
2537                sym_at(head, "deftest"),
2538                atom_at(name_span, Atom::Str(name)),
2539                body,
2540            ],
2541        ))
2542    }
2543
2544    /// `assert expr` => `(blue-assert 'expr expr)`
2545    ///
2546    /// **`blue-assert`, not `assert`.** tatara-lisp's stdlib already defines
2547    /// `assert` as a macro — `(defmacro assert (pred message) …)` — and a macro
2548    /// in the expander is consulted before any primitive in the registry. So
2549    /// lowering to `assert` bound `pred` to the *quoted form*, which is
2550    /// truthy, and **every assertion silently passed**. A test framework whose
2551    /// assertions always pass is the worst defect it can have: every test in
2552    /// the suite goes green.
2553    ///
2554    /// The lesson generalizes: any name blue lowers to that tatara already
2555    /// binds is silently captured. `blue_lang_test`'s
2556    /// `no_lowered_name_is_shadowed_by_the_runtime` gates the whole class.
2557    ///
2558    /// **Both the form and the value.** A test framework whose failure says
2559    /// only "assertion failed" makes the author re-derive what they were
2560    /// checking; one that shows the expression does not. The quoted form is
2561    /// the expression as DATA, so the runner can render it — and it renders it
2562    /// through `blue-lang-fmt`, meaning the failure message is in canonical
2563    /// blue syntax rather than the underlying tatara-lisp.
2564    ///
2565    /// This is homoiconicity paying for itself: the capture needs no source
2566    /// map, no macro hygiene, and no string of the original text.
2567    fn assert_form(&mut self, head: Span) -> Result<Spanned, Fail> {
2568        let e = self.expr(0)?;
2569        let quoted = Spanned::new(e.span, SpannedForm::Quote(Box::new(e.clone())));
2570        Ok(list_at(
2571            self.span_since(head.start),
2572            vec![sym_at(head, LOWERED_ASSERT), quoted, e],
2573        ))
2574    }
2575
2576    /// `defmacro name(a, b) ... end` => `(defmacro name (a b) body)`
2577    ///
2578    /// **Deliberately untyped.** A macro's parameters are *source forms*, not
2579    /// values, so `a: Int` would be a category error: the argument at expansion
2580    /// time is a fragment of syntax. §IV's ladder types values; macro
2581    /// parameters are not on it. Annotating one is rejected rather than
2582    /// silently ignored — an ignored annotation is how an author comes to
2583    /// believe a check is running.
2584    fn defmacro_form(&mut self, head: Span) -> Result<Spanned, Fail> {
2585        let name_span = self.peek_span();
2586        let name = match self.bump() {
2587            TokenKind::Ident(n) => n,
2588            other => {
2589                return Err(fail(
2590                    format!("expected a name after `defmacro`, found {other}"),
2591                    name_span,
2592                ))
2593            }
2594        };
2595        self.not_reserved(&name, name_span, "a `defmacro` name")?;
2596        let mut params: Vec<Spanned> = Vec::new();
2597        if self.at(&TokenKind::LParen) {
2598            self.bump();
2599            self.skip_newlines();
2600            if !self.eat(&TokenKind::RParen) {
2601                loop {
2602                    self.skip_newlines();
2603                    let p_span = self.peek_span();
2604                    match self.bump() {
2605                        TokenKind::Ident(p) => params.push(sym_at(p_span, &p)),
2606                        TokenKind::Label(p) => {
2607                            return Err(self.error(format!(
2608                                "macro parameter `{p}` cannot be typed: a macro receives \
2609                                 source forms, not values"
2610                            )))
2611                        }
2612                        other => {
2613                            return Err(fail(
2614                                format!("expected a parameter name, found {other}"),
2615                                p_span,
2616                            ))
2617                        }
2618                    }
2619                    self.skip_newlines();
2620                    if self.eat(&TokenKind::Comma) {
2621                        if self.trailing_comma(&TokenKind::RParen) {
2622                            break;
2623                        }
2624                        continue;
2625                    }
2626                    self.expect(&TokenKind::RParen, "`,` or `)`")?;
2627                    break;
2628                }
2629            }
2630        }
2631        if matches!(self.peek(), TokenKind::Op(o) if o == "->") {
2632            return Err(self.error(
2633                "a macro has no return type: it produces source forms, not values".to_string(),
2634            ));
2635        }
2636
2637        let params_span = cover(&params, name_span);
2638        let body = self.body(&["end"])?;
2639        self.expect_ident("end")?;
2640
2641        // `(defmacro name (params) body)` — tatara-lisp's own shape, so blue
2642        // registers into the SAME expander rather than a parallel one.
2643        Ok(list_at(
2644            self.span_since(head.start),
2645            vec![
2646                sym_at(head, "defmacro"),
2647                sym_at(name_span, &name),
2648                list_at(params_span, params),
2649                body,
2650            ],
2651        ))
2652    }
2653
2654    /// `quote ... end` => `` `body `` (a quasiquote).
2655    ///
2656    /// Quasiquote rather than plain quote, because a macro body that could not
2657    /// splice its arguments in would be useless — this is Elixir's `quote do`,
2658    /// which is likewise a template and not inert data.
2659    fn quote_form(&mut self, head: Span) -> Result<Spanned, Fail> {
2660        self.quoting += 1;
2661        let body = self.body(&["end"]);
2662        self.quoting -= 1;
2663        let body = body?;
2664        self.expect_ident("end")?;
2665        // `Sexp::Quasiquote`, NOT `(quasiquote body)` as a list.
2666        //
2667        // The list form Displays as the text `(quasiquote …)`, which the
2668        // tatara-lisp reader reads back as an ordinary list whose head happens
2669        // to be the symbol `quasiquote` — losing the structure. The evaluator
2670        // then reached the inner `,x` with no enclosing quasiquote and rejected
2671        // it: "unquote outside of quasiquote". Building the real variant makes
2672        // it Display as `` ` `` and survive the round trip.
2673        Ok(Spanned::new(
2674            self.span_since(head.start),
2675            SpannedForm::Quasiquote(Box::new(body)),
2676        ))
2677    }
2678
2679    /// `unquote(expr)` => `,expr`; `unquote_splice(expr)` => `,@expr`.
2680    fn unquote_form(&mut self, head: Span, splice: bool) -> Result<Spanned, Fail> {
2681        self.expect(&TokenKind::LParen, "`(` after unquote")?;
2682        self.skip_newlines();
2683        let inner = self.expr(0)?;
2684        self.skip_newlines();
2685        self.expect(&TokenKind::RParen, "`)`")?;
2686        let span = self.span_since(head.start);
2687        Ok(Spanned::new(
2688            span,
2689            if splice {
2690                SpannedForm::UnquoteSplice(Box::new(inner))
2691            } else {
2692                SpannedForm::Unquote(Box::new(inner))
2693            },
2694        ))
2695    }
2696
2697    fn protected_body(&mut self, head: Span) -> Result<Spanned, Fail> {
2698        let body = self.body(&["rescue", "ensure", "end"])?;
2699        if self.at_ident("rescue") || self.at_ident("ensure") {
2700            return self.rescue_tail(head, body);
2701        }
2702        Ok(body)
2703    }
2704
2705    fn begin_form(&mut self, head: Span) -> Result<Spanned, Fail> {
2706        let body = self.body(&["rescue", "ensure", "end"])?;
2707        if !(self.at_ident("rescue") || self.at_ident("ensure")) {
2708            return Err(self.error(
2709                "a `begin` block protects its statements with `rescue` or `ensure`; without either, write the statements directly",
2710            ));
2711        }
2712        let tree = self.rescue_tail(head, body)?;
2713        self.expect_ident("end")?;
2714        Ok(Spanned::new(self.span_since(head.start), tree.form))
2715    }
2716
2717    fn rescue_tail(&mut self, head: Span, body: Spanned) -> Result<Spanned, Fail> {
2718        let mut clauses: Vec<Spanned> = Vec::new();
2719        let mut catch_all: Option<(Spanned, Spanned, Span)> = None;
2720        while self.at_ident("rescue") {
2721            let kw = self.peek_span();
2722            self.bump();
2723            if catch_all.is_some() {
2724                return Err(fail(
2725                    "a `rescue NAME` catches every error, so it is the last `rescue`; put the kinds it would shadow above it",
2726                    kw,
2727                ));
2728            }
2729            match self.peek().clone() {
2730                TokenKind::Sym(_) => {
2731                    let mut kinds = Vec::new();
2732                    loop {
2733                        let k_span = self.peek_span();
2734                        match self.bump() {
2735                            TokenKind::Sym(k) => kinds.push(atom_at(k_span, Atom::Keyword(k))),
2736                            other => {
2737                                return Err(fail(
2738                                    format!("expected an error kind such as `:not_found`, found {other}"),
2739                                    k_span,
2740                                ))
2741                            }
2742                        }
2743                        if self.eat(&TokenKind::Comma) {
2744                            self.skip_newlines();
2745                            continue;
2746                        }
2747                        break;
2748                    }
2749                    let binder = if self.eat_ident("as") {
2750                        let b_span = self.peek_span();
2751                        match self.bump() {
2752                            TokenKind::Ident(b) if !is_reserved_word(&b) => Some(sym_at(b_span, &b)),
2753                            other => {
2754                                return Err(fail(
2755                                    format!("expected the name the error is bound to after `as`, found {other}"),
2756                                    b_span,
2757                                ))
2758                            }
2759                        }
2760                    } else {
2761                        None
2762                    };
2763                    let handler = self.body(&["rescue", "ensure", "end"])?;
2764                    let mut test = vec![sym_at(kw, LOWERED_ERROR_MATCH), sym_at(kw, RESCUED)];
2765                    test.extend(kinds);
2766                    let test = list_at(cover(&test, kw), test);
2767                    let handler = match binder {
2768                        Some(b) => let_bound(b, handler, kw),
2769                        None => handler,
2770                    };
2771                    clauses.push(list_at(kw.merge(handler.span), vec![test, handler]));
2772                }
2773                TokenKind::Ident(b) if !is_reserved_word(&b) => {
2774                    let b_span = self.peek_span();
2775                    self.bump();
2776                    let handler = self.body(&["rescue", "ensure", "end"])?;
2777                    catch_all = Some((sym_at(b_span, &b), handler, kw));
2778                }
2779                _ => {
2780                    return Err(fail(
2781                        "`rescue` names what it catches: `rescue :kind`, `rescue :a, :b as e`, or `rescue e` for every error (`rescue _` discards it)",
2782                        kw,
2783                    ))
2784                }
2785            }
2786        }
2787        let ensure = if self.at_ident("ensure") {
2788            self.bump();
2789            let cleanup = self.body(&["rescue", "end"])?;
2790            if self.at_ident("rescue") {
2791                return Err(self.error("`ensure` comes after every `rescue` of its block"));
2792            }
2793            Some(cleanup)
2794        } else {
2795            None
2796        };
2797        let whole = self.span_since(head.start);
2798        let protected = if clauses.is_empty() {
2799            match catch_all {
2800                None => body,
2801                Some((binder, handler, kw)) => list_at(
2802                    whole,
2803                    vec![
2804                        sym_at(head, "try"),
2805                        body,
2806                        list_at(
2807                            kw.merge(handler.span),
2808                            vec![
2809                                sym_at(kw, "catch"),
2810                                list_at(binder.span, vec![binder]),
2811                                handler,
2812                            ],
2813                        ),
2814                    ],
2815                ),
2816            }
2817        } else {
2818            let first_kw = clauses[0].span;
2819            let otherwise = match catch_all {
2820                Some((binder, handler, kw)) => {
2821                    let bound = let_bound(binder, handler, kw);
2822                    list_at(kw.merge(bound.span), vec![sym_at(kw, "else"), bound])
2823                }
2824                None => list_at(
2825                    head,
2826                    vec![
2827                        sym_at(head, "else"),
2828                        list_at(
2829                            head,
2830                            vec![sym_at(head, LOWERED_RERAISE), sym_at(head, RESCUED)],
2831                        ),
2832                    ],
2833                ),
2834            };
2835            let mut cond = vec![sym_at(first_kw, "cond")];
2836            cond.extend(clauses);
2837            cond.push(otherwise);
2838            let cond_span = cover(&cond, first_kw);
2839            list_at(
2840                whole,
2841                vec![
2842                    sym_at(head, "try"),
2843                    body,
2844                    list_at(
2845                        cond_span,
2846                        vec![
2847                            sym_at(first_kw, "catch"),
2848                            list_at(first_kw, vec![sym_at(first_kw, RESCUED)]),
2849                            list_at(cond_span, cond),
2850                        ],
2851                    ),
2852                ],
2853            )
2854        };
2855        Ok(match ensure {
2856            None => protected,
2857            Some(cleanup) => {
2858                let attempted = list_at(
2859                    protected.span,
2860                    vec![sym_at(head, LOWERED_ATTEMPTED), protected],
2861                );
2862                let failed = list_at(
2863                    head,
2864                    vec![sym_at(head, LOWERED_FAILED), sym_at(head, RESCUED)],
2865                );
2866                let caught = list_at(
2867                    whole,
2868                    vec![
2869                        sym_at(head, "try"),
2870                        attempted,
2871                        list_at(
2872                            head,
2873                            vec![
2874                                sym_at(head, "catch"),
2875                                list_at(head, vec![sym_at(head, RESCUED)]),
2876                                failed,
2877                            ],
2878                        ),
2879                    ],
2880                );
2881                list_at(
2882                    whole,
2883                    vec![sym_at(head, LOWERED_ENSURE), caught, thunk(cleanup, head)],
2884                )
2885            }
2886        })
2887    }
2888
2889    fn def_form(&mut self, head: Span) -> Result<Spanned, Fail> {
2890        let name_span = self.peek_span();
2891        let name = match self.bump() {
2892            TokenKind::Ident(n) => n,
2893            other => {
2894                return Err(fail(
2895                    format!("expected a name after `def`, found {other}"),
2896                    name_span,
2897                ))
2898            }
2899        };
2900        self.not_reserved(&name, name_span, "a `def` name")?;
2901        let mut params: Vec<(String, Span, Option<Spanned>)> = Vec::new();
2902        let mut optional_from: Option<usize> = None;
2903        let mut rest: Vec<Spanned> = Vec::new();
2904        if !self.at(&TokenKind::LParen) {
2905            self.note(
2906                Spelling::ParameterList,
2907                name_span,
2908                &format!("def {name}"),
2909                &format!("def {name}()"),
2910            );
2911        }
2912        if self.at(&TokenKind::LParen) {
2913            self.bump();
2914            self.skip_newlines();
2915            if !self.eat(&TokenKind::RParen) {
2916                loop {
2917                    self.skip_newlines();
2918                    let p_span = self.peek_span();
2919                    if self.at_star() {
2920                        if optional_from.is_some() {
2921                            return Err(fail(
2922                                "a rest parameter cannot follow an optional one",
2923                                p_span,
2924                            ));
2925                        }
2926                        rest = self.rest_parameter()?.to_vec();
2927                        self.rest_is_last()?;
2928                        self.expect(&TokenKind::RParen, "`)` after the rest parameter")?;
2929                        break;
2930                    }
2931                    match self.bump() {
2932                        // `a` — unannotated
2933                        TokenKind::Ident(p) => {
2934                            self.not_reserved(&p, p_span, "a parameter name")?;
2935                            let p =
2936                                self.optional_mark(p, p_span, &mut optional_from, params.len())?;
2937                            params.push((p, p_span, None));
2938                        }
2939                        // `a:` came through as one token, so a type follows
2940                        TokenKind::Label(p) => {
2941                            self.not_reserved(&p, p_span, "a parameter name")?;
2942                            let p =
2943                                self.optional_mark(p, p_span, &mut optional_from, params.len())?;
2944                            self.skip_newlines();
2945                            let ty = self.type_expr()?;
2946                            params.push((p, p_span, Some(ty)));
2947                        }
2948                        other => {
2949                            return Err(fail(
2950                                format!("expected a parameter name, found {other}"),
2951                                p_span,
2952                            ))
2953                        }
2954                    }
2955                    self.skip_newlines();
2956                    if self.eat(&TokenKind::Comma) {
2957                        if self.trailing_comma(&TokenKind::RParen) {
2958                            break;
2959                        }
2960                        continue;
2961                    }
2962                    self.expect(&TokenKind::RParen, "`,` or `)`")?;
2963                    break;
2964                }
2965            }
2966        }
2967        // The signature list spans the name through the closing `)`.
2968        let sig_span = self.span_since(name_span.start);
2969
2970        // Optional `-> R`
2971        let ret = if matches!(self.peek(), TokenKind::Op(o) if o == "->") {
2972            self.bump();
2973            self.skip_newlines();
2974            Some(self.type_expr()?)
2975        } else {
2976            None
2977        };
2978
2979        let begins = {
2980            let save = self.pos;
2981            self.skip_newlines();
2982            let at = self.at_ident("begin").then(|| self.peek_span());
2983            self.pos = save;
2984            at
2985        };
2986        let body = self.protected_body(head)?;
2987        if let Some(b) = begins {
2988            let protects = matches!(&body.form, SpannedForm::List(items)
2989                if matches!(items.first().and_then(Spanned::as_symbol), Some("try") | Some(LOWERED_ENSURE)));
2990            if protects && body.span.start == b.start {
2991                self.note(
2992                    Spelling::TryCatch,
2993                    b,
2994                    "begin … end as a whole def body",
2995                    "the clauses on the def itself",
2996                );
2997            }
2998        }
2999        self.expect_ident("end")?;
3000        let whole = self.span_since(head.start);
3001
3002        let annotated = ret.is_some() || params.iter().any(|(_, _, t)| t.is_some());
3003        if !annotated {
3004            let mut sig = vec![sym_at(name_span, &name)];
3005            for (i, (p, s, _)) in params.into_iter().enumerate() {
3006                if optional_from == Some(i) {
3007                    sig.push(sym_at(s, OPTIONAL_MARKER));
3008                }
3009                sig.push(sym_at(s, &p));
3010            }
3011            sig.extend(rest);
3012            return Ok(list_at(
3013                whole,
3014                vec![sym_at(head, "define"), list_at(sig_span, sig), body],
3015            ));
3016        }
3017
3018        // Typed shape. An un-annotated parameter in an otherwise annotated
3019        // signature is written `(p dyn)` so the checker sees the ladder
3020        // position explicitly rather than inferring it from absence.
3021        //
3022        // A `dyn` written by the parser rather than the author takes the
3023        // parameter's own span, so a diagnostic about it points at the
3024        // parameter — the only thing in the source it could mean.
3025        let mut sig = vec![sym_at(name_span, &name)];
3026        for (i, (p, s, t)) in params.into_iter().enumerate() {
3027            if optional_from == Some(i) {
3028                sig.push(sym_at(s, OPTIONAL_MARKER));
3029            }
3030            let ty = t.unwrap_or_else(|| sym_at(s, "dyn"));
3031            sig.push(list_at(s.merge(ty.span), vec![sym_at(s, &p), ty]));
3032        }
3033        sig.extend(rest);
3034        Ok(list_at(
3035            whole,
3036            vec![
3037                sym_at(head, "define-typed"),
3038                list_at(sig_span, sig),
3039                ret.unwrap_or_else(|| sym_at(head, "dyn")),
3040                body,
3041            ],
3042        ))
3043    }
3044
3045    /// A type expression: a name, or a constructor over types of any arity.
3046    fn type_expr(&mut self) -> Result<Spanned, Fail> {
3047        let name_span = self.peek_span();
3048        let name = match self.bump() {
3049            TokenKind::Ident(n) => n,
3050            other => {
3051                return Err(fail(
3052                    format!("expected a type name, found {other}"),
3053                    name_span,
3054                ))
3055            }
3056        };
3057        self.not_reserved(&name, name_span, "a type name")?;
3058        if self.at(&TokenKind::LParen) {
3059            self.bump();
3060            self.skip_newlines();
3061            let mut list = vec![sym_at(name_span, &name)];
3062            if !self.eat(&TokenKind::RParen) {
3063                loop {
3064                    self.skip_newlines();
3065                    list.push(self.type_expr()?);
3066                    self.skip_newlines();
3067                    if self.eat(&TokenKind::Comma) {
3068                        if self.trailing_comma(&TokenKind::RParen) {
3069                            break;
3070                        }
3071                        continue;
3072                    }
3073                    self.expect(&TokenKind::RParen, "`,` or `)`")?;
3074                    break;
3075                }
3076            }
3077            return Ok(list_at(self.span_since(name_span.start), list));
3078        }
3079        Ok(sym_at(name_span, &name))
3080    }
3081
3082    fn not_reserved(&self, name: &str, at: Span, what: &str) -> Result<(), Fail> {
3083        if is_reserved_word(name) {
3084            return Err(fail(
3085                format!("`{name}` is a reserved word and cannot be {what}"),
3086                at,
3087            ));
3088        }
3089        Ok(())
3090    }
3091
3092    /// Consume `name` if it is the next token, else leave the position alone.
3093    fn eat_ident(&mut self, name: &str) -> bool {
3094        if self.at_ident(name) {
3095            self.bump();
3096            true
3097        } else {
3098            false
3099        }
3100    }
3101
3102    fn expect_ident(&mut self, name: &str) -> Result<(), Fail> {
3103        if self.at_ident(name) {
3104            self.bump();
3105            Ok(())
3106        } else {
3107            Err(self.error(format!("expected `{name}`, found {}", self.peek())))
3108        }
3109    }
3110
3111    /// A sequence of expressions up to one of `terminators`, wrapped in
3112    /// `(begin ...)` when there is more than one.
3113    fn body(&mut self, terminators: &[&str]) -> Result<Spanned, Fail> {
3114        // Anchored BEFORE the leading newlines are skipped, so an empty body's
3115        // span sits where the body would have started.
3116        let start = self.mark();
3117        let mut forms = Vec::new();
3118        loop {
3119            self.skip_newlines();
3120            if matches!(self.peek(), TokenKind::Eof) {
3121                return Err(self.error(format!(
3122                    "unterminated block: expected {} before the end of the file",
3123                    one_of(terminators)
3124                )));
3125            }
3126            if terminators.iter().any(|t| self.at_ident(t)) {
3127                break;
3128            }
3129            forms.push(self.statement()?);
3130            self.end_of_statement(terminators)?;
3131        }
3132        Ok(match forms.len() {
3133            0 => Spanned::new(Span::new(start, start), SpannedForm::Nil),
3134            1 => forms.into_iter().next().expect("checked len"),
3135            _ => {
3136                // `cover`, NOT `span_since`: the loop breaks *before* consuming
3137                // the terminator but *after* `skip_newlines`, so the last token
3138                // behind `pos` can be a newline and `span_since` would stretch
3139                // the body across trailing blank lines. The forms know exactly
3140                // where they are.
3141                let span = cover(&forms, Span::new(start, start));
3142                // The synthesized `begin` is zero-width at the FIRST FORM's
3143                // start, not at `start`. `start` is anchored before the leading
3144                // newlines are skipped, so it sits earlier than the body itself
3145                // — and a head node outside its own list's span is a span bug
3146                // that `every_node_sits_inside_its_parents_span` reports.
3147                let head = Span::new(span.start, span.start);
3148                let mut list = vec![sym_at(head, "begin")];
3149                list.extend(forms);
3150                list_at(span, list)
3151            }
3152        })
3153    }
3154}
3155
3156/// `` `a` ``, `` `a` or `b` ``, `` `a`, `b` or `c` `` — a list of keywords as
3157/// a sentence says it.
3158fn one_of(words: &[&str]) -> String {
3159    let quoted: Vec<String> = words.iter().map(|w| format!("`{w}`")).collect();
3160    match quoted.split_last() {
3161        None => String::new(),
3162        Some((last, [])) => last.clone(),
3163        Some((last, rest)) => format!("{} or {last}", rest.join(", ")),
3164    }
3165}
3166
3167fn let_bound(binder: Spanned, body: Spanned, at: Span) -> Spanned {
3168    let span = at.merge(body.span);
3169    let pair = list_at(binder.span, vec![binder, sym_at(at, RESCUED)]);
3170    list_at(
3171        span,
3172        vec![sym_at(at, "let"), list_at(pair.span, vec![pair]), body],
3173    )
3174}
3175
3176fn thunk(body: Spanned, at: Span) -> Spanned {
3177    let span = at.merge(body.span);
3178    list_at(
3179        span,
3180        vec![sym_at(at, "lambda"), list_at(at, Vec::new()), body],
3181    )
3182}
3183
3184/// Whether `name:` lexes as exactly one label spelling `name`: the parser's and
3185/// the formatter's shared test for a key that can be punned (`{name:}`). It asks
3186/// the lexer, so a name ending in `?` or `!` puns too and no second list of
3187/// "plain names" can drift from what a label is.
3188#[must_use]
3189pub fn label_name(name: &str) -> bool {
3190    matches!(
3191        lex(&format!("{name}:")).as_deref(),
3192        Ok([t, e]) if matches!(&t.kind, TokenKind::Label(n) if n == name) && e.kind == TokenKind::Eof
3193    )
3194}
3195
3196fn blue_lang_name(name: &str) -> bool {
3197    matches!(
3198        lex(name).as_deref(),
3199        Ok([t, e]) if matches!(&t.kind, TokenKind::Ident(n) if n == name) && e.kind == TokenKind::Eof
3200    ) && !name.ends_with(['?', '!'])
3201}
3202
3203fn mentions(form: &Spanned, name: &str) -> bool {
3204    match &form.form {
3205        SpannedForm::Atom(Atom::Symbol(s)) => s == name,
3206        SpannedForm::List(items) => items.iter().any(|i| mentions(i, name)),
3207        SpannedForm::Quote(i)
3208        | SpannedForm::Quasiquote(i)
3209        | SpannedForm::Unquote(i)
3210        | SpannedForm::UnquoteSplice(i) => mentions(i, name),
3211        _ => false,
3212    }
3213}
3214
3215/// A symbol node at `span`.
3216fn sym_at(span: Span, s: &str) -> Spanned {
3217    Spanned::new(span, SpannedForm::Atom(Atom::Symbol(s.to_string())))
3218}
3219
3220/// An atom node at `span`.
3221fn atom_at(span: Span, a: Atom) -> Spanned {
3222    Spanned::new(span, SpannedForm::Atom(a))
3223}
3224
3225/// A list node at `span`.
3226fn list_at(span: Span, items: Vec<Spanned>) -> Spanned {
3227    Spanned::new(span, SpannedForm::List(items))
3228}
3229
3230/// The smallest span covering every node in `items`, or `fallback` when there
3231/// are none.
3232///
3233/// The fallback is a parameter rather than `Span::synthetic()` because a
3234/// synthetic span is a claim that a node has no source origin at all — and an
3235/// empty parameter list in `def f()` very much has one. Defaulting to synthetic
3236/// here is how "no span" leaks into a tree where every node does have a place.
3237fn cover(items: &[Spanned], fallback: Span) -> Span {
3238    let merged = items
3239        .iter()
3240        .fold(Span::synthetic(), |acc, s| acc.merge(s.span));
3241    if merged.is_synthetic() {
3242        fallback
3243    } else {
3244        merged
3245    }
3246}
3247
3248#[cfg(test)]
3249mod tests {
3250    use super::*;
3251
3252    /// Render an `Sexp` to canonical text so tests can state the expected
3253    /// quoted form as a string. This is `Display`, which tatara-lisp owns —
3254    /// blue does not build Lisp syntax by concatenation.
3255    fn q(src: &str) -> String {
3256        parse_expr(src)
3257            .map(|s| s.to_string())
3258            .unwrap_or_else(|e| panic!("{src:?}: {e}"))
3259    }
3260
3261    #[test]
3262    fn a_reserved_word_is_not_a_def_name_a_parameter_or_a_type() {
3263        for src in [
3264            "def def a(x)\n  x\nend",
3265            "def end()\n  1\nend",
3266            "def f(end)\n  1\nend",
3267            "def f(a: not)\n  1\nend",
3268            "def f(a) -> not a\n  a\nend",
3269            "def f(a: List(1 + 2))\n  a\nend",
3270            "defmacro defmacro m(a)\n  quote\n    a\n  end\nend",
3271        ] {
3272            assert!(parse_program(src).is_err(), "{src:?} parsed");
3273        }
3274    }
3275
3276    #[test]
3277    fn a_type_application_takes_types_of_any_arity() {
3278        assert_eq!(
3279            q("def f(a: Map(Str, List(Int))) -> Unit()\n  a\nend"),
3280            "(define-typed (f (a (Map Str (List Int)))) (Unit) a)"
3281        );
3282    }
3283
3284    // ---- the thesis: Ruby-shaped source becomes tatara-lisp ----------
3285
3286    #[test]
3287    fn arithmetic_respects_precedence() {
3288        assert_eq!(q("1 + 2 * 3"), "(+ 1 (* 2 3))");
3289        assert_eq!(q("(1 + 2) * 3"), "(* (+ 1 2) 3)");
3290    }
3291
3292    #[test]
3293    fn comparison_binds_looser_than_arithmetic() {
3294        assert_eq!(q("a + 1 < b"), "(< (+ a 1) b)");
3295    }
3296
3297    /// The expected tree names `or`/`and`, not `||`/`&&`: the surface
3298    /// spelling is the SURFACE's, and lowering renames it to the form
3299    /// tatara-lisp actually has. This test previously asserted the verbatim
3300    /// spelling, which is how `(== a b)` — a symbol nothing binds — shipped.
3301    #[test]
3302    fn logical_operators_bind_loosest_and_lower_to_tataras_names() {
3303        assert_eq!(q("a && b || c"), "(or (and a b) c)");
3304    }
3305
3306    #[test]
3307    fn left_associativity() {
3308        assert_eq!(q("1 - 2 - 3"), "(- (- 1 2) 3)");
3309    }
3310
3311    #[test]
3312    fn a_dot_without_parentheses_reads_a_field() {
3313        assert_eq!(q("user.name"), "(blue-field user :name)");
3314    }
3315
3316    #[test]
3317    fn method_call_with_args() {
3318        assert_eq!(q("user.greet(1, 2)"), "(greet user 1 2)");
3319    }
3320
3321    #[test]
3322    fn chained_fields_read_left_to_right() {
3323        assert_eq!(q("a.b.c"), "(blue-field (blue-field a :b) :c)");
3324        assert_eq!(q("a.b(1).c"), "(blue-field (b a 1) :c)");
3325    }
3326
3327    #[test]
3328    fn plain_call() {
3329        assert_eq!(q("f(1, 2)"), "(f 1 2)");
3330    }
3331
3332    /// The pipeline threads into the FIRST argument, as Elixir's does —
3333    /// that is what makes `|>` composable rather than decorative.
3334    #[test]
3335    fn pipeline_threads_into_first_argument() {
3336        assert_eq!(q("x |> f"), "(f x)");
3337        assert_eq!(q("x |> f(1)"), "(f x 1)");
3338        assert_eq!(q("x |> f |> g"), "(g (f x))");
3339    }
3340
3341    #[test]
3342    fn pipeline_binds_looser_than_arithmetic() {
3343        assert_eq!(q("1 + 2 |> f"), "(f (+ 1 2))");
3344    }
3345
3346    // ---- §V.13's rendering law, enforced at the parser ---------------
3347
3348    /// `a: 1` and `:a => 1` are the SAME TREE. That is exactly why the
3349    /// formatter may always render the shorthand: they are not two
3350    /// spellings of two things, they are two spellings of one thing.
3351    #[test]
3352    fn label_and_rocket_produce_the_same_tree_for_a_symbol_key() {
3353        assert_eq!(q("{a: 1}"), q("{:a => 1}"));
3354        assert_eq!(q("{a: 1}"), "(hash-map :a 1)");
3355    }
3356
3357    /// And where the key is NOT a plain symbol, the rocket is the only
3358    /// spelling — so it survives because it must, never as a style choice.
3359    #[test]
3360    fn a_string_key_has_no_shorthand() {
3361        assert_eq!(q(r#"{"k" => 1}"#), r#"(hash-map "k" 1)"#);
3362    }
3363
3364    #[test]
3365    fn a_newline_escape_and_its_lines_are_one_tree() {
3366        for (escaped, lines) in [
3367            (r#""a\nb""#, r#"join_lines(["a", "b"])"#),
3368            (r##""#{m}\n#{h}""##, "join_lines([m, h])"),
3369            (r##""n=#{n}\ndone\n""##, r##"unlines(["n=#{n}", "done"])"##),
3370            (r#""\n""#, "line_break"),
3371            (r#""a\n\n\tb\n""#, r#"unlines(["a", "", "\tb"])"#),
3372            (r#""\na""#, r#"join_lines(["", "a"])"#),
3373            (r#""\n\n""#, r#"unlines(["", ""])"#),
3374        ] {
3375            assert_eq!(q(escaped), q(lines), "{escaped}");
3376        }
3377        assert_eq!(q(r#""a\tb""#), q(r#""a\tb""#));
3378        assert!(!q(r#""a\tb""#).contains("join_lines"));
3379    }
3380
3381    #[test]
3382    fn a_newline_escape_is_noted_and_a_quoted_one_is_data() {
3383        let notes = spelling_notes(r#""a\nb""#).unwrap();
3384        assert_eq!(notes.len(), 1);
3385        assert_eq!(notes[0].spelling, Spelling::NewlineEscape);
3386        assert!(spelling_notes(r#""a\tb""#).unwrap().is_empty());
3387        let quoted = q("def f()\n  quote\n    \"x\\ny\"\n  end\nend\n");
3388        assert!(!quoted.contains("join_lines"), "{quoted}");
3389        assert!(
3390            spelling_notes("def f()\n  quote\n    \"x\\ny\"\n  end\nend\n")
3391                .unwrap()
3392                .is_empty()
3393        );
3394    }
3395
3396    #[test]
3397    fn list_literal() {
3398        assert_eq!(q("[1, 2, 3]"), "(list 1 2 3)");
3399        assert_eq!(q("[]"), "(list)");
3400    }
3401
3402    // ---- blocks ------------------------------------------------------
3403
3404    #[test]
3405    fn if_else_end() {
3406        assert_eq!(q("if a\n  1\nelse\n  2\nend"), "(if a 1 2)");
3407    }
3408
3409    #[test]
3410    fn if_without_else() {
3411        assert_eq!(q("if a\n  1\nend"), "(if a 1)");
3412    }
3413
3414    /// `unless` lowers to `(if (not c) …)` — one tree per meaning, so
3415    /// every downstream tool sees exactly one shape.
3416    #[test]
3417    fn unless_is_a_negated_if() {
3418        assert_eq!(q("unless a\n  1\nend"), "(if (not a) 1)");
3419    }
3420
3421    #[test]
3422    fn multi_statement_body_becomes_begin() {
3423        assert_eq!(q("if a\n  1\n  2\nend"), "(if a (begin 1 2))");
3424    }
3425
3426    #[test]
3427    fn def_lowers_to_define() {
3428        assert_eq!(
3429            q("def add(a, b)\n  a + b\nend"),
3430            "(define (add a b) (+ a b))"
3431        );
3432    }
3433
3434    #[test]
3435    fn def_with_no_params() {
3436        assert_eq!(q("def zero()\n  0\nend"), "(define (zero) 0)");
3437    }
3438
3439    fn refused(src: &str) -> String {
3440        match parse_expr(src) {
3441            Ok(s) => panic!("{src:?} should be refused, parsed to {s}"),
3442            Err(e) => e.to_string(),
3443        }
3444    }
3445
3446    #[test]
3447    fn a_question_mark_parameter_is_optional() {
3448        assert_eq!(q("def f(a, b?)\n  b\nend"), "(define (f a &optional b) b)");
3449        assert_eq!(q("def f(a?, b?)\n  a\nend"), "(define (f &optional a b) a)");
3450        assert_eq!(q("def f(a?)\n  a\nend"), "(define (f &optional a) a)");
3451    }
3452
3453    #[test]
3454    fn an_annotated_optional_parameter_keeps_its_type_after_the_marker() {
3455        assert_eq!(
3456            q("def f(a: Int, b?: Str) -> Int\n  a\nend"),
3457            "(define-typed (f (a Int) &optional (b Str)) Int a)"
3458        );
3459        assert_eq!(
3460            q("def f(a, b?: Str)\n  a\nend"),
3461            "(define-typed (f (a dyn) &optional (b Str)) dyn a)"
3462        );
3463    }
3464
3465    #[test]
3466    fn a_def_without_question_marks_carries_no_marker() {
3467        assert_eq!(q("def f(a, b)\n  a\nend"), "(define (f a b) a)");
3468        assert_eq!(q("def f(a, *r)\n  a\nend"), "(define (f a &rest r) a)");
3469    }
3470
3471    #[test]
3472    fn a_required_parameter_cannot_follow_an_optional_one() {
3473        assert!(refused("def f(a?, b)\n  a\nend").contains("`b` cannot follow an optional"));
3474        assert!(refused("def f(a, b?, c: Int)\n  a\nend").contains("`c` cannot follow"));
3475    }
3476
3477    #[test]
3478    fn a_rest_parameter_cannot_follow_an_optional_one() {
3479        assert!(refused("def f(a, b?, *r)\n  a\nend").contains("rest parameter cannot follow"));
3480    }
3481
3482    // ---- literals ----------------------------------------------------
3483
3484    #[test]
3485    fn literals_lower_to_atoms() {
3486        assert_eq!(q("42"), "42");
3487        assert_eq!(q("true"), "#t");
3488        assert_eq!(q(":ok"), ":ok");
3489        assert_eq!(q(r#""hi""#), r#""hi""#);
3490    }
3491
3492    #[test]
3493    fn unary_minus_and_not() {
3494        assert_eq!(q("-x"), "(- 0 x)");
3495        assert_eq!(q("!x"), "(not x)");
3496    }
3497
3498    // ---- programs and errors -----------------------------------------
3499
3500    #[test]
3501    fn a_program_is_a_sequence_of_forms() {
3502        let forms = parse_program("def f()\n  1\nend\nf()").expect("parse");
3503        assert_eq!(forms.len(), 2);
3504        assert_eq!(forms[1].to_string(), "(f)");
3505    }
3506
3507    #[test]
3508    fn unterminated_block_is_an_error_naming_what_was_expected() {
3509        let e = parse_program("if a\n  1").expect_err("must fail");
3510        assert!(e.message.contains("unterminated"), "{}", e.message);
3511    }
3512
3513    #[test]
3514    fn a_parse_error_carries_a_span_into_the_source() {
3515        let src = "1 + )";
3516        let e = parse_program(src).expect_err("must fail");
3517        assert!(e.span.start < src.len(), "span {:?} outside source", e.span);
3518    }
3519
3520    // ---- one statement per line -------------------------------------
3521
3522    /// `y = xs[0]` was TWO statements, `(define y xs)` and `(list 0)`: nothing
3523    /// required a separator between statements, so the parser finished `xs`,
3524    /// saw a list literal and started a new statement on the same line. The
3525    /// program ran and `y` was the whole list. Found by the 2026-09-29 audit.
3526    #[test]
3527    fn a_spaced_index_is_refused_naming_the_touching_one() {
3528        let e = parse_program("y = xs [0]").expect_err("must not split into two statements");
3529        assert!(e.message.contains("`xs[0]`"), "{}", e.message);
3530        assert_eq!((e.line, e.col), (1, 8), "{e}");
3531    }
3532
3533    #[test]
3534    fn an_index_is_the_tree_of_get_or_nth() {
3535        for (short, long) in [
3536            ("m[:k]", "get(m, :k)"),
3537            (r#"m["k"]"#, r#"get(m, "k")"#),
3538            ("xs[0]", "nth(0, xs)"),
3539            ("xs[i + 1]", "nth(i + 1, xs)"),
3540            ("f(x)[2]", "nth(2, f(x))"),
3541            ("m[:a][:b]", "get(get(m, :a), :b)"),
3542            ("xs[0][:k]", "get(nth(0, xs), :k)"),
3543            ("[1, 2][1]", "nth(1, [1, 2])"),
3544        ] {
3545            assert_eq!(q(short), q(long), "{short}");
3546        }
3547        assert_eq!(q("get(m, k)"), "(get m k)");
3548        let notes = spelling_notes("get(m, :k)\nnth(0, xs)\nget(m, k)\n").unwrap();
3549        assert_eq!(
3550            notes
3551                .iter()
3552                .filter(|n| n.spelling == Spelling::Index)
3553                .count(),
3554            2
3555        );
3556    }
3557
3558    /// `a = f x` — Ruby's command call — parsed as `a = f` and then `x`.
3559    #[test]
3560    fn a_call_without_parentheses_is_refused_naming_the_call() {
3561        let e = parse_program("a = f x").expect_err("must not split into two statements");
3562        assert!(e.message.contains("`f(x)`"), "{}", e.message);
3563        assert_eq!((e.line, e.col), (1, 7), "{e}");
3564    }
3565
3566    /// The same rule inside a block body, and in every body shape.
3567    #[test]
3568    fn two_statements_on_one_line_are_refused_in_a_body() {
3569        for src in [
3570            "def f(xs)\n  xs [0]\nend",
3571            "if a\n  f x\nend",
3572            "fn(x) x 1 end",
3573            "test \"t\"\n  assert a b\nend",
3574            "1 2",
3575        ] {
3576            assert!(parse_program(src).is_err(), "{src:?} must not parse");
3577        }
3578    }
3579
3580    /// `(x) -> e` is the inline spelling of `fn(x) e end`: one tree.
3581    #[test]
3582    fn an_arrow_lambda_lowers_to_the_same_tree_as_fn() {
3583        for (arrow, block) in [
3584            ("(x) -> x * 2", "fn(x) x * 2 end"),
3585            ("(a, b) -> a + b", "fn(a, b) a + b end"),
3586            ("() -> 1", "fn() 1 end"),
3587            ("map(xs, (x) -> x + 1)", "map(xs, fn(x) x + 1 end)"),
3588        ] {
3589            let a = parse_program(arrow).expect(arrow);
3590            let b = parse_program(block).expect(block);
3591            assert_eq!(format!("{a:?}"), format!("{b:?}"), "{arrow}");
3592        }
3593        assert_eq!(
3594            parse_program("(x)").map(|p| p.len()).ok(),
3595            Some(1),
3596            "a parenthesised name is still a group"
3597        );
3598    }
3599
3600    /// The controls: every place a statement legitimately ends on its line.
3601    /// A block keyword closing the body on the same line is one of them, which
3602    /// is what keeps a one-line lambda one line.
3603    #[test]
3604    fn a_statement_may_end_at_a_newline_the_end_or_a_closing_keyword() {
3605        for src in [
3606            "fn(x) x + 1 end",
3607            "map(fn(x) x end, xs)",
3608            "if a\n  1\nelse\n  2\nend",
3609            "if a 1 else 2 end",
3610            "case x\nwhen 1 :a\nwhen 2 :b\nelse :c\nend",
3611            "x = 1\ny = 2",
3612            "x = 1",
3613        ] {
3614            assert!(
3615                parse_program(src).is_ok(),
3616                "{src:?}: {:?}",
3617                parse_program(src)
3618            );
3619        }
3620    }
3621
3622    /// `if c then a else b end` read `then` as a statement and `a` as another.
3623    #[test]
3624    fn then_is_refused_by_name() {
3625        let e = parse_program("x = if c then 1 else 2 end").expect_err("then is not a keyword");
3626        assert!(e.message.contains("`then`"), "{}", e.message);
3627    }
3628
3629    /// A brace block after a send is not a map literal the author meant.
3630    #[test]
3631    fn a_brace_block_is_refused_naming_fn() {
3632        let e = parse_program("xs.map { |x| x }").expect_err("no brace blocks");
3633        assert!(e.message.contains("fn(x)"), "{}", e.message);
3634    }
3635
3636    // ---- qualified names ------------------------------------------------
3637
3638    /// `retsu::first(xs)` is one symbol, tatara's `require` shape.
3639    ///
3640    /// Red run (2026-09-29), before the `::` token: `expected an expression,
3641    /// found `:``.
3642    #[test]
3643    fn a_qualified_name_is_one_symbol() {
3644        assert_eq!(q("retsu::first(xs)"), "(retsu/first xs)");
3645        assert_eq!(q("retsu::first"), "retsu/first");
3646        assert_eq!(q("xs |> retsu::first"), "(retsu/first xs)");
3647        assert_eq!(q("blue::length(xs)"), "(blue/length xs)");
3648    }
3649
3650    /// Whitespace around `::` is layout, not meaning.
3651    #[test]
3652    fn whitespace_around_the_qualifier_is_accepted() {
3653        assert_eq!(q("retsu :: first(xs)"), "(retsu/first xs)");
3654    }
3655
3656    /// After `::` a reserved word is a name: `kueri::if` is kueri's
3657    /// definition, not the `if` form.
3658    #[test]
3659    fn a_reserved_word_after_the_qualifier_is_a_name() {
3660        assert_eq!(q("kueri::if(a, b)"), "(kueri/if a b)");
3661        assert_eq!(q("heni::test(x)"), "(heni/test x)");
3662        assert_eq!(q("sabi::fn"), "sabi/fn");
3663    }
3664
3665    #[test]
3666    fn a_qualified_name_has_exactly_one_qualifier() {
3667        for (src, says) in [
3668            ("::first(xs)", "`package::name`"),
3669            ("a::b::c", "one qualifier"),
3670            ("if::x", "reserved word"),
3671            ("retsu::1", "expected a name after `retsu::`"),
3672        ] {
3673            let e = parse_program(src).expect_err(src);
3674            assert!(e.message.contains(says), "{src}: {}", e.message);
3675        }
3676    }
3677
3678    /// The one reader of the shape: division is not qualified, and neither
3679    /// is anything the surface could not have written.
3680    #[test]
3681    fn qualified_reads_only_the_package_name_shape() {
3682        assert_eq!(qualified("retsu/first"), Some(("retsu", "first")));
3683        assert_eq!(qualified("kueri/if"), Some(("kueri", "if")));
3684        assert_eq!(qualified("nisshi/pair?"), Some(("nisshi", "pair?")));
3685        for not in ["/", "a/", "/a", "a/b/c", "if/x", "hash-map", "a"] {
3686            assert_eq!(qualified(not), None, "{not}");
3687        }
3688        assert!(is_callable_name("kueri/if"));
3689    }
3690
3691    /// `:pair?` is the symbol `pair?`: an import list can name a predicate.
3692    ///
3693    /// Red run (2026-09-29), before: `unexpected character `?``, found by
3694    /// `blue migrate` listing nisshi's `pair?`.
3695    #[test]
3696    fn a_symbol_may_end_in_a_question_or_bang() {
3697        assert_eq!(q("[:pair?, :set!]"), "(list :pair? :set!)");
3698        assert_eq!(q("use(\"n\", [:empty?])"), "(use \"n\" (list :empty?))");
3699        assert_eq!(q(":a != :b"), "(not= :a :b)");
3700    }
3701
3702    // ---- errors a person can read -------------------------------------
3703
3704    /// `line:col` and blue's words, never `at 10..11` and `Ident("y")`.
3705    #[test]
3706    fn a_parse_error_displays_line_col_and_the_source_text() {
3707        let e = parse_program("x = 1\ny = [1, 2 y]").expect_err("must fail");
3708        assert_eq!(e.to_string(), "2:11: expected `,` or `]`, found `y`");
3709        assert_eq!(e.span, Span::new(16, 17), "the span stays, as data");
3710    }
3711
3712    /// Every token kind renders without Rust's `Debug` shape.
3713    #[test]
3714    fn no_error_message_names_a_rust_variant() {
3715        for src in [
3716            "f(1 2)",
3717            "{a: 1 b}",
3718            "def 1\nend",
3719            "x.1",
3720            "if a\n",
3721            "fn(1) 2 end",
3722            "[1 :b]",
3723            "1 +",
3724            "(1",
3725        ] {
3726            let e = parse_program(src).expect_err(src);
3727            let shown = e.to_string();
3728            for tell in [
3729                "Ident(", "Int(", "Op(", "Label(", "Sym(", "Str(", "LParen", "RBracket", "Eof",
3730                "Newline", "..",
3731            ] {
3732                assert!(!shown.contains(tell), "{src:?} -> {shown}");
3733            }
3734            assert!(e.line >= 1 && e.col >= 1, "{src:?} not located: {shown}");
3735        }
3736    }
3737
3738    /// A lex error reaches the author through the same door, located.
3739    #[test]
3740    fn a_lex_error_is_located_too() {
3741        let e = parse_program("x = 1\ny = 2; z = 3").expect_err("no semicolons");
3742        assert_eq!((e.line, e.col), (2, 6), "{e}");
3743        assert!(e.message.contains("new line"), "{e}");
3744    }
3745
3746    /// Anti-vacuity: `q` must be able to FAIL. If every input parsed, the
3747    /// assertions above would be worthless.
3748    #[test]
3749    fn the_parser_rejects_garbage() {
3750        assert!(parse_program("def").is_err());
3751        assert!(parse_program("(1").is_err());
3752        assert!(parse_program("end").is_err());
3753    }
3754}