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