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