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