Skip to main content

blue_lang_syntax/
parse.rs

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