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