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