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    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    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    PARSES.with(|n| n.set(n.get() + 1));
144    let mut p = Parser::over(src, lex(src).map_err(|e| ParseError::from(e).located(src))?, max_depth);
145    p.program().map_err(|e| e.located(src))
146}
147
148pub fn spelling_notes(src: &str) -> Result<Vec<Note>, ParseError> {
149    let mut p = Parser::over(src, lex(src).map_err(|e| ParseError::from(e).located(src))?, MAX_EXPR_DEPTH);
150    p.program().map_err(|e| e.located(src))?;
151    Ok(p.notes)
152}
153
154/// Parse a program written in a [`Yakugo`](crate::yakugo::Yakugo) surface.
155///
156/// The pack is applied to the TOKEN STREAM, between lexing and parsing, so the
157/// parser below is untouched and every keyword site works by construction.
158/// What comes out is the same `Sexp` the English surface produces — that is
159/// the invariant the pack tests assert, and the only thing that makes a
160/// surface a surface rather than a dialect.
161///
162/// # Errors
163///
164/// Lex and parse errors propagate unchanged. A pack cannot repair source that
165/// does not tokenize, and reporting otherwise would name the wrong problem.
166pub fn parse_program_in(src: &str, pack: &crate::yakugo::Yakugo) -> Result<Vec<Sexp>, ParseError> {
167    Ok(parse_program_tree_in(src, pack)?
168        .iter()
169        .map(Spanned::to_sexp)
170        .collect())
171}
172
173/// [`parse_program_in`] keeping **every node's** source span.
174///
175/// The same relation [`parse_program_tree`] has to [`parse_program`], and it
176/// exists for the same reason: a surface is a spelling, so a program written in
177/// one has positions exactly as real as an English-surface program's. Without
178/// this door, `pipeline::run_in_surface` would have to lift a spanless tree back
179/// up and every type error in a `yakugo` program would report `<synthetic>` —
180/// a position the parser HAD and threw away one line earlier, which is the
181/// class `Diagnostic::span`'s own docs name.
182///
183/// [`parse_program_in`] is now its projection, so the two cannot drift.
184///
185/// # Errors
186///
187/// As [`parse_program_in`].
188pub fn parse_program_tree_in(
189    src: &str,
190    pack: &crate::yakugo::Yakugo,
191) -> Result<Vec<Spanned>, ParseError> {
192    let toks = crate::yakugo::canonical_tokens(src, pack)
193        .map_err(|e| ParseError::from(e).located(src))?;
194    PARSES.with(|n| n.set(n.get() + 1));
195    let mut p = Parser::over(src, toks, MAX_EXPR_DEPTH);
196    p.program().map_err(|e| e.located(src))
197}
198
199/// [`parse_program`] with the nesting bound supplied by the caller.
200///
201/// The bound is a **safety limit, not a dialect**: `max_depth` cannot change
202/// what any program means, only whether a pathological one is refused before
203/// the stack is at risk. That is why it is a parameter here and
204/// [`MAX_EXPR_DEPTH`] is only the default — a knob that could alter meaning
205/// would belong nowhere near a config file (see `blue-lang-cli`'s `config`
206/// module for the rule this obeys).
207pub fn parse_program_with_depth(src: &str, max_depth: usize) -> Result<Vec<Sexp>, ParseError> {
208    Ok(parse_program_tree_with_depth(src, max_depth)?
209        .iter()
210        .map(Spanned::to_sexp)
211        .collect())
212}
213
214/// Parse, keeping each top-level form's source span.
215///
216/// The spans are what let the formatter put comments back. A comment is not
217/// part of a program's *meaning*, so it has no place in the `Sexp` tree —
218/// putting it there would break canonicality, since two programs differing only
219/// in a comment would stop formatting identically. Instead the formatter renders
220/// the tree and re-interleaves comments by position, which needs to know where
221/// each form started and ended.
222///
223/// A projection of [`parse_program_tree`], which carries a span on every node
224/// rather than only the top-level ones. This entry point survives because the
225/// formatter wants exactly the top-level pairing and nothing deeper.
226pub fn parse_program_spanned(src: &str) -> Result<Vec<(Sexp, Span)>, ParseError> {
227    parse_program_spanned_with_depth(src, MAX_EXPR_DEPTH)
228}
229
230/// [`parse_program_spanned`] with the nesting bound supplied by the caller.
231pub fn parse_program_spanned_with_depth(
232    src: &str,
233    max_depth: usize,
234) -> Result<Vec<(Sexp, Span)>, ParseError> {
235    Ok(parse_program_tree_with_depth(src, max_depth)?
236        .iter()
237        .map(|f| (f.to_sexp(), f.span))
238        .collect())
239}
240
241/// Every comment in `src`, with its byte span and whether it sits alone on its
242/// line.
243///
244/// `own_line` is the distinction that decides placement: a comment alone on its
245/// line belongs *before* the form that follows, while one after code on the same
246/// line belongs *to* that line. Conflating them moves a trailing note onto the
247/// wrong row.
248pub fn comments(src: &str) -> Vec<Comment> {
249    lex(src)
250        .map(|toks| {
251            toks.iter()
252                .filter_map(|t| match &t.kind {
253                    TokenKind::Comment(text) => Some(Comment {
254                        text: text.clone(),
255                        span: t.span,
256                        own_line: line_before_is_blank(src, t.span.start),
257                    }),
258                    _ => None,
259                })
260                .collect()
261        })
262        .unwrap_or_default()
263}
264
265/// A comment, and where it sat.
266#[derive(Clone, Debug, PartialEq, Eq)]
267pub struct Comment {
268    /// The text, including the leading `#`.
269    pub text: String,
270    pub span: Span,
271    /// Nothing but whitespace precedes it on its line.
272    pub own_line: bool,
273}
274
275fn line_before_is_blank(src: &str, start: usize) -> bool {
276    src[..start.min(src.len())]
277        .rsplit('\n')
278        .next()
279        .is_some_and(|prefix| prefix.trim().is_empty())
280}
281
282/// Parse a single blue expression. Convenience for tests and the REPL.
283pub fn parse_expr(src: &str) -> Result<Sexp, ParseError> {
284    let forms = parse_program(src)?;
285    match forms.len() {
286        1 => Ok(forms.into_iter().next().expect("checked len")),
287        n => Err(ParseError::new(
288            format!("expected exactly one expression, found {n}"),
289            Span::new(0, src.len()),
290        )
291        .located(src)),
292    }
293}
294
295/// What an infix operator binds like, and what it lowers to.
296///
297/// **One row per operator, carrying both facts.** Precedence and callee
298/// used to live apart, and the cost was immediate: the surface spelling was
299/// emitted verbatim, so `a == b` lowered to `(== a b)` — a symbol no
300/// interpreter binds — and the program died at *runtime* with `unbound
301/// symbol ==`. Splitting the two invites exactly that: add an operator to
302/// the precedence table, forget the lowering, ship a parse that cannot run.
303/// Joined here, "has a precedence" and "has a callee" are the same fact.
304#[derive(Clone, Copy, Debug)]
305pub struct Infix {
306    /// Surface spelling.
307    pub op: &'static str,
308    /// Left and right binding power. Higher binds tighter.
309    pub power: (u8, u8),
310    /// The tatara-lisp callee this lowers to.
311    pub callee: &'static str,
312    /// One line for a reader: what the operator computes, and the trap in it
313    /// when there is one. `blue reference` prints it; a row without one fails
314    /// `every_operator_and_keyword_is_described`.
315    pub doc: &'static str,
316}
317
318/// The complete infix table. Precedence follows Ruby's where Ruby has an
319/// opinion; `|>` (below) sits under everything so `a |> f |> g` chains
320/// without parentheses.
321pub const INFIX: &[Infix] = &[
322    Infix {
323        op: "or",
324        power: (1, 2),
325        callee: "or",
326        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).",
327    },
328    Infix {
329        op: "and",
330        power: (3, 4),
331        callee: "and",
332        doc: "Logical and: the right value if the left is truthy, otherwise the left; short-circuits. `&&` is read as `and` and written `and` (B0025).",
333    },
334    // `==` is STRUCTURAL equality, so it lowers to `equal?` and not to `=`.
335    //
336    // tatara's `=` is NUMERIC comparison: `"a" = "a"` is a type error, not
337    // false. Lowering `==` to it meant every string, list or nil comparison
338    // failed with "expected number, got string" — found by blue's own spec
339    // suite the moment a test compared two strings, which is the first thing
340    // anybody does.
341    //
342    // `equal?` is structural and total over the value domain: strings, lists
343    // and nil all compare, and numbers still compare as numbers.
344    Infix {
345        op: "==",
346        power: (5, 6),
347        callee: "equal?",
348        doc: "Structural equality over every value (okite D0001): 4 == 4.0, lists and maps by content, nil only equals nil.",
349    },
350    Infix {
351        op: "!=",
352        power: (5, 6),
353        callee: "not=",
354        doc: "Exactly not(a == b) (okite D0002).",
355    },
356    Infix {
357        op: "<",
358        power: (5, 6),
359        callee: "<",
360        doc: "Numeric less-than. A string operand is a type error: order text with compare(a, b).",
361    },
362    Infix {
363        op: "<=",
364        power: (5, 6),
365        callee: "<=",
366        doc: "Numeric less-than-or-equal. Numbers only, like <.",
367    },
368    Infix {
369        op: ">",
370        power: (5, 6),
371        callee: ">",
372        doc: "Numeric greater-than. Numbers only, like <.",
373    },
374    Infix {
375        op: ">=",
376        power: (5, 6),
377        callee: ">=",
378        doc: "Numeric greater-than-or-equal. Numbers only, like <.",
379    },
380    Infix {
381        op: "+",
382        power: (7, 8),
383        callee: "+",
384        doc: "Numeric addition; an Int and a Float give a Float. Not string concatenation: interpolate instead.",
385    },
386    Infix {
387        op: "-",
388        power: (7, 8),
389        callee: "-",
390        doc: "Numeric subtraction. Written before an operand with nothing on its left, it is unary minus: -n.",
391    },
392    Infix {
393        op: "*",
394        power: (9, 10),
395        callee: "*",
396        doc: "Numeric multiplication.",
397    },
398    Infix {
399        op: "/",
400        power: (9, 10),
401        callee: "/",
402        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).",
403    },
404    Infix {
405        op: "%",
406        power: (9, 10),
407        callee: "mod",
408        doc: "Euclidean remainder: never negative, so (0 - 7) % 3 is 2, not -1. A zero divisor is a runtime error.",
409    },
410];
411
412/// Every surface keyword that BEGINS an expression.
413///
414/// Exists so the formatter's corpus can be checked against it. Three separate
415/// times a form was added to this parser, the formatter was not extended, and
416/// all three formatting laws still passed — because the corpus is
417/// hand-maintained and contained no example of the new form. The annotated
418/// `def` rendered as a method send; `defmacro` rendered as
419/// `defmacro(double, x(), …)`, which does not re-parse at all.
420///
421/// A law cannot notice a case nobody wrote down. `blue-lang-fmt`'s
422/// `every_surface_keyword_appears_in_the_corpus` closes that by making the
423/// omission itself the failure, so adding a keyword here forces the corpus
424/// entry that exercises the other three laws over it.
425///
426/// `do` and `end` are absent deliberately: they are block delimiters, not
427/// expression heads, and have no standalone rendering to test.
428/// The callee `assert e` lowers to.
429///
430/// **Owned here, by the lowering itself, and read by every consumer.** It was
431/// briefly a literal here and a separate `const` in `blue-lang-test`, and the
432/// shadowing gate then could not see a parser that lowered to the wrong name:
433/// the gate checked its own copy. Same duplication class as the operator table.
434pub const LOWERED_ASSERT: &str = "blue-assert";
435
436/// The callee a `{...}` map literal lowers to.
437///
438/// **`hash-map`, not `map`.** tatara binds `map` to the higher-order function,
439/// so a literal lowering to `(map …)` called the HOF with the key/value pairs
440/// as arguments and failed with "expected list, got int". Same shadowing class
441/// as [`LOWERED_ASSERT`], caught by the same gate once the name was listed
442/// there — it was not, which is why this shipped.
443pub const LOWERED_MAP: &str = "hash-map";
444
445/// The callee string interpolation lowers to.
446///
447/// blue's own `concat`, which renders either side through `to_s` — that is what
448/// lets `"n=#{42}"` interpolate a number. Not `+`, which is arithmetic.
449pub const LOWERED_CONCAT: &str = "concat";
450
451pub const LOWERED_FIELD: &str = "blue-field";
452
453pub const LOWERED_ERROR_MATCH: &str = "blue-rescues?";
454
455pub const LOWERED_ENSURE: &str = "blue-ensure";
456
457pub const LOWERED_RERAISE: &str = "blue-reraise";
458
459pub const LOWERED_ATTEMPTED: &str = "blue-attempted";
460
461pub const LOWERED_FAILED: &str = "blue-failed";
462
463pub const RESCUED: &str = "rescued-error";
464
465pub const SURFACE_KEYWORDS: &[&str] = &[
466    "if",
467    "begin",
468    "unless",
469    "def",
470    "defmacro",
471    "quote",
472    "unquote",
473    "unquote_splice",
474    "test",
475    "assert",
476    "fn",
477    "case",
478];
479
480/// The words that are reserved but are not heads of a `SURFACE_KEYWORDS` form
481/// — block delimiters and the three literals.
482///
483/// Split out as data rather than inlined in a `matches!` because a reserved
484/// word is not only the parser's business: a highlighter has to paint exactly
485/// this set, and one that reads `SURFACE_KEYWORDS` alone silently under-paints
486/// `do` / `end` / `else` / `elsif`. That was not hypothetical — escriba's
487/// `escriba-render/src/langs.rs` hand-transcribed "SURFACE_KEYWORDS plus the
488/// four block words `is_reserved_word` adds", because the union was
489/// unreachable from outside this module. A transcription is a second
490/// definition, and it drifts the first time this list changes.
491pub const BLOCK_KEYWORDS: &[&str] = &[
492    "do", "end", "else", "elsif", "rescue", "ensure", "true", "false", "nil",
493];
494
495pub const OPERATOR_KEYWORDS: &[&str] = &["and", "or", "not"];
496
497/// A surface keyword may not be rebound. `if = 1` is a mistake, not a binding,
498/// and letting it through would shadow the form for the rest of the file.
499///
500/// Public because it is the ONE answer to "is this identifier a keyword" —
501/// the parser's rebinding check and every downstream highlighter read it here
502/// rather than each carrying its own copy of the list.
503pub fn is_reserved_word(name: &str) -> bool {
504    SURFACE_KEYWORDS.contains(&name)
505        || BLOCK_KEYWORDS.contains(&name)
506        || OPERATOR_KEYWORDS.contains(&name)
507}
508
509/// Can `name` be written as the head of a call — `name(…)`?
510///
511/// True exactly when the lexer reads `name` as one identifier and it is not
512/// reserved. Answered BY the lexer rather than by a character class copied
513/// from it, so the two cannot disagree: the formatter asks this before
514/// printing a tree as `name(args)`, and a head such as `hash-map`, `not=` or
515/// `if` — which a tree can carry and a call cannot spell — must get a
516/// different rendering or none.
517pub fn is_callable_name(name: &str) -> bool {
518    // `retsu/first` is written `retsu::first(…)`.
519    if qualified(name).is_some() {
520        return true;
521    }
522    !is_reserved_word(name)
523        && matches!(
524            lex(name).as_deref(),
525            Ok([t, e]) if matches!(&t.kind, TokenKind::Ident(n) if n == name)
526                && e.kind == TokenKind::Eof
527        )
528}
529
530/// The character a qualified name's AST symbol joins its two halves with:
531/// `retsu::first` is the symbol `retsu/first`. The surface never writes it
532/// (`/` is division, and no identifier can contain it), which is what makes
533/// a symbol with text on both sides of one `/` unambiguously qualified.
534pub const QUALIFIER: char = '/';
535
536/// The qualifier that names a builtin: `blue::count` is the interpreter's
537/// `count`, whatever a program defines. No package may be named it.
538pub const BUILTIN_QUALIFIER: &str = "blue";
539
540/// The AST symbol for `pkg::name`.
541#[must_use]
542pub fn qualify(pkg: &str, name: &str) -> String {
543    format!("{pkg}{QUALIFIER}{name}")
544}
545
546/// `retsu/first` → `Some(("retsu", "first"))`: the package and the name of a
547/// qualified symbol. `None` for every other symbol, `/` (division) included.
548///
549/// The ONE reader of the shape, so the formatter, the name table and the
550/// runtime's renderer cannot disagree about what counts as qualified.
551#[must_use]
552pub fn qualified(sym: &str) -> Option<(&str, &str)> {
553    let (pkg, name) = sym.split_once(QUALIFIER)?;
554    let ident = |s: &str| {
555        matches!(
556            lex(s).as_deref(),
557            Ok([t, e]) if matches!(&t.kind, TokenKind::Ident(n) if n == s) && e.kind == TokenKind::Eof
558        )
559    };
560    let named = |s: &str| ident(s) || matches!(s, "true" | "false" | "nil");
561    (ident(pkg) && !is_reserved_word(pkg) && named(name)).then_some((pkg, name))
562}
563
564/// One line for a reader on each reserved word: what it writes, and the shape.
565///
566/// Beside the two tables rather than inside them, because both are `&[&str]`
567/// read by the formatter's corpus gate and by highlighters outside this repo;
568/// changing their type would break every one of those readers.
569/// `every_operator_and_keyword_is_described` makes a word without a line here a
570/// red build, so the omission cannot ship. `blue reference` prints these.
571#[must_use]
572pub fn keyword_doc(word: &str) -> &'static str {
573    match word {
574        "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).",
575        "unless" => "`unless c … end` is read as `if not c` and written that way (B0026).",
576        "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`.",
577        "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 `|>`.",
578        "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.",
579        "and" => "`a and b`: logical and; `&&` is read as `and` and written `and` (B0025).",
580        "or" => "`a or b`: logical or; `||` is read as `or` and written `or` (B0025).",
581        "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).",
582        "def" => "`def name(a, b) … end`: a named function; its value is the last expression. Parameters may be typed: `def f(n: Int) -> Int`.",
583        "defmacro" => "`defmacro name(x) quote … unquote(x) … end end`: a macro, a rewrite of the unevaluated form.",
584        "quote" => "`quote … end`: the form itself as data, not its value. Used inside `defmacro`.",
585        "unquote" => "`unquote(x)`: inside `quote`, splice in the value of `x`.",
586        "unquote_splice" => "`unquote_splice(xs)`: inside `quote`, splice the items of the form `xs` in place, not the form itself.",
587        "test" => "`test \"what must hold\" … end`: a test block, run by `blue test` and ignored by `blue run`.",
588        "assert" => "`assert expr`: inside a test, fail it unless `expr` is truthy; the failure quotes the expression.",
589        "fn" => "`fn(x) x * 2 end`: an anonymous function, a value. Blue has no brace or do blocks.",
590        "case" => "`case v when 1 … when 2 … else … end`: compares with `==`; it does not destructure.",
591        "do" => "Reserved and refused: blue has no do-blocks. Pass `fn(x) … end` instead.",
592        "end" => "Closes `def`, `defmacro`, `if`, `case`, `fn`, `test` and `quote`.",
593        "else" => "The branch of `if` or `case` taken when nothing before it matched.",
594        "elsif" => "A further condition in an `if` chain; an `else` holding only an `if` is written `elsif` (B0027).",
595        "true" => "The boolean true.",
596        "false" => "The boolean false. Only `false` and `nil` are falsy; 0, \"\" and [] are truthy.",
597        "nil" => "The absent value. It is not `[]` (okite D0004), and only `nil == nil`.",
598        _ => "",
599    }
600}
601
602fn infix(op: &str) -> Option<&'static Infix> {
603    INFIX.iter().find(|i| i.op == op)
604}
605
606pub const SYMBOL_SPELLINGS: &[(&str, &str)] = &[("&&", "and"), ("||", "or"), ("!", "not")];
607
608fn word_for(op: &str) -> &str {
609    SYMBOL_SPELLINGS
610        .iter()
611        .find(|(sym, _)| *sym == op)
612        .map_or(op, |(_, word)| word)
613}
614
615fn infix_of(kind: &TokenKind) -> Option<&'static Infix> {
616    match kind {
617        TokenKind::Op(o) => infix(word_for(o)),
618        TokenKind::Ident(w) if w == "and" || w == "or" => infix(w),
619        _ => None,
620    }
621}
622
623const PIPE_POWER: (u8, u8) = (0, 1);
624const RESCUE_POWER: (u8, u8) = (0, 1);
625
626struct Parser {
627    toks: Vec<Token>,
628    pos: usize,
629    /// Current expression-nesting depth, bounded by [`Self::max_depth`].
630    ///
631    /// Without this the parser does not fail on deep input — it **aborts the
632    /// process** with a stack overflow (SIGABRT), which `catch_unwind` cannot
633    /// catch. Measured 2026-08-01: `"(".repeat(2_000)` killed the test runner
634    /// outright. Every consumer inherited it — an LSP parsing a half-typed
635    /// line, a formatter, and shikumi loading a `.b` config off disk.
636    depth: usize,
637    /// The bound [`Self::depth`] is checked against.
638    ///
639    /// Defaults to [`MAX_EXPR_DEPTH`] on every entry point that does not name
640    /// one; `parse_program_with_depth` exists so an operator can raise it
641    /// without recompiling. It is a **bound**, so raising it changes no
642    /// program's meaning — only which pathological inputs are refused.
643    max_depth: usize,
644    src: String,
645    comment_spans: Vec<Span>,
646    notes: Vec<Note>,
647}
648
649/// Maximum expression nesting before the parser refuses.
650///
651/// Chosen well above anything human-written (blue's own `spec/*.b` peaks in
652/// single digits) and far below the measured overflow point, so the bound is
653/// hit as a typed `Err` long before the stack is at risk. A limit that is
654/// merely *near* the crash point is not a safety bound; it is a race.
655pub const MAX_EXPR_DEPTH: usize = 256;
656
657/// The parser's internal error: a [`ParseError`] behind a pointer.
658///
659/// Every production returns a `Result`, and a debug build keeps several per
660/// frame down the recursion, so the error's size is paid at every level of
661/// nesting whether or not anything fails. Measured 2026-09-29 on a 2 MiB
662/// debug thread, parsing `(((…1…)))` with no bound: the unboxed 40-byte error
663/// overflowed between 255 and 260 levels, barely past [`MAX_EXPR_DEPTH`];
664/// adding `line`/`col` unboxed moved it to 240–250, under the bound, and
665/// `depth_beyond_the_limit_is_an_err_not_an_abort` aborted. Boxed, the
666/// overflow is between 300 and 310. The error path is cold; the pointer is
667/// what keeps the bound a bound.
668type Fail = Box<ParseError>;
669
670fn fail(message: impl Into<String>, span: Span) -> Fail {
671    Box::new(ParseError::new(message, span))
672}
673
674impl Parser {
675    fn over(src: &str, all: Vec<Token>, max_depth: usize) -> Self {
676        let comment_spans = all
677            .iter()
678            .filter(|t| matches!(t.kind, TokenKind::Comment(_)))
679            .map(|t| t.span)
680            .collect();
681        let toks = all
682            .into_iter()
683            .filter(|t| !matches!(t.kind, TokenKind::Comment(_)))
684            .collect();
685        Parser {
686            toks,
687            pos: 0,
688            depth: 0,
689            max_depth,
690            src: src.to_string(),
691            comment_spans,
692            notes: Vec::new(),
693        }
694    }
695
696    fn note(&mut self, spelling: Spelling, span: Span, written: &str, canonical: &str) {
697        self.notes.push(Note {
698            spelling,
699            span,
700            written: written.to_string(),
701            canonical: canonical.to_string(),
702        });
703    }
704
705    fn text(&self, span: Span) -> &str {
706        self.src.get(span.start..span.end).unwrap_or("")
707    }
708
709    fn comment_within(&self, lo: usize, hi: usize) -> bool {
710        self.comment_spans.iter().any(|c| c.start >= lo && c.start < hi)
711    }
712
713    fn touches_previous(&self) -> bool {
714        let here = self.peek_span();
715        self.toks
716            .get(self.pos.wrapping_sub(1))
717            .is_some_and(|prev| prev.span.end == here.start)
718    }
719
720    fn peek(&self) -> &TokenKind {
721        &self.toks[self.pos.min(self.toks.len() - 1)].kind
722    }
723
724    fn peek_span(&self) -> Span {
725        self.toks[self.pos.min(self.toks.len() - 1)].span
726    }
727
728    fn bump(&mut self) -> TokenKind {
729        let k = self.toks[self.pos.min(self.toks.len() - 1)].kind.clone();
730        if self.pos < self.toks.len() {
731            self.pos += 1;
732        }
733        k
734    }
735
736    fn at(&self, k: &TokenKind) -> bool {
737        self.peek() == k
738    }
739
740    fn eat(&mut self, k: &TokenKind) -> bool {
741        if self.at(k) {
742            self.bump();
743            true
744        } else {
745            false
746        }
747    }
748
749    fn expect(&mut self, k: &TokenKind, what: &str) -> Result<(), Fail> {
750        if self.eat(k) {
751            Ok(())
752        } else {
753            Err(self.error(format!("expected {what}, found {}", self.peek())))
754        }
755    }
756
757    fn error(&self, message: impl Into<String>) -> Fail {
758        fail(message, self.peek_span())
759    }
760
761    /// Skip statement separators (newlines and semicolon-free layout).
762    fn skip_newlines(&mut self) {
763        while matches!(self.peek(), TokenKind::Newline) {
764            self.bump();
765        }
766    }
767
768    fn at_ident(&self, name: &str) -> bool {
769        matches!(self.peek(), TokenKind::Ident(n) if n == name)
770    }
771
772    /// Byte offset where the next token begins — the anchor a production spans
773    /// *from*. Paired with [`Self::span_since`].
774    fn mark(&self) -> usize {
775        self.peek_span().start
776    }
777
778    /// The span from `start` to the end of the **last consumed** token.
779    ///
780    /// `pos` has already advanced past that token, so this looks one back. Call
781    /// it after a production has consumed everything it owns; calling it early
782    /// yields a span that stops short of the node it describes.
783    fn span_since(&self, start: usize) -> Span {
784        let end = self
785            .toks
786            .get(self.pos.saturating_sub(1))
787            .map_or(start, |t| t.span.end);
788        Span::new(start, end.max(start))
789    }
790
791    fn program(&mut self) -> Result<Vec<Spanned>, Fail> {
792        let mut out = Vec::new();
793        loop {
794            self.skip_newlines();
795            if matches!(self.peek(), TokenKind::Eof) {
796                break;
797            }
798            out.push(self.statement()?);
799            self.end_of_statement(&[])?;
800        }
801        Ok(out)
802    }
803
804    /// **A statement ends at the end of its line.** Refuse anything else.
805    ///
806    /// The lexer has always said newlines "are statement separators", and the
807    /// parser never asked for one: `program` and `body` looped over
808    /// `statement()` and started the next one wherever the last stopped. So a
809    /// form blue does not have became two forms it does, silently —
810    /// `y = xs[0]` parsed as `y = xs` then the list `[0]`, and Ruby's
811    /// `a = f x` as `a = f` then `x`. Both ran; both bound the wrong value.
812    ///
813    /// **Refused rather than given a meaning**, because each would be a second
814    /// spelling of a tree blue already spells one way (§0's FORM axis):
815    /// indexing is the call `nth(i, xs)`, and a call has parentheses. `;` is
816    /// refused by the lexer for the same reason — one separator, the newline.
817    ///
818    /// What may follow a statement: a newline, the end of the file, or — in a
819    /// body — the keyword that closes it, which is what keeps
820    /// `fn(x) x + 1 end` a one-line lambda.
821    fn end_of_statement(&self, terminators: &[&str]) -> Result<(), Fail> {
822        let next = self.peek();
823        if matches!(next, TokenKind::Newline | TokenKind::Eof)
824            || terminators.iter().any(|t| self.at_ident(t))
825        {
826            return Ok(());
827        }
828        let prev = self.toks.get(self.pos.wrapping_sub(1)).map(|t| &t.kind);
829        let prev_name = match prev {
830            Some(TokenKind::Ident(n)) => Some(n.as_str()),
831            _ => None,
832        };
833        let callee = prev_name.unwrap_or("f");
834        let why = match (prev, next) {
835            (Some(TokenKind::Ident(t)), _) if t == "then" => {
836                "blue's `if` has no `then`: put the branch on the next line".to_string()
837            }
838            (_, TokenKind::Ident(d)) if d == "do" => "blue has no `do` blocks: pass a function \
839                 as an argument, `f(xs, fn(x) … end)`"
840                .to_string(),
841            (_, TokenKind::LBracket) => {
842                let index = match (self.toks.get(self.pos + 1), self.toks.get(self.pos + 2)) {
843                    (Some(i), Some(close)) if close.kind == TokenKind::RBracket => match &i.kind {
844                        TokenKind::Int(n) => n.to_string(),
845                        TokenKind::Ident(n) => n.clone(),
846                        _ => "i".to_string(),
847                    },
848                    _ => "i".to_string(),
849                };
850                let list = prev_name.unwrap_or("xs");
851                format!(
852                    "blue has no `xs[i]` indexing: write `nth({index}, {list})` \
853                     (or `first({list})`, `last({list})`)"
854                )
855            }
856            (_, TokenKind::LBrace) => "blue has no brace blocks: pass a function as an \
857                 argument, `map(xs, fn(x) … end)`"
858                .to_string(),
859            (
860                Some(TokenKind::Ident(_)),
861                TokenKind::Ident(_)
862                | TokenKind::Int(_)
863                | TokenKind::Float(_)
864                | TokenKind::Str(_)
865                | TokenKind::InterpolatedStr { .. }
866                | TokenKind::Sym(_)
867                | TokenKind::True
868                | TokenKind::False
869                | TokenKind::Nil,
870            ) => {
871                let arg = match next {
872                    TokenKind::Ident(a) => a.clone(),
873                    _ => "x".to_string(),
874                };
875                format!("a call needs parentheses: write `{callee}({arg})`")
876            }
877            _ => "put each statement on its own line".to_string(),
878        };
879        Err(self.error(format!("expected the end of the line, found {next}: {why}")))
880    }
881
882    /// A statement: either a binding or an expression.
883    ///
884    /// `x = 5` lowers to `(define x 5)`. Blue had NO way to name a value — a
885    /// capability probe found `x = 5` was a parse error, which makes every
886    /// program a single expression. That is more fundamental than anything else
887    /// the probe found.
888    ///
889    /// Only at STATEMENT position, never inside an expression, so `f(x = 1)` is
890    /// still an error rather than a silent binding. Ruby allows assignment as an
891    /// expression and it is a well-known footgun — `if x = 1` where `==` was
892    /// meant. Blue declines it, and the cost is only that a walrus-style idiom
893    /// has to be two lines.
894    fn statement(&mut self) -> Result<Spanned, Fail> {
895        // The SECOND recursion cycle, and it needs the same guard as `expr`.
896        //
897        // Guarding `expr` alone was not enough — measured 2026-08-01: with the
898        // expression bound in place, `"def a\n".repeat(2_000)` STILL aborted
899        // the process. Block nesting (`def` opening a body that contains more
900        // statements) recurses through here, not through `expr`, so a fix
901        // applied to one cycle silently left the other reachable. Two paths to
902        // the same crash; one guard covered one of them.
903        //
904        // Shares `self.depth` with `expr` on purpose: what the stack cares
905        // about is TOTAL nesting, not which grammar production produced it, so
906        // two independent counters would each permit their own full budget and
907        // together exceed what the stack can hold.
908        let max = self.max_depth;
909        if self.depth >= max {
910            return Err(self.error(format!(
911                "statement nests deeper than {max}; refusing to \
912                 recurse further (this is a limit, not a syntax error)"
913            )));
914        }
915        self.depth += 1;
916        let r = self.statement_inner();
917        self.depth -= 1;
918        r
919    }
920
921    fn statement_inner(&mut self) -> Result<Spanned, Fail> {
922        if let TokenKind::Ident(name) = self.peek().clone() {
923            if self.peek_at(1) == "=" && !is_reserved_word(&name) {
924                let name_span = self.peek_span();
925                self.bump(); // name
926                self.bump(); // =
927                self.skip_newlines();
928                let value = self.expr(0)?;
929                // `define` is synthesized — `x = 5` contains no such word — so
930                // it takes the span of the name being bound, which is what a
931                // reader looks at when told something about the binding.
932                return Ok(list_at(
933                    self.span_since(name_span.start),
934                    vec![sym_at(name_span, "define"), sym_at(name_span, &name), value],
935                ));
936            }
937        }
938        let e = self.expr(0)?;
939        if let SpannedForm::List(items) = &e.form {
940            if items.len() == 3
941                && items[0].as_symbol() == Some("define")
942                && items[1].as_symbol().is_some()
943                && self.text(items[0].span) == "define"
944            {
945                self.note(Spelling::OperatorSpelling, e.span, "define(x, v)", "x = v");
946            }
947        }
948        Ok(e)
949    }
950
951    /// The token `n` positions ahead, for the two-token lookahead a binding
952    /// needs. Returns `Eof` past the end rather than panicking.
953    fn peek_at(&self, n: usize) -> String {
954        match self.toks.get(self.pos + n).map(|t| &t.kind) {
955            Some(TokenKind::Op(o)) => o.clone(),
956            _ => String::new(),
957        }
958    }
959
960    /// Pratt loop.
961    fn expr(&mut self, min_bp: u8) -> Result<Spanned, Fail> {
962        // Depth guard at the single recursion cycle (`expr` -> `prefix` ->
963        // `expr`). Returning an Err here converts an UNRECOVERABLE abort into
964        // an ordinary parse failure a caller can render — the difference
965        // between an LSP showing a squiggle and an LSP being gone.
966        //
967        // The decrement is deliberately not RAII: every exit from this
968        // function is via `?` or a normal return, and both are covered by the
969        // explicit decrements below. A guard object would be tidier but would
970        // also hide the invariant this comment is here to state.
971        let max = self.max_depth;
972        if self.depth >= max {
973            return Err(self.error(format!(
974                "expression nests deeper than {max}; refusing to \
975                 recurse further (this is a limit, not a syntax error)"
976            )));
977        }
978        self.depth += 1;
979        let r = self.expr_inner(min_bp);
980        self.depth -= 1;
981        r
982    }
983
984    #[inline(never)]
985    fn rescue_node(&self, start: usize, op_span: Span, lhs: Spanned, fallback: Spanned) -> Spanned {
986        list_at(
987            self.span_since(start),
988            vec![
989                sym_at(op_span, "try"),
990                lhs,
991                list_at(
992                    op_span.merge(fallback.span),
993                    vec![
994                        sym_at(op_span, "catch"),
995                        list_at(op_span, vec![sym_at(op_span, "_")]),
996                        fallback,
997                    ],
998                ),
999            ],
1000        )
1001    }
1002
1003    #[inline(never)]
1004    fn infix_spelling(&mut self, callee: Option<&str>, op_span: Span) {
1005        let written = self.text(op_span).to_string();
1006        match (callee, self.peek().clone()) {
1007            (None, _) => self.note(Spelling::CallForm, op_span, "|>", "a call"),
1008            (Some(c), TokenKind::Op(o)) if word_for(&o) != o => {
1009                self.note(Spelling::LogicalSymbol, op_span, &written, c);
1010            }
1011            (Some(_), TokenKind::Op(o)) if written != o => {
1012                self.note(Spelling::OperatorSpelling, op_span, &written, &o);
1013            }
1014            _ => {}
1015        }
1016    }
1017
1018    #[inline(never)]
1019    fn infix_node(&mut self, start: usize, callee: Option<&str>, op_span: Span, lhs: Spanned, rhs: Spanned) -> Spanned {
1020        if callee == Some("-") && matches!(lhs.form, SpannedForm::Atom(Atom::Int(0))) {
1021            self.note(Spelling::OperatorSpelling, lhs.span.merge(op_span), "0 - x", "-x");
1022        }
1023
1024        if callee.is_none() {
1025            // `x |> f`      => (f x)
1026            // `x |> f(a)`   => (f x a)   — the pipeline threads into
1027            //                  the FIRST argument position, as Elixir's
1028            //                  does; that is what makes it composable.
1029            let rhs_span = rhs.span;
1030            let threaded = match rhs.form {
1031                SpannedForm::List(mut items) if !items.is_empty() => {
1032                    items.insert(1, lhs);
1033                    list_at(self.span_since(start), items)
1034                }
1035                callee => list_at(
1036                    self.span_since(start),
1037                    vec![Spanned::new(rhs_span, callee), lhs],
1038                ),
1039            };
1040            match &threaded.form {
1041                SpannedForm::List(items)
1042                    if items.len() == 2 && items[0].as_symbol() == Some("not") =>
1043                {
1044                    let (head, operand) = (items[0].span, items[1].clone());
1045                    self.negation(head, threaded.span, operand)
1046                }
1047                SpannedForm::List(items)
1048                    if items.len() == 4
1049                        && items[0].as_symbol() == Some("if")
1050                        && matches!(items[3].form, SpannedForm::Nil) =>
1051                {
1052                    list_at(threaded.span, items[..3].to_vec())
1053                }
1054                _ => threaded,
1055            }
1056        } else {
1057            // The callee symbol takes the OPERATOR's span, not the whole
1058            // expression's: `a + b` lowers to `(+ a b)` and the `+` node is
1059            // the one byte-range in the source that means it. Handing it the
1060            // whole expression's span would make a diagnostic about the
1061            // operator underline both operands too.
1062            list_at(
1063                self.span_since(start),
1064                vec![sym_at(op_span, callee.unwrap()), lhs, rhs],
1065            )
1066        }
1067    }
1068
1069    fn expr_inner(&mut self, min_bp: u8) -> Result<Spanned, Fail> {
1070        let start = self.mark();
1071        let mut lhs = self.prefix()?;
1072
1073        loop {
1074            // Postfix: `.name`, `.name(args)`, `(args)`
1075            match self.peek() {
1076                TokenKind::Dot => {
1077                    let dot = self.peek_span();
1078                    self.bump();
1079                    lhs = self.finish_send(start, lhs, dot)?;
1080                    continue;
1081                }
1082                TokenKind::LParen => {
1083                    let (args, splat) = self.call_args()?;
1084                    let call = self.call_node(self.span_since(start), vec![lhs], args, splat);
1085                    lhs = self.call_spelling(call);
1086                    continue;
1087                }
1088                _ => {}
1089            }
1090
1091            // Infix
1092            // `callee == None` marks the pipeline, which is a rewrite rather
1093            // than a call.
1094            let op_span = self.peek_span();
1095            if self.at_ident("rescue") {
1096                if RESCUE_POWER.0 < min_bp {
1097                    break;
1098                }
1099                self.bump();
1100                self.skip_newlines();
1101                let fallback = self.expr(RESCUE_POWER.1)?;
1102                lhs = self.rescue_node(start, op_span, lhs, fallback);
1103                continue;
1104            }
1105            let (callee, (lbp, rbp)) = match self.peek() {
1106                TokenKind::Pipe => (None, PIPE_POWER),
1107                other => match infix_of(other) {
1108                    Some(i) => (Some(i.callee), i.power),
1109                    None => break,
1110                },
1111            };
1112            if lbp < min_bp {
1113                break;
1114            }
1115            self.infix_spelling(callee, op_span);
1116            self.bump();
1117            self.skip_newlines();
1118            let rhs = self.expr(rbp)?;
1119            lhs = self.infix_node(start, callee, op_span, lhs, rhs);
1120        }
1121
1122        Ok(lhs)
1123    }
1124
1125    #[inline(never)]
1126    fn interpolation(span: Span, parts: &[String], exprs: &[String]) -> Result<Spanned, Fail> {
1127        // Every node here carries the WHOLE string literal's span.
1128        //
1129        // The inner expression is parsed from `raw` — a substring — so
1130        // its own offsets index that substring, not `src`. Carrying them
1131        // through would produce spans that look real and point into a
1132        // buffer nobody has: for `"a#{x}"` at offset 40, `x`'s span
1133        // would be 0..1, i.e. the start of the file. The lexer does not
1134        // record where each part sat in the outer source, so the string
1135        // literal is the most precise honest answer available.
1136        let mut acc = atom_at(span, Atom::Str(parts[0].clone()));
1137        for (i, raw) in exprs.iter().enumerate() {
1138            let inner = parse_expr(raw).map_err(|e| {
1139                fail(
1140                    format!("in interpolation `#{{{raw}}}`: {}", e.message),
1141                    span,
1142                )
1143            })?;
1144            let inner = Spanned::from_sexp_at(&inner, span);
1145            acc = list_at(span, vec![sym_at(span, LOWERED_CONCAT), acc, inner]);
1146            // `parts.len() == exprs.len() + 1` by construction, so this
1147            // index is always in range.
1148            acc = list_at(
1149                span,
1150                vec![
1151                    sym_at(span, LOWERED_CONCAT),
1152                    acc,
1153                    atom_at(span, Atom::Str(parts[i + 1].clone())),
1154                ],
1155            );
1156        }
1157        Ok(acc)
1158    }
1159
1160    #[inline(never)]
1161    fn not_prefix(&mut self, span: Span) -> Result<Spanned, Fail> {
1162        if self.at(&TokenKind::LParen) && self.touches_previous() {
1163            self.note(Spelling::OperatorSpelling, span, "not(x)", "not x");
1164            let (args, splat) = self.call_args()?;
1165            if args.len() != 1 || splat.is_some() {
1166                let call = self.call_node(self.span_since(span.start), vec![sym_at(span, "not")], args, splat);
1167                return Ok(call);
1168            }
1169            let operand = args.into_iter().next().expect("one argument");
1170            return Ok(self.negation(span, self.span_since(span.start), operand));
1171        }
1172        let rhs = self.expr(11)?;
1173        Ok(self.negation(span, self.span_since(span.start), rhs))
1174    }
1175
1176    fn prefix(&mut self) -> Result<Spanned, Fail> {
1177        if self.at(&TokenKind::LParen) {
1178            self.bump();
1179            return self.group();
1180        }
1181        self.prefix_token()
1182    }
1183
1184    fn group(&mut self) -> Result<Spanned, Fail> {
1185        self.skip_newlines();
1186        let inner = self.expr(0)?;
1187        self.skip_newlines();
1188        self.expect(&TokenKind::RParen, "`)`")?;
1189        Ok(inner)
1190    }
1191
1192    #[inline(never)]
1193    fn prefix_token(&mut self) -> Result<Spanned, Fail> {
1194        let span = self.peek_span();
1195        match self.bump() {
1196            TokenKind::Int(v) => Ok(atom_at(span, Atom::Int(v))),
1197            TokenKind::Float(v) => Ok(atom_at(span, Atom::Float(v))),
1198            TokenKind::Str(s) => Ok(atom_at(span, Atom::Str(s))),
1199
1200            // `"a#{x}b"` → `(concat (concat "a" x) "b")`.
1201            //
1202            // Lowered to `concat`, not to `+`: blue's `+` is arithmetic (see
1203            // the INFIX table), and interpolation must render a value of ANY
1204            // type — `concat` goes through `to_s`, which is what makes
1205            // `"n=#{42}"` work.
1206            //
1207            // The expression source is parsed HERE with the ordinary parser
1208            // rather than lexed inside the string, so an interpolation can hold
1209            // anything an expression can and the two can never drift.
1210            TokenKind::InterpolatedStr { parts, exprs } => Self::interpolation(span, &parts, &exprs),
1211            TokenKind::Sym(s) => Ok(atom_at(span, Atom::Keyword(s))),
1212            TokenKind::True => Ok(atom_at(span, Atom::Bool(true))),
1213            TokenKind::False => Ok(atom_at(span, Atom::Bool(false))),
1214            TokenKind::Nil => Ok(Spanned::new(span, SpannedForm::Nil)),
1215
1216            TokenKind::Op(o) if o == "-" => {
1217                let rhs = self.expr(11)?; // binds tighter than `*`
1218                Ok(list_at(
1219                    self.span_since(span.start),
1220                    vec![sym_at(span, "-"), atom_at(span, Atom::Int(0)), rhs],
1221                ))
1222            }
1223            TokenKind::Op(o) if o == "!" => {
1224                let written = self.text(span).to_string();
1225                self.note(Spelling::LogicalSymbol, span, &written, "not");
1226                let rhs = self.expr(11)?;
1227                Ok(self.negation(span, self.span_since(span.start), rhs))
1228            }
1229            TokenKind::Ident(w) if w == "not" => self.not_prefix(span),
1230            TokenKind::Ident(w) if (w == "and" || w == "or") && self.at(&TokenKind::LParen) => {
1231                Ok(sym_at(span, &w))
1232            }
1233            TokenKind::Ident(w) if w == "and" || w == "or" => Err(fail(
1234                format!("`{w}` joins two expressions: `a {w} b`"),
1235                span,
1236            )),
1237
1238            TokenKind::LParen => {
1239                self.skip_newlines();
1240                let inner = self.expr(0)?;
1241                self.skip_newlines();
1242                self.expect(&TokenKind::RParen, "`)`")?;
1243                // The inner expression's own span, NOT one widened to include
1244                // the parentheses: grouping is not part of the expression's
1245                // meaning, and `(a + b)` should underline `a + b`.
1246                Ok(inner)
1247            }
1248
1249            TokenKind::LBracket => self.list_literal(span),
1250            TokenKind::LBrace => self.map_literal(span),
1251
1252            TokenKind::Ident(name) if self.at(&TokenKind::PathSep) => {
1253                self.qualified_name(span, &name)
1254            }
1255            TokenKind::PathSep => Err(fail(
1256                "a qualified name is `package::name`, with the package written before `::`",
1257                span,
1258            )),
1259            TokenKind::Ident(name) => match name.as_str() {
1260                "if" => self.if_form(span, false),
1261                "begin" if !(self.at(&TokenKind::LParen) && self.touches_previous()) => {
1262                    self.begin_form(span)
1263                }
1264                "rescue" | "ensure" => Err(fail(
1265                    format!("`{name}` belongs to a `begin … end` block or a `def` body, after the statements it protects"),
1266                    span,
1267                )),
1268                "unless" => {
1269                    self.note(Spelling::Unless, span, "unless", "if not");
1270                    self.if_form(span, true)
1271                }
1272                "def" => self.def_form(span),
1273                "defmacro" => self.defmacro_form(span),
1274                "case" => self.case_form(span),
1275                "fn" => self.lambda_form(span),
1276                "test" => self.test_form(span),
1277                "assert" => self.assert_form(span),
1278                "quote" => self.quote_form(span),
1279                "unquote" => self.unquote_form(span, false),
1280                "unquote_splice" => self.unquote_form(span, true),
1281                "do" => Err(fail(
1282                    "blue has no `do` blocks: pass a function as an argument, \
1283                     `f(xs, fn(x) … end)`",
1284                    span,
1285                )),
1286                "end" => Err(fail(
1287                    "unexpected `end`: nothing open here for it to close",
1288                    span,
1289                )),
1290                _ => Ok(sym_at(span, &name)),
1291            },
1292
1293            other => Err(fail(format!("expected an expression, found {other}"), span)),
1294        }
1295    }
1296
1297    /// After `package`, at `::`: `retsu::first` lowers to the ONE symbol
1298    /// `retsu/first` — tatara's `require` shape, so a qualified name means the
1299    /// same thing in both lisps.
1300    ///
1301    /// After `::` any identifier is a name, reserved words included:
1302    /// `kueri::if` names kueri's definition, never the `if` form, because a
1303    /// reserved word is reserved only where it could start a form. The
1304    /// package side is never a reserved word, and a name has exactly one
1305    /// qualifier (`a::b::c` is refused), since a package is one level deep.
1306    fn qualified_name(&mut self, pkg_span: Span, pkg: &str) -> Result<Spanned, Fail> {
1307        if is_reserved_word(pkg) {
1308            return Err(fail(
1309                format!("`{pkg}` is a reserved word and cannot name a package"),
1310                pkg_span,
1311            ));
1312        }
1313        self.bump(); // ::
1314        let name_span = self.peek_span();
1315        let name = match self.bump() {
1316            TokenKind::Ident(n) => n,
1317            // `true`, `false` and `nil` lex as their own tokens.
1318            TokenKind::True => "true".to_string(),
1319            TokenKind::False => "false".to_string(),
1320            TokenKind::Nil => "nil".to_string(),
1321            other => {
1322                return Err(fail(
1323                    format!("expected a name after `{pkg}::`, found {other}"),
1324                    name_span,
1325                ))
1326            }
1327        };
1328        if self.at(&TokenKind::PathSep) {
1329            return Err(self.error(format!(
1330                "a qualified name has one qualifier: `package::name`, and `{pkg}::{name}::…` has two"
1331            )));
1332        }
1333        Ok(sym_at(
1334            self.span_since(pkg_span.start),
1335            &qualify(pkg, &name),
1336        ))
1337    }
1338
1339    /// After a `.`: `recv.name` or `recv.name(args)`.
1340    ///
1341    /// **A bare `recv.name` is a SEND, not a field read.** Blue commits to
1342    /// the uniform access principle here: a structure exposes no public
1343    /// fields, so a field can later become a computed method without
1344    /// breaking a caller.
1345    #[inline(never)]
1346    fn negation(&mut self, head: Span, whole: Span, operand: Spanned) -> Spanned {
1347        if let SpannedForm::List(items) = &operand.form {
1348            let flipped = match items.first().and_then(Spanned::as_symbol) {
1349                Some("equal?") => Some("not="),
1350                Some("not=") => Some("equal?"),
1351                _ => None,
1352            };
1353            if let (Some(to), 3) = (flipped, items.len()) {
1354                let (written, canonical) = if to == "not=" {
1355                    ("not (a == b)", "a != b")
1356                } else {
1357                    ("not (a != b)", "a == b")
1358                };
1359                self.note(Spelling::NegatedComparison, whole, written, canonical);
1360                let mut out = items.clone();
1361                out[0] = sym_at(items[0].span, to);
1362                return list_at(whole, out);
1363            }
1364        }
1365        list_at(whole, vec![sym_at(head, "not"), operand])
1366    }
1367
1368    #[inline(never)]
1369    fn call_spelling(&mut self, call: Spanned) -> Spanned {
1370        let SpannedForm::List(items) = &call.form else {
1371            return call;
1372        };
1373        let Some(head) = items.first().and_then(Spanned::as_symbol) else {
1374            return call;
1375        };
1376        let argc = items.len() - 1;
1377        let operator = match (head, argc) {
1378            ("and" | "or" | "mod" | "equal?", 2) => Some(head),
1379            ("list", _) => Some("list"),
1380            _ => None,
1381        };
1382        if let Some(op) = operator {
1383            let canonical = match op {
1384                "list" => "[…]".to_string(),
1385                "mod" => "a % b".to_string(),
1386                "equal?" => "a == b".to_string(),
1387                other => format!("a {other} b"),
1388            };
1389            self.note(Spelling::OperatorSpelling, call.span, &format!("{head}(…)"), &canonical);
1390            return call;
1391        }
1392        if head == "apply"
1393            && self.text(items[0].span) == "apply"
1394            && argc >= 2
1395            && items[1].as_symbol().is_some_and(is_callable_name)
1396        {
1397            self.note(Spelling::Apply, call.span, "apply(f, …, xs)", "f(…, *xs)");
1398            return call;
1399        }
1400        if head == "try" {
1401            return self.try_spelling(call);
1402        }
1403        call
1404    }
1405
1406    fn try_spelling(&mut self, call: Spanned) -> Spanned {
1407        let SpannedForm::List(items) = &call.form else {
1408            return call;
1409        };
1410        let Some((catch, body)) = items[1..].split_last() else {
1411            return call;
1412        };
1413        let Some(clause) = catch.as_list() else {
1414            return call;
1415        };
1416        let binder = match clause {
1417            [head, binding, handler @ ..]
1418                if head.as_symbol() == Some("catch") && !handler.is_empty() =>
1419            {
1420                match binding.as_list() {
1421                    Some([b]) => b.as_symbol().map(|n| (n.to_string(), b.span)),
1422                    _ => None,
1423                }
1424            }
1425            _ => None,
1426        };
1427        let Some((name, binder_span)) = binder else {
1428            return call;
1429        };
1430        if body.is_empty() {
1431            return call;
1432        }
1433        self.note(Spelling::TryCatch, call.span, "try(…, catch(…))", "rescue");
1434        let handler = &clause[2..];
1435        let rename = name.starts_with('_') && name != "_" && !handler.iter().any(|h| mentions(h, &name));
1436        let body = if body.len() == 1 {
1437            body[0].clone()
1438        } else {
1439            let span = cover(body, call.span);
1440            let mut list = vec![sym_at(Span::new(span.start, span.start), "begin")];
1441            list.extend(body.iter().cloned());
1442            list_at(span, list)
1443        };
1444        let handler = if handler.len() == 1 {
1445            handler[0].clone()
1446        } else {
1447            let span = cover(handler, catch.span);
1448            let mut list = vec![sym_at(Span::new(span.start, span.start), "begin")];
1449            list.extend(handler.iter().cloned());
1450            list_at(span, list)
1451        };
1452        let binder = sym_at(binder_span, if rename { "_" } else { &name });
1453        list_at(
1454            call.span,
1455            vec![
1456                items[0].clone(),
1457                body,
1458                list_at(
1459                    catch.span,
1460                    vec![
1461                        clause[0].clone(),
1462                        list_at(clause[1].span, vec![binder]),
1463                        handler,
1464                    ],
1465                ),
1466            ],
1467        )
1468    }
1469
1470    fn finish_send(&mut self, start: usize, recv: Spanned, dot: Span) -> Result<Spanned, Fail> {
1471        let name_span = self.peek_span();
1472        let name = match self.bump() {
1473            // A RESERVED WORD is not a method name.
1474            //
1475            // Found by the formatter property suite, minimal input `1.def`.
1476            // The send parsed happily into `(def 1)` — `def` is just an
1477            // identifier to the lexer — and the formatter then rendered that
1478            // as `def(1)`, which the parser rejects. So `format` turned valid
1479            // source into source that does not parse: corruption, not a style
1480            // choice, and silent until a round-trip property looked.
1481            //
1482            // Rejecting here rather than teaching the formatter to quote it
1483            // fixes the class instead of the symptom: `x.if`, `x.end` and
1484            // `x.case` all lower onto special forms the same way, and none of
1485            // them is a method anyone meant to call.
1486            TokenKind::Ident(n) if is_reserved_word(&n) => {
1487                return Err(self.error(format!(
1488                    "`{n}` is a reserved word and cannot be a method name — \
1489                     `recv.{n}` would lower onto the `{n}` form itself"
1490                )))
1491            }
1492            TokenKind::Ident(n) => n,
1493            other => {
1494                return Err(fail(
1495                    format!("expected a method name after `.`, found {other}"),
1496                    name_span,
1497                ))
1498            }
1499        };
1500        if !self.at(&TokenKind::LParen) {
1501            return Ok(list_at(
1502                self.span_since(start),
1503                vec![
1504                    sym_at(dot, LOWERED_FIELD),
1505                    recv,
1506                    atom_at(name_span, Atom::Keyword(name)),
1507                ],
1508            ));
1509        }
1510        self.note(Spelling::CallForm, dot.merge(name_span), &format!("x.{name}(…)"), &format!("{name}(x, …)"));
1511        let (args, splat) = self.call_args()?;
1512        Ok(self.call_node(self.span_since(start), vec![sym_at(name_span, &name), recv], args, splat))
1513    }
1514
1515    fn call_node(&self, span: Span, head: Vec<Spanned>, args: Vec<Spanned>, splat: Option<Span>) -> Spanned {
1516        let mut list = Vec::with_capacity(head.len() + args.len() + 1);
1517        if let Some(at) = splat {
1518            list.push(sym_at(at, "apply"));
1519        }
1520        list.extend(head);
1521        list.extend(args);
1522        list_at(span, list)
1523    }
1524
1525    fn paren_args(&mut self) -> Result<Vec<Spanned>, Fail> {
1526        let (args, splat) = self.call_args()?;
1527        if let Some(at) = splat {
1528            return Err(fail("a splat `*xs` passes a list as a call's last arguments, and this is not a call", at));
1529        }
1530        Ok(args)
1531    }
1532
1533    fn call_args(&mut self) -> Result<(Vec<Spanned>, Option<Span>), Fail> {
1534        self.expect(&TokenKind::LParen, "`(`")?;
1535        let mut args = Vec::new();
1536        let mut splat = None;
1537        self.skip_newlines();
1538        if self.eat(&TokenKind::RParen) {
1539            return Ok((args, splat));
1540        }
1541        loop {
1542            self.skip_newlines();
1543            if splat.is_some() {
1544                return Err(self.error("a splat `*xs` passes a list as the LAST arguments: write it last"));
1545            }
1546            if matches!(self.peek(), TokenKind::Op(o) if o == "*") {
1547                splat = Some(self.peek_span());
1548                self.bump();
1549            }
1550            args.push(self.expr(0)?);
1551            self.skip_newlines();
1552            if self.eat(&TokenKind::Comma) {
1553                if self.trailing_comma(&TokenKind::RParen) {
1554                    break;
1555                }
1556                continue;
1557            }
1558            self.expect(&TokenKind::RParen, "`,` or `)`")?;
1559            break;
1560        }
1561        Ok((args, splat))
1562    }
1563
1564    fn trailing_comma(&mut self, closer: &TokenKind) -> bool {
1565        let comma = self.toks.get(self.pos.wrapping_sub(1)).map_or(self.peek_span(), |t| t.span);
1566        self.skip_newlines();
1567        if self.at(closer) {
1568            self.note(Spelling::TrailingComma, comma, ",", "");
1569            self.bump();
1570            return true;
1571        }
1572        false
1573    }
1574
1575    fn list_literal(&mut self, open: Span) -> Result<Spanned, Fail> {
1576        let mut items = vec![sym_at(open, "list")];
1577        self.skip_newlines();
1578        if self.eat(&TokenKind::RBracket) {
1579            return Ok(list_at(self.span_since(open.start), items));
1580        }
1581        loop {
1582            self.skip_newlines();
1583            items.push(self.expr(0)?);
1584            self.skip_newlines();
1585            if self.eat(&TokenKind::Comma) {
1586                if self.trailing_comma(&TokenKind::RBracket) {
1587                    break;
1588                }
1589                continue;
1590            }
1591            self.expect(&TokenKind::RBracket, "`,` or `]`")?;
1592            break;
1593        }
1594        Ok(list_at(self.span_since(open.start), items))
1595    }
1596
1597    /// `{a: 1, "k" => v}` — both spellings, one tree.
1598    ///
1599    /// This is §V.13's rendering law at the parser: `a: 1` and `:a => 1`
1600    /// produce the *same* s-expression, which is precisely why the
1601    /// formatter may always choose the shorthand. The rocket survives only
1602    /// where the key is not a plain symbol.
1603    fn map_literal(&mut self, open: Span) -> Result<Spanned, Fail> {
1604        let mut items = vec![sym_at(open, LOWERED_MAP)];
1605        self.skip_newlines();
1606        if self.eat(&TokenKind::RBrace) {
1607            return Ok(list_at(self.span_since(open.start), items));
1608        }
1609        loop {
1610            self.skip_newlines();
1611            match self.peek().clone() {
1612                TokenKind::Label(name) => {
1613                    let label_span = self.peek_span();
1614                    self.bump();
1615                    self.skip_newlines();
1616                    items.push(atom_at(label_span, Atom::Keyword(name)));
1617                    items.push(self.expr(0)?);
1618                }
1619                _ => {
1620                    let k = self.expr(0)?;
1621                    self.skip_newlines();
1622                    if let SpannedForm::Atom(Atom::Keyword(name)) = &k.form {
1623                        if blue_lang_name(name) {
1624                            let written = format!(":{name} =>");
1625                            let canonical = format!("{name}:");
1626                            self.note(Spelling::KeywordRocket, k.span, &written, &canonical);
1627                        }
1628                    }
1629                    self.expect(&TokenKind::Rocket, "`=>` in a map literal")?;
1630                    self.skip_newlines();
1631                    items.push(k);
1632                    items.push(self.expr(0)?);
1633                }
1634            }
1635            self.skip_newlines();
1636            if self.eat(&TokenKind::Comma) {
1637                if self.trailing_comma(&TokenKind::RBrace) {
1638                    break;
1639                }
1640                continue;
1641            }
1642            self.expect(&TokenKind::RBrace, "`,` or `}`")?;
1643            break;
1644        }
1645        Ok(list_at(self.span_since(open.start), items))
1646    }
1647
1648    /// `if c ... [else ...] end`, and `unless` as its negation.
1649    ///
1650    /// `unless` lowers to `(if (not c) ...)` rather than to a distinct
1651    /// form: one tree per meaning, so the formatter and every downstream
1652    /// tool see exactly one shape.
1653    /// An `elsif` arm: an `if` that does NOT consume the closing `end`.
1654    ///
1655    /// `if / elsif / elsif / else / end` is one `end` for the whole chain, so
1656    /// every arm but the first must leave it for its parent.
1657    fn if_chain(&mut self, head: Span) -> Result<Spanned, Fail> {
1658        let cond = self.expr(0)?;
1659        let then = self.body(&["elsif", "else", "end"])?;
1660        let els = self.else_part()?;
1661        let mut out = vec![sym_at(head, "if"), cond, then];
1662        if let Some(e) = els {
1663            out.push(e);
1664        }
1665        Ok(list_at(self.span_since(head.start), out))
1666    }
1667
1668    fn else_part(&mut self) -> Result<Option<Spanned>, Fail> {
1669        if self.at_ident("elsif") {
1670            let elsif = self.peek_span();
1671            self.bump();
1672            return Ok(Some(self.if_chain(elsif)?));
1673        }
1674        if !self.at_ident("else") {
1675            self.expect_ident("end")?;
1676            return Ok(None);
1677        }
1678        let else_span = self.peek_span();
1679        self.bump();
1680        let save = self.pos;
1681        self.skip_newlines();
1682        let leads_with_if = self.at_ident("if") || self.at_ident("unless");
1683        self.pos = save;
1684        let e = self.body(&["end"])?;
1685        let end_at = self.peek_span().start;
1686        self.expect_ident("end")?;
1687        if matches!(e.form, SpannedForm::Nil) && !self.comment_within(else_span.start, end_at) {
1688            self.note(Spelling::ElseNil, else_span, "else nil", "no else");
1689            return Ok(None);
1690        }
1691        let single_if = matches!(&e.form, SpannedForm::List(items)
1692            if (items.len() == 3 || items.len() == 4) && items[0].as_symbol() == Some("if"));
1693        if leads_with_if && single_if {
1694            self.note(Spelling::ElseIf, else_span, "else if", "elsif");
1695        }
1696        Ok(Some(e))
1697    }
1698
1699    fn if_form(&mut self, head: Span, negate: bool) -> Result<Spanned, Fail> {
1700        let cond = self.expr(0)?;
1701        let cond = if negate {
1702            // `unless c` lowers to `(if (not c) …)`. The synthesized `not` takes
1703            // the `unless` keyword's span — that word is what put it there.
1704            let inner = cond.span;
1705            self.negation(head, head.merge(inner), cond)
1706        } else {
1707            cond
1708        };
1709        // `elsif` terminates the then-body too, or the chain is swallowed.
1710        //
1711        // It used to be absent entirely — not a keyword, not reserved, nowhere
1712        // in this file — so `elsif cond` parsed as a stray expression inside
1713        // the then-body and the branch it guarded VANISHED. No error: the
1714        // program ran and returned the `else` value. Measured:
1715        //
1716        //   if a < 1 then 1 elsif a < 5 then 2 else 3 end   with a = 3
1717        //     wanted 2, returned 3
1718        //
1719        // A silent wrong answer in the most-used control form in the language,
1720        // reachable by writing the Ruby every blue user already knows.
1721        let then = self.body(&["elsif", "else", "end"])?;
1722        let els = self.else_part()?;
1723        let mut out = vec![sym_at(head, "if"), cond, then];
1724        if let Some(e) = els {
1725            out.push(e);
1726        }
1727        Ok(list_at(self.span_since(head.start), out))
1728    }
1729
1730    /// `def name(a, b) ... end`            => `(define (name a b) body)`
1731    /// `def name(a: T, b: T) -> R ... end`  => `(define-typed (name (a T) (b T)) R body)`
1732    ///
1733    /// **The two shapes are deliberately different heads.** §0 says an
1734    /// unannotated program gets ZERO analysis, and the cleanest way to
1735    /// mean that is for untyped code not to reach the typing machinery at
1736    /// all — not to reach it and be waved through. A checker that must
1737    /// walk every node to discover there is nothing to check has already
1738    /// paid the cost the ladder exists to avoid.
1739    ///
1740    /// Annotations are per-parameter, so a signature may be partially
1741    /// annotated. That is the ladder at its finest grain: `a: Int` is
1742    /// checked and a bare `b` stays `dyn`, in the same signature.
1743    /// `case subject / when a / … / else / … / end` => a `cond` over equality.
1744    ///
1745    /// **Value matching, not destructuring.** Elixir's `case` binds pattern
1746    /// variables; blue's compares with the same `equal?` the `==` operator uses,
1747    /// so `when [1, 2]` matches a list by value. Destructuring needs a pattern
1748    /// language and a binder, which blue does not have — and a `case` that
1749    /// *looked* like Elixir's while silently only comparing would be worse than
1750    /// one that plainly compares.
1751    ///
1752    /// The subject is evaluated ONCE, into a binding, so `case expensive()` does
1753    /// not re-run per arm. That is a correctness property, not an optimisation:
1754    /// a subject with a side effect would fire once per `when`.
1755    fn case_form(&mut self, head: Span) -> Result<Spanned, Fail> {
1756        let subject = self.expr(0)?;
1757        self.skip_newlines();
1758
1759        // A fresh name the surface cannot spell, so it cannot capture a user
1760        // binding of the same name.
1761        let subject_var = "case-subject";
1762        let mut arms: Vec<Spanned> = Vec::new();
1763        let mut otherwise: Option<Spanned> = None;
1764        // The `else` keyword's own span, so the synthesized `(else body)` clause
1765        // sits where the author wrote it rather than back at `case`.
1766        let mut else_kw: Option<Span> = None;
1767
1768        loop {
1769            self.skip_newlines();
1770            if self.at_ident("end") {
1771                break;
1772            }
1773            let maybe_else = self.peek_span();
1774            if self.eat_ident("else") {
1775                else_kw = Some(maybe_else);
1776                otherwise = Some(self.body(&["end"])?);
1777                continue;
1778            }
1779            let when = self.peek_span();
1780            if !self.eat_ident("when") {
1781                return Err(self.error(format!(
1782                    "expected `when`, `else` or `end` in a case, found {}",
1783                    self.peek()
1784                )));
1785            }
1786            self.skip_newlines();
1787            let pattern = self.expr(0)?;
1788            let body = self.body(&["when", "else", "end"])?;
1789            // The synthesized comparison takes the arm's `when` span: that word
1790            // is what put an `equal?` there, and it is where a reader looks when
1791            // told an arm cannot match.
1792            let test = list_at(
1793                when.merge(pattern.span),
1794                vec![sym_at(when, "equal?"), sym_at(when, subject_var), pattern],
1795            );
1796            arms.push(list_at(when.merge(body.span), vec![test, body]));
1797        }
1798        self.expect_ident("end")?;
1799
1800        if arms.is_empty() && otherwise.is_none() {
1801            return Err(self.error("a case needs at least one `when` or an `else`".to_string()));
1802        }
1803
1804        // A case with no matching arm and no else is NIL, matching Ruby. Elixir
1805        // raises CaseClauseError; blue follows Ruby because its `if` without an
1806        // else is already nil, and having two different answers to "no branch
1807        // taken" in one language is the inconsistency.
1808        let mut cond = vec![sym_at(head, "cond")];
1809        cond.extend(arms);
1810        // The `else` clause spans its own keyword through its body. Anchoring it
1811        // at `head` — the `case` keyword — put a node whose subtree sits at the
1812        // END of the form at a span near its START, so the else body escaped its
1813        // parent's range entirely. `every_node_sits_inside_its_parents_span`
1814        // caught that; it is the kind of mistake no output-shape test can see,
1815        // because the tree is correct and only the positions are wrong.
1816        let else_span = else_kw.unwrap_or(head);
1817        let else_body = otherwise.unwrap_or_else(|| Spanned::new(else_span, SpannedForm::Nil));
1818        cond.push(list_at(
1819            else_span.merge(else_body.span),
1820            vec![sym_at(else_span, "else"), else_body],
1821        ));
1822
1823        let whole = self.span_since(head.start);
1824        let binding = list_at(
1825            head.merge(subject.span),
1826            vec![sym_at(head, subject_var), subject],
1827        );
1828        Ok(list_at(
1829            whole,
1830            vec![
1831                sym_at(head, "let"),
1832                list_at(binding.span, vec![binding]),
1833                list_at(whole, cond),
1834            ],
1835        ))
1836    }
1837
1838    /// `fn(a, b) ... end` => `(lambda (a b) body)`
1839    ///
1840    /// Without this the higher-order functions are unreachable in practice:
1841    /// `map(inc, xs)` works only because `inc` happens to be a named stdlib
1842    /// function, and there was no way to write the one-off the call site
1843    /// actually wants.
1844    ///
1845    /// `fn` rather than Ruby's `->` or `lambda`: `->` collides with the return-
1846    /// type arrow the typed `def` already uses, and reusing one glyph for two
1847    /// unrelated things is the ambiguity the FORM axis exists to prevent.
1848    fn lambda_form(&mut self, head: Span) -> Result<Spanned, Fail> {
1849        let mut params: Vec<Spanned> = Vec::new();
1850        if !self.at(&TokenKind::LParen) {
1851            self.note(Spelling::ParameterList, head, "fn", "fn()");
1852        }
1853        if self.at(&TokenKind::LParen) {
1854            self.bump();
1855            self.skip_newlines();
1856            if !self.eat(&TokenKind::RParen) {
1857                loop {
1858                    self.skip_newlines();
1859                    let p_span = self.peek_span();
1860                    match self.bump() {
1861                        TokenKind::Ident(p) => params.push(sym_at(p_span, &p)),
1862                        other => {
1863                            return Err(fail(
1864                                format!("expected a parameter name, found {other}"),
1865                                p_span,
1866                            ))
1867                        }
1868                    }
1869                    self.skip_newlines();
1870                    if self.eat(&TokenKind::Comma) {
1871                        if self.trailing_comma(&TokenKind::RParen) {
1872                            break;
1873                        }
1874                        continue;
1875                    }
1876                    self.expect(&TokenKind::RParen, "`,` or `)`")?;
1877                    break;
1878                }
1879            }
1880        }
1881        let params_span = cover(&params, head);
1882        let body = self.body(&["end"])?;
1883        self.expect_ident("end")?;
1884        Ok(list_at(
1885            self.span_since(head.start),
1886            vec![sym_at(head, "lambda"), list_at(params_span, params), body],
1887        ))
1888    }
1889
1890    /// `test "name" ... end` => `(deftest "name" body)`
1891    ///
1892    /// A string, not an identifier: a test name is prose for a human report,
1893    /// and forcing it into an identifier is how test names become
1894    /// `test_adds_two_numbers_correctly`.
1895    fn test_form(&mut self, head: Span) -> Result<Spanned, Fail> {
1896        let name_span = self.peek_span();
1897        let name = match self.bump() {
1898            TokenKind::Str(s) => s,
1899            other => {
1900                return Err(fail(
1901                    format!("expected a string name after `test`, found {other}"),
1902                    name_span,
1903                ))
1904            }
1905        };
1906        let body = self.body(&["end"])?;
1907        self.expect_ident("end")?;
1908        Ok(list_at(
1909            self.span_since(head.start),
1910            vec![
1911                sym_at(head, "deftest"),
1912                atom_at(name_span, Atom::Str(name)),
1913                body,
1914            ],
1915        ))
1916    }
1917
1918    /// `assert expr` => `(blue-assert 'expr expr)`
1919    ///
1920    /// **`blue-assert`, not `assert`.** tatara-lisp's stdlib already defines
1921    /// `assert` as a macro — `(defmacro assert (pred message) …)` — and a macro
1922    /// in the expander is consulted before any primitive in the registry. So
1923    /// lowering to `assert` bound `pred` to the *quoted form*, which is
1924    /// truthy, and **every assertion silently passed**. A test framework whose
1925    /// assertions always pass is the worst defect it can have: every test in
1926    /// the suite goes green.
1927    ///
1928    /// The lesson generalizes: any name blue lowers to that tatara already
1929    /// binds is silently captured. `blue_lang_test`'s
1930    /// `no_lowered_name_is_shadowed_by_the_runtime` gates the whole class.
1931    ///
1932    /// **Both the form and the value.** A test framework whose failure says
1933    /// only "assertion failed" makes the author re-derive what they were
1934    /// checking; one that shows the expression does not. The quoted form is
1935    /// the expression as DATA, so the runner can render it — and it renders it
1936    /// through `blue-lang-fmt`, meaning the failure message is in canonical
1937    /// blue syntax rather than the underlying tatara-lisp.
1938    ///
1939    /// This is homoiconicity paying for itself: the capture needs no source
1940    /// map, no macro hygiene, and no string of the original text.
1941    fn assert_form(&mut self, head: Span) -> Result<Spanned, Fail> {
1942        let e = self.expr(0)?;
1943        let quoted = Spanned::new(e.span, SpannedForm::Quote(Box::new(e.clone())));
1944        Ok(list_at(
1945            self.span_since(head.start),
1946            vec![sym_at(head, LOWERED_ASSERT), quoted, e],
1947        ))
1948    }
1949
1950    /// `defmacro name(a, b) ... end` => `(defmacro name (a b) body)`
1951    ///
1952    /// **Deliberately untyped.** A macro's parameters are *source forms*, not
1953    /// values, so `a: Int` would be a category error: the argument at expansion
1954    /// time is a fragment of syntax. §IV's ladder types values; macro
1955    /// parameters are not on it. Annotating one is rejected rather than
1956    /// silently ignored — an ignored annotation is how an author comes to
1957    /// believe a check is running.
1958    fn defmacro_form(&mut self, head: Span) -> Result<Spanned, Fail> {
1959        let name_span = self.peek_span();
1960        let name = match self.bump() {
1961            TokenKind::Ident(n) => n,
1962            other => {
1963                return Err(fail(
1964                    format!("expected a name after `defmacro`, found {other}"),
1965                    name_span,
1966                ))
1967            }
1968        };
1969        let mut params: Vec<Spanned> = Vec::new();
1970        if self.at(&TokenKind::LParen) {
1971            self.bump();
1972            self.skip_newlines();
1973            if !self.eat(&TokenKind::RParen) {
1974                loop {
1975                    self.skip_newlines();
1976                    let p_span = self.peek_span();
1977                    match self.bump() {
1978                        TokenKind::Ident(p) => params.push(sym_at(p_span, &p)),
1979                        TokenKind::Label(p) => {
1980                            return Err(self.error(format!(
1981                                "macro parameter `{p}` cannot be typed: a macro receives \
1982                                 source forms, not values"
1983                            )))
1984                        }
1985                        other => {
1986                            return Err(fail(
1987                                format!("expected a parameter name, found {other}"),
1988                                p_span,
1989                            ))
1990                        }
1991                    }
1992                    self.skip_newlines();
1993                    if self.eat(&TokenKind::Comma) {
1994                        if self.trailing_comma(&TokenKind::RParen) {
1995                            break;
1996                        }
1997                        continue;
1998                    }
1999                    self.expect(&TokenKind::RParen, "`,` or `)`")?;
2000                    break;
2001                }
2002            }
2003        }
2004        if matches!(self.peek(), TokenKind::Op(o) if o == "->") {
2005            return Err(self.error(
2006                "a macro has no return type: it produces source forms, not values".to_string(),
2007            ));
2008        }
2009
2010        let params_span = cover(&params, name_span);
2011        let body = self.body(&["end"])?;
2012        self.expect_ident("end")?;
2013
2014        // `(defmacro name (params) body)` — tatara-lisp's own shape, so blue
2015        // registers into the SAME expander rather than a parallel one.
2016        Ok(list_at(
2017            self.span_since(head.start),
2018            vec![
2019                sym_at(head, "defmacro"),
2020                sym_at(name_span, &name),
2021                list_at(params_span, params),
2022                body,
2023            ],
2024        ))
2025    }
2026
2027    /// `quote ... end` => `` `body `` (a quasiquote).
2028    ///
2029    /// Quasiquote rather than plain quote, because a macro body that could not
2030    /// splice its arguments in would be useless — this is Elixir's `quote do`,
2031    /// which is likewise a template and not inert data.
2032    fn quote_form(&mut self, head: Span) -> Result<Spanned, Fail> {
2033        let body = self.body(&["end"])?;
2034        self.expect_ident("end")?;
2035        // `Sexp::Quasiquote`, NOT `(quasiquote body)` as a list.
2036        //
2037        // The list form Displays as the text `(quasiquote …)`, which the
2038        // tatara-lisp reader reads back as an ordinary list whose head happens
2039        // to be the symbol `quasiquote` — losing the structure. The evaluator
2040        // then reached the inner `,x` with no enclosing quasiquote and rejected
2041        // it: "unquote outside of quasiquote". Building the real variant makes
2042        // it Display as `` ` `` and survive the round trip.
2043        Ok(Spanned::new(
2044            self.span_since(head.start),
2045            SpannedForm::Quasiquote(Box::new(body)),
2046        ))
2047    }
2048
2049    /// `unquote(expr)` => `,expr`; `unquote_splice(expr)` => `,@expr`.
2050    fn unquote_form(&mut self, head: Span, splice: bool) -> Result<Spanned, Fail> {
2051        self.expect(&TokenKind::LParen, "`(` after unquote")?;
2052        self.skip_newlines();
2053        let inner = self.expr(0)?;
2054        self.skip_newlines();
2055        self.expect(&TokenKind::RParen, "`)`")?;
2056        let span = self.span_since(head.start);
2057        Ok(Spanned::new(
2058            span,
2059            if splice {
2060                SpannedForm::UnquoteSplice(Box::new(inner))
2061            } else {
2062                SpannedForm::Unquote(Box::new(inner))
2063            },
2064        ))
2065    }
2066
2067    fn protected_body(&mut self, head: Span) -> Result<Spanned, Fail> {
2068        let body = self.body(&["rescue", "ensure", "end"])?;
2069        if self.at_ident("rescue") || self.at_ident("ensure") {
2070            return self.rescue_tail(head, body);
2071        }
2072        Ok(body)
2073    }
2074
2075    fn begin_form(&mut self, head: Span) -> Result<Spanned, Fail> {
2076        let body = self.body(&["rescue", "ensure", "end"])?;
2077        if !(self.at_ident("rescue") || self.at_ident("ensure")) {
2078            return Err(self.error(
2079                "a `begin` block protects its statements with `rescue` or `ensure`; without either, write the statements directly",
2080            ));
2081        }
2082        let tree = self.rescue_tail(head, body)?;
2083        self.expect_ident("end")?;
2084        Ok(Spanned::new(self.span_since(head.start), tree.form))
2085    }
2086
2087    fn rescue_tail(&mut self, head: Span, body: Spanned) -> Result<Spanned, Fail> {
2088        let mut clauses: Vec<Spanned> = Vec::new();
2089        let mut catch_all: Option<(Spanned, Spanned, Span)> = None;
2090        while self.at_ident("rescue") {
2091            let kw = self.peek_span();
2092            self.bump();
2093            if catch_all.is_some() {
2094                return Err(fail(
2095                    "a `rescue NAME` catches every error, so it is the last `rescue`; put the kinds it would shadow above it",
2096                    kw,
2097                ));
2098            }
2099            match self.peek().clone() {
2100                TokenKind::Sym(_) => {
2101                    let mut kinds = Vec::new();
2102                    loop {
2103                        let k_span = self.peek_span();
2104                        match self.bump() {
2105                            TokenKind::Sym(k) => kinds.push(atom_at(k_span, Atom::Keyword(k))),
2106                            other => {
2107                                return Err(fail(
2108                                    format!("expected an error kind such as `:not_found`, found {other}"),
2109                                    k_span,
2110                                ))
2111                            }
2112                        }
2113                        if self.eat(&TokenKind::Comma) {
2114                            self.skip_newlines();
2115                            continue;
2116                        }
2117                        break;
2118                    }
2119                    let binder = if self.eat_ident("as") {
2120                        let b_span = self.peek_span();
2121                        match self.bump() {
2122                            TokenKind::Ident(b) if !is_reserved_word(&b) => Some(sym_at(b_span, &b)),
2123                            other => {
2124                                return Err(fail(
2125                                    format!("expected the name the error is bound to after `as`, found {other}"),
2126                                    b_span,
2127                                ))
2128                            }
2129                        }
2130                    } else {
2131                        None
2132                    };
2133                    let handler = self.body(&["rescue", "ensure", "end"])?;
2134                    let mut test = vec![sym_at(kw, LOWERED_ERROR_MATCH), sym_at(kw, RESCUED)];
2135                    test.extend(kinds);
2136                    let test = list_at(cover(&test, kw), test);
2137                    let handler = match binder {
2138                        Some(b) => let_bound(b, handler, kw),
2139                        None => handler,
2140                    };
2141                    clauses.push(list_at(kw.merge(handler.span), vec![test, handler]));
2142                }
2143                TokenKind::Ident(b) if !is_reserved_word(&b) => {
2144                    let b_span = self.peek_span();
2145                    self.bump();
2146                    let handler = self.body(&["rescue", "ensure", "end"])?;
2147                    catch_all = Some((sym_at(b_span, &b), handler, kw));
2148                }
2149                _ => {
2150                    return Err(fail(
2151                        "`rescue` names what it catches: `rescue :kind`, `rescue :a, :b as e`, or `rescue e` for every error (`rescue _` discards it)",
2152                        kw,
2153                    ))
2154                }
2155            }
2156        }
2157        let ensure = if self.at_ident("ensure") {
2158            self.bump();
2159            let cleanup = self.body(&["rescue", "end"])?;
2160            if self.at_ident("rescue") {
2161                return Err(self.error("`ensure` comes after every `rescue` of its block"));
2162            }
2163            Some(cleanup)
2164        } else {
2165            None
2166        };
2167        let whole = self.span_since(head.start);
2168        let protected = if clauses.is_empty() {
2169            match catch_all {
2170                None => body,
2171                Some((binder, handler, kw)) => list_at(
2172                    whole,
2173                    vec![
2174                        sym_at(head, "try"),
2175                        body,
2176                        list_at(
2177                            kw.merge(handler.span),
2178                            vec![sym_at(kw, "catch"), list_at(binder.span, vec![binder]), handler],
2179                        ),
2180                    ],
2181                ),
2182            }
2183        } else {
2184            let first_kw = clauses[0].span;
2185            let otherwise = match catch_all {
2186                Some((binder, handler, kw)) => {
2187                    let bound = let_bound(binder, handler, kw);
2188                    list_at(kw.merge(bound.span), vec![sym_at(kw, "else"), bound])
2189                }
2190                None => list_at(
2191                    head,
2192                    vec![
2193                        sym_at(head, "else"),
2194                        list_at(head, vec![sym_at(head, LOWERED_RERAISE), sym_at(head, RESCUED)]),
2195                    ],
2196                ),
2197            };
2198            let mut cond = vec![sym_at(first_kw, "cond")];
2199            cond.extend(clauses);
2200            cond.push(otherwise);
2201            let cond_span = cover(&cond, first_kw);
2202            list_at(
2203                whole,
2204                vec![
2205                    sym_at(head, "try"),
2206                    body,
2207                    list_at(
2208                        cond_span,
2209                        vec![
2210                            sym_at(first_kw, "catch"),
2211                            list_at(first_kw, vec![sym_at(first_kw, RESCUED)]),
2212                            list_at(cond_span, cond),
2213                        ],
2214                    ),
2215                ],
2216            )
2217        };
2218        Ok(match ensure {
2219            None => protected,
2220            Some(cleanup) => {
2221                let attempted = list_at(protected.span, vec![sym_at(head, LOWERED_ATTEMPTED), protected]);
2222                let failed = list_at(head, vec![sym_at(head, LOWERED_FAILED), sym_at(head, RESCUED)]);
2223                let caught = list_at(
2224                    whole,
2225                    vec![
2226                        sym_at(head, "try"),
2227                        attempted,
2228                        list_at(head, vec![sym_at(head, "catch"), list_at(head, vec![sym_at(head, RESCUED)]), failed]),
2229                    ],
2230                );
2231                list_at(whole, vec![sym_at(head, LOWERED_ENSURE), caught, thunk(cleanup, head)])
2232            }
2233        })
2234    }
2235
2236    fn def_form(&mut self, head: Span) -> Result<Spanned, Fail> {
2237        let name_span = self.peek_span();
2238        let name = match self.bump() {
2239            TokenKind::Ident(n) => n,
2240            other => {
2241                return Err(fail(
2242                    format!("expected a name after `def`, found {other}"),
2243                    name_span,
2244                ))
2245            }
2246        };
2247        let mut params: Vec<(String, Span, Option<Spanned>)> = Vec::new();
2248        if !self.at(&TokenKind::LParen) {
2249            self.note(Spelling::ParameterList, name_span, &format!("def {name}"), &format!("def {name}()"));
2250        }
2251        if self.at(&TokenKind::LParen) {
2252            self.bump();
2253            self.skip_newlines();
2254            if !self.eat(&TokenKind::RParen) {
2255                loop {
2256                    self.skip_newlines();
2257                    let p_span = self.peek_span();
2258                    match self.bump() {
2259                        // `a` — unannotated
2260                        TokenKind::Ident(p) => params.push((p, p_span, None)),
2261                        // `a:` came through as one token, so a type follows
2262                        TokenKind::Label(p) => {
2263                            self.skip_newlines();
2264                            let ty = self.type_expr()?;
2265                            params.push((p, p_span, Some(ty)));
2266                        }
2267                        other => {
2268                            return Err(fail(
2269                                format!("expected a parameter name, found {other}"),
2270                                p_span,
2271                            ))
2272                        }
2273                    }
2274                    self.skip_newlines();
2275                    if self.eat(&TokenKind::Comma) {
2276                        if self.trailing_comma(&TokenKind::RParen) {
2277                            break;
2278                        }
2279                        continue;
2280                    }
2281                    self.expect(&TokenKind::RParen, "`,` or `)`")?;
2282                    break;
2283                }
2284            }
2285        }
2286        // The signature list spans the name through the closing `)`.
2287        let sig_span = self.span_since(name_span.start);
2288
2289        // Optional `-> R`
2290        let ret = if matches!(self.peek(), TokenKind::Op(o) if o == "->") {
2291            self.bump();
2292            self.skip_newlines();
2293            Some(self.type_expr()?)
2294        } else {
2295            None
2296        };
2297
2298        let begins = {
2299            let save = self.pos;
2300            self.skip_newlines();
2301            let at = self.at_ident("begin").then(|| self.peek_span());
2302            self.pos = save;
2303            at
2304        };
2305        let body = self.protected_body(head)?;
2306        if let Some(b) = begins {
2307            let protects = matches!(&body.form, SpannedForm::List(items)
2308                if matches!(items.first().and_then(Spanned::as_symbol), Some("try") | Some(LOWERED_ENSURE)));
2309            if protects && body.span.start == b.start {
2310                self.note(Spelling::TryCatch, b, "begin … end as a whole def body", "the clauses on the def itself");
2311            }
2312        }
2313        self.expect_ident("end")?;
2314        let whole = self.span_since(head.start);
2315
2316        let annotated = ret.is_some() || params.iter().any(|(_, _, t)| t.is_some());
2317        if !annotated {
2318            let mut sig = vec![sym_at(name_span, &name)];
2319            sig.extend(params.into_iter().map(|(p, s, _)| sym_at(s, &p)));
2320            return Ok(list_at(
2321                whole,
2322                vec![sym_at(head, "define"), list_at(sig_span, sig), body],
2323            ));
2324        }
2325
2326        // Typed shape. An un-annotated parameter in an otherwise annotated
2327        // signature is written `(p dyn)` so the checker sees the ladder
2328        // position explicitly rather than inferring it from absence.
2329        //
2330        // A `dyn` written by the parser rather than the author takes the
2331        // parameter's own span, so a diagnostic about it points at the
2332        // parameter — the only thing in the source it could mean.
2333        let mut sig = vec![sym_at(name_span, &name)];
2334        for (p, s, t) in params {
2335            let ty = t.unwrap_or_else(|| sym_at(s, "dyn"));
2336            sig.push(list_at(s.merge(ty.span), vec![sym_at(s, &p), ty]));
2337        }
2338        Ok(list_at(
2339            whole,
2340            vec![
2341                sym_at(head, "define-typed"),
2342                list_at(sig_span, sig),
2343                ret.unwrap_or_else(|| sym_at(head, "dyn")),
2344                body,
2345            ],
2346        ))
2347    }
2348
2349    /// A type expression. Currently a bare name (`Int`, `Str`, `dyn`) or a
2350    /// one-argument constructor (`List(Int)`).
2351    fn type_expr(&mut self) -> Result<Spanned, Fail> {
2352        let name_span = self.peek_span();
2353        let name = match self.bump() {
2354            TokenKind::Ident(n) => n,
2355            other => {
2356                return Err(fail(
2357                    format!("expected a type name, found {other}"),
2358                    name_span,
2359                ))
2360            }
2361        };
2362        if self.at(&TokenKind::LParen) {
2363            let args = self.paren_args()?;
2364            let mut list = vec![sym_at(name_span, &name)];
2365            list.extend(args);
2366            return Ok(list_at(self.span_since(name_span.start), list));
2367        }
2368        Ok(sym_at(name_span, &name))
2369    }
2370
2371    /// Consume `name` if it is the next token, else leave the position alone.
2372    fn eat_ident(&mut self, name: &str) -> bool {
2373        if self.at_ident(name) {
2374            self.bump();
2375            true
2376        } else {
2377            false
2378        }
2379    }
2380
2381    fn expect_ident(&mut self, name: &str) -> Result<(), Fail> {
2382        if self.at_ident(name) {
2383            self.bump();
2384            Ok(())
2385        } else {
2386            Err(self.error(format!("expected `{name}`, found {}", self.peek())))
2387        }
2388    }
2389
2390    /// A sequence of expressions up to one of `terminators`, wrapped in
2391    /// `(begin ...)` when there is more than one.
2392    fn body(&mut self, terminators: &[&str]) -> Result<Spanned, Fail> {
2393        // Anchored BEFORE the leading newlines are skipped, so an empty body's
2394        // span sits where the body would have started.
2395        let start = self.mark();
2396        let mut forms = Vec::new();
2397        loop {
2398            self.skip_newlines();
2399            if matches!(self.peek(), TokenKind::Eof) {
2400                return Err(self.error(format!(
2401                    "unterminated block: expected {} before the end of the file",
2402                    one_of(terminators)
2403                )));
2404            }
2405            if terminators.iter().any(|t| self.at_ident(t)) {
2406                break;
2407            }
2408            forms.push(self.statement()?);
2409            self.end_of_statement(terminators)?;
2410        }
2411        Ok(match forms.len() {
2412            0 => Spanned::new(Span::new(start, start), SpannedForm::Nil),
2413            1 => forms.into_iter().next().expect("checked len"),
2414            _ => {
2415                // `cover`, NOT `span_since`: the loop breaks *before* consuming
2416                // the terminator but *after* `skip_newlines`, so the last token
2417                // behind `pos` can be a newline and `span_since` would stretch
2418                // the body across trailing blank lines. The forms know exactly
2419                // where they are.
2420                let span = cover(&forms, Span::new(start, start));
2421                // The synthesized `begin` is zero-width at the FIRST FORM's
2422                // start, not at `start`. `start` is anchored before the leading
2423                // newlines are skipped, so it sits earlier than the body itself
2424                // — and a head node outside its own list's span is a span bug
2425                // that `every_node_sits_inside_its_parents_span` reports.
2426                let head = Span::new(span.start, span.start);
2427                let mut list = vec![sym_at(head, "begin")];
2428                list.extend(forms);
2429                list_at(span, list)
2430            }
2431        })
2432    }
2433}
2434
2435/// `` `a` ``, `` `a` or `b` ``, `` `a`, `b` or `c` `` — a list of keywords as
2436/// a sentence says it.
2437fn one_of(words: &[&str]) -> String {
2438    let quoted: Vec<String> = words.iter().map(|w| format!("`{w}`")).collect();
2439    match quoted.split_last() {
2440        None => String::new(),
2441        Some((last, [])) => last.clone(),
2442        Some((last, rest)) => format!("{} or {last}", rest.join(", ")),
2443    }
2444}
2445
2446fn let_bound(binder: Spanned, body: Spanned, at: Span) -> Spanned {
2447    let span = at.merge(body.span);
2448    let pair = list_at(binder.span, vec![binder, sym_at(at, RESCUED)]);
2449    list_at(span, vec![sym_at(at, "let"), list_at(pair.span, vec![pair]), body])
2450}
2451
2452fn thunk(body: Spanned, at: Span) -> Spanned {
2453    let span = at.merge(body.span);
2454    list_at(span, vec![sym_at(at, "lambda"), list_at(at, Vec::new()), body])
2455}
2456
2457fn blue_lang_name(name: &str) -> bool {
2458    matches!(
2459        lex(name).as_deref(),
2460        Ok([t, e]) if matches!(&t.kind, TokenKind::Ident(n) if n == name) && e.kind == TokenKind::Eof
2461    ) && !name.ends_with(['?', '!'])
2462}
2463
2464fn mentions(form: &Spanned, name: &str) -> bool {
2465    match &form.form {
2466        SpannedForm::Atom(Atom::Symbol(s)) => s == name,
2467        SpannedForm::List(items) => items.iter().any(|i| mentions(i, name)),
2468        SpannedForm::Quote(i)
2469        | SpannedForm::Quasiquote(i)
2470        | SpannedForm::Unquote(i)
2471        | SpannedForm::UnquoteSplice(i) => mentions(i, name),
2472        _ => false,
2473    }
2474}
2475
2476/// A symbol node at `span`.
2477fn sym_at(span: Span, s: &str) -> Spanned {
2478    Spanned::new(span, SpannedForm::Atom(Atom::Symbol(s.to_string())))
2479}
2480
2481/// An atom node at `span`.
2482fn atom_at(span: Span, a: Atom) -> Spanned {
2483    Spanned::new(span, SpannedForm::Atom(a))
2484}
2485
2486/// A list node at `span`.
2487fn list_at(span: Span, items: Vec<Spanned>) -> Spanned {
2488    Spanned::new(span, SpannedForm::List(items))
2489}
2490
2491/// The smallest span covering every node in `items`, or `fallback` when there
2492/// are none.
2493///
2494/// The fallback is a parameter rather than `Span::synthetic()` because a
2495/// synthetic span is a claim that a node has no source origin at all — and an
2496/// empty parameter list in `def f()` very much has one. Defaulting to synthetic
2497/// here is how "no span" leaks into a tree where every node does have a place.
2498fn cover(items: &[Spanned], fallback: Span) -> Span {
2499    let merged = items
2500        .iter()
2501        .fold(Span::synthetic(), |acc, s| acc.merge(s.span));
2502    if merged.is_synthetic() {
2503        fallback
2504    } else {
2505        merged
2506    }
2507}
2508
2509#[cfg(test)]
2510mod tests {
2511    use super::*;
2512
2513    /// Render an `Sexp` to canonical text so tests can state the expected
2514    /// quoted form as a string. This is `Display`, which tatara-lisp owns —
2515    /// blue does not build Lisp syntax by concatenation.
2516    fn q(src: &str) -> String {
2517        parse_expr(src)
2518            .map(|s| s.to_string())
2519            .unwrap_or_else(|e| panic!("{src:?}: {e}"))
2520    }
2521
2522    // ---- the thesis: Ruby-shaped source becomes tatara-lisp ----------
2523
2524    #[test]
2525    fn arithmetic_respects_precedence() {
2526        assert_eq!(q("1 + 2 * 3"), "(+ 1 (* 2 3))");
2527        assert_eq!(q("(1 + 2) * 3"), "(* (+ 1 2) 3)");
2528    }
2529
2530    #[test]
2531    fn comparison_binds_looser_than_arithmetic() {
2532        assert_eq!(q("a + 1 < b"), "(< (+ a 1) b)");
2533    }
2534
2535    /// The expected tree names `or`/`and`, not `||`/`&&`: the surface
2536    /// spelling is the SURFACE's, and lowering renames it to the form
2537    /// tatara-lisp actually has. This test previously asserted the verbatim
2538    /// spelling, which is how `(== a b)` — a symbol nothing binds — shipped.
2539    #[test]
2540    fn logical_operators_bind_loosest_and_lower_to_tataras_names() {
2541        assert_eq!(q("a && b || c"), "(or (and a b) c)");
2542    }
2543
2544    #[test]
2545    fn left_associativity() {
2546        assert_eq!(q("1 - 2 - 3"), "(- (- 1 2) 3)");
2547    }
2548
2549    #[test]
2550    fn a_dot_without_parentheses_reads_a_field() {
2551        assert_eq!(q("user.name"), "(blue-field user :name)");
2552    }
2553
2554    #[test]
2555    fn method_call_with_args() {
2556        assert_eq!(q("user.greet(1, 2)"), "(greet user 1 2)");
2557    }
2558
2559    #[test]
2560    fn chained_fields_read_left_to_right() {
2561        assert_eq!(q("a.b.c"), "(blue-field (blue-field a :b) :c)");
2562        assert_eq!(q("a.b(1).c"), "(blue-field (b a 1) :c)");
2563    }
2564
2565    #[test]
2566    fn plain_call() {
2567        assert_eq!(q("f(1, 2)"), "(f 1 2)");
2568    }
2569
2570    /// The pipeline threads into the FIRST argument, as Elixir's does —
2571    /// that is what makes `|>` composable rather than decorative.
2572    #[test]
2573    fn pipeline_threads_into_first_argument() {
2574        assert_eq!(q("x |> f"), "(f x)");
2575        assert_eq!(q("x |> f(1)"), "(f x 1)");
2576        assert_eq!(q("x |> f |> g"), "(g (f x))");
2577    }
2578
2579    #[test]
2580    fn pipeline_binds_looser_than_arithmetic() {
2581        assert_eq!(q("1 + 2 |> f"), "(f (+ 1 2))");
2582    }
2583
2584    // ---- §V.13's rendering law, enforced at the parser ---------------
2585
2586    /// `a: 1` and `:a => 1` are the SAME TREE. That is exactly why the
2587    /// formatter may always render the shorthand: they are not two
2588    /// spellings of two things, they are two spellings of one thing.
2589    #[test]
2590    fn label_and_rocket_produce_the_same_tree_for_a_symbol_key() {
2591        assert_eq!(q("{a: 1}"), q("{:a => 1}"));
2592        assert_eq!(q("{a: 1}"), "(hash-map :a 1)");
2593    }
2594
2595    /// And where the key is NOT a plain symbol, the rocket is the only
2596    /// spelling — so it survives because it must, never as a style choice.
2597    #[test]
2598    fn a_string_key_has_no_shorthand() {
2599        assert_eq!(q(r#"{"k" => 1}"#), r#"(hash-map "k" 1)"#);
2600    }
2601
2602    #[test]
2603    fn list_literal() {
2604        assert_eq!(q("[1, 2, 3]"), "(list 1 2 3)");
2605        assert_eq!(q("[]"), "(list)");
2606    }
2607
2608    // ---- blocks ------------------------------------------------------
2609
2610    #[test]
2611    fn if_else_end() {
2612        assert_eq!(q("if a\n  1\nelse\n  2\nend"), "(if a 1 2)");
2613    }
2614
2615    #[test]
2616    fn if_without_else() {
2617        assert_eq!(q("if a\n  1\nend"), "(if a 1)");
2618    }
2619
2620    /// `unless` lowers to `(if (not c) …)` — one tree per meaning, so
2621    /// every downstream tool sees exactly one shape.
2622    #[test]
2623    fn unless_is_a_negated_if() {
2624        assert_eq!(q("unless a\n  1\nend"), "(if (not a) 1)");
2625    }
2626
2627    #[test]
2628    fn multi_statement_body_becomes_begin() {
2629        assert_eq!(q("if a\n  1\n  2\nend"), "(if a (begin 1 2))");
2630    }
2631
2632    #[test]
2633    fn def_lowers_to_define() {
2634        assert_eq!(
2635            q("def add(a, b)\n  a + b\nend"),
2636            "(define (add a b) (+ a b))"
2637        );
2638    }
2639
2640    #[test]
2641    fn def_with_no_params() {
2642        assert_eq!(q("def zero()\n  0\nend"), "(define (zero) 0)");
2643    }
2644
2645    // ---- literals ----------------------------------------------------
2646
2647    #[test]
2648    fn literals_lower_to_atoms() {
2649        assert_eq!(q("42"), "42");
2650        assert_eq!(q("true"), "#t");
2651        assert_eq!(q(":ok"), ":ok");
2652        assert_eq!(q(r#""hi""#), r#""hi""#);
2653    }
2654
2655    #[test]
2656    fn unary_minus_and_not() {
2657        assert_eq!(q("-x"), "(- 0 x)");
2658        assert_eq!(q("!x"), "(not x)");
2659    }
2660
2661    // ---- programs and errors -----------------------------------------
2662
2663    #[test]
2664    fn a_program_is_a_sequence_of_forms() {
2665        let forms = parse_program("def f()\n  1\nend\nf()").expect("parse");
2666        assert_eq!(forms.len(), 2);
2667        assert_eq!(forms[1].to_string(), "(f)");
2668    }
2669
2670    #[test]
2671    fn unterminated_block_is_an_error_naming_what_was_expected() {
2672        let e = parse_program("if a\n  1").expect_err("must fail");
2673        assert!(e.message.contains("unterminated"), "{}", e.message);
2674    }
2675
2676    #[test]
2677    fn a_parse_error_carries_a_span_into_the_source() {
2678        let src = "1 + )";
2679        let e = parse_program(src).expect_err("must fail");
2680        assert!(e.span.start < src.len(), "span {:?} outside source", e.span);
2681    }
2682
2683    // ---- one statement per line -------------------------------------
2684
2685    /// `y = xs[0]` was TWO statements, `(define y xs)` and `(list 0)`: nothing
2686    /// required a separator between statements, so the parser finished `xs`,
2687    /// saw a list literal and started a new statement on the same line. The
2688    /// program ran and `y` was the whole list. Found by the 2026-09-29 audit.
2689    #[test]
2690    fn postfix_indexing_is_refused_naming_nth() {
2691        let e = parse_program("y = xs[0]").expect_err("must not split into two statements");
2692        assert!(e.message.contains("`nth(0, xs)`"), "{}", e.message);
2693        assert_eq!((e.line, e.col), (1, 7), "{e}");
2694    }
2695
2696    /// `a = f x` — Ruby's command call — parsed as `a = f` and then `x`.
2697    #[test]
2698    fn a_call_without_parentheses_is_refused_naming_the_call() {
2699        let e = parse_program("a = f x").expect_err("must not split into two statements");
2700        assert!(e.message.contains("`f(x)`"), "{}", e.message);
2701        assert_eq!((e.line, e.col), (1, 7), "{e}");
2702    }
2703
2704    /// The same rule inside a block body, and in every body shape.
2705    #[test]
2706    fn two_statements_on_one_line_are_refused_in_a_body() {
2707        for src in [
2708            "def f(xs)\n  xs[0]\nend",
2709            "if a\n  f x\nend",
2710            "fn(x) x 1 end",
2711            "test \"t\"\n  assert a b\nend",
2712            "1 2",
2713        ] {
2714            assert!(parse_program(src).is_err(), "{src:?} must not parse");
2715        }
2716    }
2717
2718    /// The controls: every place a statement legitimately ends on its line.
2719    /// A block keyword closing the body on the same line is one of them, which
2720    /// is what keeps a one-line lambda one line.
2721    #[test]
2722    fn a_statement_may_end_at_a_newline_the_end_or_a_closing_keyword() {
2723        for src in [
2724            "fn(x) x + 1 end",
2725            "map(fn(x) x end, xs)",
2726            "if a\n  1\nelse\n  2\nend",
2727            "if a 1 else 2 end",
2728            "case x\nwhen 1 :a\nwhen 2 :b\nelse :c\nend",
2729            "x = 1\ny = 2",
2730            "x = 1",
2731        ] {
2732            assert!(
2733                parse_program(src).is_ok(),
2734                "{src:?}: {:?}",
2735                parse_program(src)
2736            );
2737        }
2738    }
2739
2740    /// `if c then a else b end` read `then` as a statement and `a` as another.
2741    #[test]
2742    fn then_is_refused_by_name() {
2743        let e = parse_program("x = if c then 1 else 2 end").expect_err("then is not a keyword");
2744        assert!(e.message.contains("`then`"), "{}", e.message);
2745    }
2746
2747    /// A brace block after a send is not a map literal the author meant.
2748    #[test]
2749    fn a_brace_block_is_refused_naming_fn() {
2750        let e = parse_program("xs.map { |x| x }").expect_err("no brace blocks");
2751        assert!(e.message.contains("fn(x)"), "{}", e.message);
2752    }
2753
2754    // ---- qualified names ------------------------------------------------
2755
2756    /// `retsu::first(xs)` is one symbol, tatara's `require` shape.
2757    ///
2758    /// Red run (2026-09-29), before the `::` token: `expected an expression,
2759    /// found `:``.
2760    #[test]
2761    fn a_qualified_name_is_one_symbol() {
2762        assert_eq!(q("retsu::first(xs)"), "(retsu/first xs)");
2763        assert_eq!(q("retsu::first"), "retsu/first");
2764        assert_eq!(q("xs |> retsu::first"), "(retsu/first xs)");
2765        assert_eq!(q("blue::length(xs)"), "(blue/length xs)");
2766    }
2767
2768    /// Whitespace around `::` is layout, not meaning.
2769    #[test]
2770    fn whitespace_around_the_qualifier_is_accepted() {
2771        assert_eq!(q("retsu :: first(xs)"), "(retsu/first xs)");
2772    }
2773
2774    /// After `::` a reserved word is a name: `kueri::if` is kueri's
2775    /// definition, not the `if` form.
2776    #[test]
2777    fn a_reserved_word_after_the_qualifier_is_a_name() {
2778        assert_eq!(q("kueri::if(a, b)"), "(kueri/if a b)");
2779        assert_eq!(q("heni::test(x)"), "(heni/test x)");
2780        assert_eq!(q("sabi::fn"), "sabi/fn");
2781    }
2782
2783    #[test]
2784    fn a_qualified_name_has_exactly_one_qualifier() {
2785        for (src, says) in [
2786            ("::first(xs)", "`package::name`"),
2787            ("a::b::c", "one qualifier"),
2788            ("if::x", "reserved word"),
2789            ("retsu::1", "expected a name after `retsu::`"),
2790        ] {
2791            let e = parse_program(src).expect_err(src);
2792            assert!(e.message.contains(says), "{src}: {}", e.message);
2793        }
2794    }
2795
2796    /// The one reader of the shape: division is not qualified, and neither
2797    /// is anything the surface could not have written.
2798    #[test]
2799    fn qualified_reads_only_the_package_name_shape() {
2800        assert_eq!(qualified("retsu/first"), Some(("retsu", "first")));
2801        assert_eq!(qualified("kueri/if"), Some(("kueri", "if")));
2802        assert_eq!(qualified("nisshi/pair?"), Some(("nisshi", "pair?")));
2803        for not in ["/", "a/", "/a", "a/b/c", "if/x", "hash-map", "a"] {
2804            assert_eq!(qualified(not), None, "{not}");
2805        }
2806        assert!(is_callable_name("kueri/if"));
2807    }
2808
2809    /// `:pair?` is the symbol `pair?`: an import list can name a predicate.
2810    ///
2811    /// Red run (2026-09-29), before: `unexpected character `?``, found by
2812    /// `blue migrate` listing nisshi's `pair?`.
2813    #[test]
2814    fn a_symbol_may_end_in_a_question_or_bang() {
2815        assert_eq!(q("[:pair?, :set!]"), "(list :pair? :set!)");
2816        assert_eq!(q("use(\"n\", [:empty?])"), "(use \"n\" (list :empty?))");
2817        assert_eq!(q(":a != :b"), "(not= :a :b)");
2818    }
2819
2820    // ---- errors a person can read -------------------------------------
2821
2822    /// `line:col` and blue's words, never `at 10..11` and `Ident("y")`.
2823    #[test]
2824    fn a_parse_error_displays_line_col_and_the_source_text() {
2825        let e = parse_program("x = 1\ny = [1, 2 y]").expect_err("must fail");
2826        assert_eq!(e.to_string(), "2:11: expected `,` or `]`, found `y`");
2827        assert_eq!(e.span, Span::new(16, 17), "the span stays, as data");
2828    }
2829
2830    /// Every token kind renders without Rust's `Debug` shape.
2831    #[test]
2832    fn no_error_message_names_a_rust_variant() {
2833        for src in [
2834            "f(1 2)",
2835            "{a: 1 b}",
2836            "def 1\nend",
2837            "x.1",
2838            "if a\n",
2839            "fn(1) 2 end",
2840            "[1 :b]",
2841            "1 +",
2842            "(1",
2843        ] {
2844            let e = parse_program(src).expect_err(src);
2845            let shown = e.to_string();
2846            for tell in [
2847                "Ident(", "Int(", "Op(", "Label(", "Sym(", "Str(", "LParen", "RBracket", "Eof",
2848                "Newline", "..",
2849            ] {
2850                assert!(!shown.contains(tell), "{src:?} -> {shown}");
2851            }
2852            assert!(e.line >= 1 && e.col >= 1, "{src:?} not located: {shown}");
2853        }
2854    }
2855
2856    /// A lex error reaches the author through the same door, located.
2857    #[test]
2858    fn a_lex_error_is_located_too() {
2859        let e = parse_program("x = 1\ny = 2; z = 3").expect_err("no semicolons");
2860        assert_eq!((e.line, e.col), (2, 6), "{e}");
2861        assert!(e.message.contains("new line"), "{e}");
2862    }
2863
2864    /// Anti-vacuity: `q` must be able to FAIL. If every input parsed, the
2865    /// assertions above would be worthless.
2866    #[test]
2867    fn the_parser_rejects_garbage() {
2868        assert!(parse_program("def").is_err());
2869        assert!(parse_program("(1").is_err());
2870        assert!(parse_program("end").is_err());
2871    }
2872}