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};
48
49/// Why a source did not parse, and where.
50///
51/// **Structured, and located at the boundary.** `message` says what was
52/// expected and what was found, in blue's own words; `span` is the byte range
53/// of the offending text; `line` and `col` are that range's start, 1-based,
54/// with the column counted in characters. Every public entry point fills
55/// `line`/`col` from the source it was given, so `Display` reads
56/// `3:7: expected `)`, found `]`` and never a byte offset.
57///
58/// The byte offsets used to BE the display (`… at 10..11`), and a Rust
59/// `Debug` rendering named the token (`found Ident("y")`). Both are facts
60/// about the implementation; neither is something an author can act on.
61/// The span stays, as data, for any consumer that needs to point into the
62/// buffer — an editor squiggle, a diagnostic code attached downstream.
63#[derive(Clone, Debug, PartialEq)]
64pub struct ParseError {
65    pub message: String,
66    pub span: Span,
67    /// 1-based line of `span.start`.
68    pub line: usize,
69    /// 1-based column of `span.start`, in characters.
70    pub col: usize,
71}
72
73impl ParseError {
74    /// An error at `span`, not yet located: the parser sees tokens, not text,
75    /// so the line and column are filled by [`Self::located`] at the entry
76    /// point that holds the source.
77    fn new(message: impl Into<String>, span: Span) -> Self {
78        Self {
79            message: message.into(),
80            span,
81            line: 0,
82            col: 0,
83        }
84    }
85
86    /// Resolve `line`/`col` against the source this error's span indexes.
87    fn located(mut self, src: &str) -> Self {
88        let (line, col) = Span::line_col(src, self.span.start.min(src.len()));
89        self.line = line;
90        self.col = col;
91        self
92    }
93}
94
95impl std::fmt::Display for ParseError {
96    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97        write!(f, "{}:{}: {}", self.line, self.col, self.message)
98    }
99}
100
101impl std::error::Error for ParseError {}
102
103impl From<crate::lex::LexError> for ParseError {
104    fn from(e: crate::lex::LexError) -> Self {
105        Self::new(e.message, e.span)
106    }
107}
108
109thread_local! {
110    static PARSES: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
111}
112
113/// How many programs this thread has parsed: an independent count for a
114/// caller that promises to parse a text once, read off the parser itself
115/// rather than off the caller's own bookkeeping.
116#[must_use]
117pub fn parses_on_this_thread() -> u64 {
118    PARSES.with(std::cell::Cell::get)
119}
120
121/// Parse a blue program into a sequence of tatara-lisp forms.
122pub fn parse_program(src: &str) -> Result<Vec<Sexp>, ParseError> {
123    parse_program_with_depth(src, MAX_EXPR_DEPTH)
124}
125
126/// Parse a blue program keeping **every node's** source span.
127///
128/// This is the parser's real output; [`parse_program`] projects the spans away.
129/// Reach for this one whenever a position is going to be shown to a human — an
130/// editor squiggle, a `line:col` in a CLI error, a future debugger's breakpoint
131/// table. Reach for [`parse_program`] only when the consumer genuinely does not
132/// care where anything was.
133pub fn parse_program_tree(src: &str) -> Result<Vec<Spanned>, ParseError> {
134    parse_program_tree_with_depth(src, MAX_EXPR_DEPTH)
135}
136
137/// [`parse_program_tree`] with the nesting bound supplied by the caller.
138pub fn parse_program_tree_with_depth(
139    src: &str,
140    max_depth: usize,
141) -> Result<Vec<Spanned>, ParseError> {
142    PARSES.with(|n| n.set(n.get() + 1));
143    let toks: Vec<Token> = lex(src)
144        .map_err(|e| ParseError::from(e).located(src))?
145        .into_iter()
146        .filter(|t| !matches!(t.kind, TokenKind::Comment(_)))
147        .collect();
148    let mut p = Parser {
149        toks,
150        pos: 0,
151        depth: 0,
152        max_depth,
153    };
154    p.program().map_err(|e| e.located(src))
155}
156
157/// Parse a program written in a [`Yakugo`](crate::yakugo::Yakugo) surface.
158///
159/// The pack is applied to the TOKEN STREAM, between lexing and parsing, so the
160/// parser below is untouched and every keyword site works by construction.
161/// What comes out is the same `Sexp` the English surface produces — that is
162/// the invariant the pack tests assert, and the only thing that makes a
163/// surface a surface rather than a dialect.
164///
165/// # Errors
166///
167/// Lex and parse errors propagate unchanged. A pack cannot repair source that
168/// does not tokenize, and reporting otherwise would name the wrong problem.
169pub fn parse_program_in(src: &str, pack: &crate::yakugo::Yakugo) -> Result<Vec<Sexp>, ParseError> {
170    Ok(parse_program_tree_in(src, pack)?
171        .iter()
172        .map(Spanned::to_sexp)
173        .collect())
174}
175
176/// [`parse_program_in`] keeping **every node's** source span.
177///
178/// The same relation [`parse_program_tree`] has to [`parse_program`], and it
179/// exists for the same reason: a surface is a spelling, so a program written in
180/// one has positions exactly as real as an English-surface program's. Without
181/// this door, `pipeline::run_in_surface` would have to lift a spanless tree back
182/// up and every type error in a `yakugo` program would report `<synthetic>` —
183/// a position the parser HAD and threw away one line earlier, which is the
184/// class `Diagnostic::span`'s own docs name.
185///
186/// [`parse_program_in`] is now its projection, so the two cannot drift.
187///
188/// # Errors
189///
190/// As [`parse_program_in`].
191pub fn parse_program_tree_in(
192    src: &str,
193    pack: &crate::yakugo::Yakugo,
194) -> Result<Vec<Spanned>, ParseError> {
195    PARSES.with(|n| n.set(n.get() + 1));
196    let toks: Vec<Token> = crate::yakugo::canonical_tokens(src, pack)
197        .map_err(|e| ParseError::from(e).located(src))?
198        .into_iter()
199        .filter(|t| !matches!(t.kind, TokenKind::Comment(_)))
200        .collect();
201    let mut p = Parser {
202        toks,
203        pos: 0,
204        depth: 0,
205        max_depth: MAX_EXPR_DEPTH,
206    };
207    p.program().map_err(|e| e.located(src))
208}
209
210/// [`parse_program`] with the nesting bound supplied by the caller.
211///
212/// The bound is a **safety limit, not a dialect**: `max_depth` cannot change
213/// what any program means, only whether a pathological one is refused before
214/// the stack is at risk. That is why it is a parameter here and
215/// [`MAX_EXPR_DEPTH`] is only the default — a knob that could alter meaning
216/// would belong nowhere near a config file (see `blue-lang-cli`'s `config`
217/// module for the rule this obeys).
218pub fn parse_program_with_depth(src: &str, max_depth: usize) -> Result<Vec<Sexp>, ParseError> {
219    Ok(parse_program_tree_with_depth(src, max_depth)?
220        .iter()
221        .map(Spanned::to_sexp)
222        .collect())
223}
224
225/// Parse, keeping each top-level form's source span.
226///
227/// The spans are what let the formatter put comments back. A comment is not
228/// part of a program's *meaning*, so it has no place in the `Sexp` tree —
229/// putting it there would break canonicality, since two programs differing only
230/// in a comment would stop formatting identically. Instead the formatter renders
231/// the tree and re-interleaves comments by position, which needs to know where
232/// each form started and ended.
233///
234/// A projection of [`parse_program_tree`], which carries a span on every node
235/// rather than only the top-level ones. This entry point survives because the
236/// formatter wants exactly the top-level pairing and nothing deeper.
237pub fn parse_program_spanned(src: &str) -> Result<Vec<(Sexp, Span)>, ParseError> {
238    parse_program_spanned_with_depth(src, MAX_EXPR_DEPTH)
239}
240
241/// [`parse_program_spanned`] with the nesting bound supplied by the caller.
242pub fn parse_program_spanned_with_depth(
243    src: &str,
244    max_depth: usize,
245) -> Result<Vec<(Sexp, Span)>, ParseError> {
246    Ok(parse_program_tree_with_depth(src, max_depth)?
247        .iter()
248        .map(|f| (f.to_sexp(), f.span))
249        .collect())
250}
251
252/// Every comment in `src`, with its byte span and whether it sits alone on its
253/// line.
254///
255/// `own_line` is the distinction that decides placement: a comment alone on its
256/// line belongs *before* the form that follows, while one after code on the same
257/// line belongs *to* that line. Conflating them moves a trailing note onto the
258/// wrong row.
259pub fn comments(src: &str) -> Vec<Comment> {
260    lex(src)
261        .map(|toks| {
262            toks.iter()
263                .filter_map(|t| match &t.kind {
264                    TokenKind::Comment(text) => Some(Comment {
265                        text: text.clone(),
266                        span: t.span,
267                        own_line: line_before_is_blank(src, t.span.start),
268                    }),
269                    _ => None,
270                })
271                .collect()
272        })
273        .unwrap_or_default()
274}
275
276/// A comment, and where it sat.
277#[derive(Clone, Debug, PartialEq, Eq)]
278pub struct Comment {
279    /// The text, including the leading `#`.
280    pub text: String,
281    pub span: Span,
282    /// Nothing but whitespace precedes it on its line.
283    pub own_line: bool,
284}
285
286fn line_before_is_blank(src: &str, start: usize) -> bool {
287    src[..start.min(src.len())]
288        .rsplit('\n')
289        .next()
290        .is_some_and(|prefix| prefix.trim().is_empty())
291}
292
293/// Parse a single blue expression. Convenience for tests and the REPL.
294pub fn parse_expr(src: &str) -> Result<Sexp, ParseError> {
295    let forms = parse_program(src)?;
296    match forms.len() {
297        1 => Ok(forms.into_iter().next().expect("checked len")),
298        n => Err(ParseError::new(
299            format!("expected exactly one expression, found {n}"),
300            Span::new(0, src.len()),
301        )
302        .located(src)),
303    }
304}
305
306/// What an infix operator binds like, and what it lowers to.
307///
308/// **One row per operator, carrying both facts.** Precedence and callee
309/// used to live apart, and the cost was immediate: the surface spelling was
310/// emitted verbatim, so `a == b` lowered to `(== a b)` — a symbol no
311/// interpreter binds — and the program died at *runtime* with `unbound
312/// symbol ==`. Splitting the two invites exactly that: add an operator to
313/// the precedence table, forget the lowering, ship a parse that cannot run.
314/// Joined here, "has a precedence" and "has a callee" are the same fact.
315#[derive(Clone, Copy, Debug)]
316pub struct Infix {
317    /// Surface spelling.
318    pub op: &'static str,
319    /// Left and right binding power. Higher binds tighter.
320    pub power: (u8, u8),
321    /// The tatara-lisp callee this lowers to.
322    pub callee: &'static str,
323    /// One line for a reader: what the operator computes, and the trap in it
324    /// when there is one. `blue reference` prints it; a row without one fails
325    /// `every_operator_and_keyword_is_described`.
326    pub doc: &'static str,
327}
328
329/// The complete infix table. Precedence follows Ruby's where Ruby has an
330/// opinion; `|>` (below) sits under everything so `a |> f |> g` chains
331/// without parentheses.
332pub const INFIX: &[Infix] = &[
333    Infix {
334        op: "||",
335        power: (1, 2),
336        callee: "or",
337        doc: "Logical or: the left value if truthy, otherwise the right; the right is evaluated only when needed.",
338    },
339    Infix {
340        op: "&&",
341        power: (3, 4),
342        callee: "and",
343        doc: "Logical and: the right value if the left is truthy, otherwise the left; short-circuits.",
344    },
345    // `==` is STRUCTURAL equality, so it lowers to `equal?` and not to `=`.
346    //
347    // tatara's `=` is NUMERIC comparison: `"a" = "a"` is a type error, not
348    // false. Lowering `==` to it meant every string, list or nil comparison
349    // failed with "expected number, got string" — found by blue's own spec
350    // suite the moment a test compared two strings, which is the first thing
351    // anybody does.
352    //
353    // `equal?` is structural and total over the value domain: strings, lists
354    // and nil all compare, and numbers still compare as numbers.
355    Infix {
356        op: "==",
357        power: (5, 6),
358        callee: "equal?",
359        doc: "Structural equality over every value (okite D0001): 4 == 4.0, lists and maps by content, nil only equals nil.",
360    },
361    Infix {
362        op: "!=",
363        power: (5, 6),
364        callee: "not=",
365        doc: "Exactly not(a == b) (okite D0002).",
366    },
367    Infix {
368        op: "<",
369        power: (5, 6),
370        callee: "<",
371        doc: "Numeric less-than. A string operand is a type error: order text with compare(a, b).",
372    },
373    Infix {
374        op: "<=",
375        power: (5, 6),
376        callee: "<=",
377        doc: "Numeric less-than-or-equal. Numbers only, like <.",
378    },
379    Infix {
380        op: ">",
381        power: (5, 6),
382        callee: ">",
383        doc: "Numeric greater-than. Numbers only, like <.",
384    },
385    Infix {
386        op: ">=",
387        power: (5, 6),
388        callee: ">=",
389        doc: "Numeric greater-than-or-equal. Numbers only, like <.",
390    },
391    Infix {
392        op: "+",
393        power: (7, 8),
394        callee: "+",
395        doc: "Numeric addition; an Int and a Float give a Float. Not string concatenation: interpolate instead.",
396    },
397    Infix {
398        op: "-",
399        power: (7, 8),
400        callee: "-",
401        doc: "Numeric subtraction. Written before an operand with nothing on its left, it is unary minus: -n.",
402    },
403    Infix {
404        op: "*",
405        power: (9, 10),
406        callee: "*",
407        doc: "Numeric multiplication.",
408    },
409    Infix {
410        op: "/",
411        power: (9, 10),
412        callee: "/",
413        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).",
414    },
415    Infix {
416        op: "%",
417        power: (9, 10),
418        callee: "mod",
419        doc: "Euclidean remainder: never negative, so (0 - 7) % 3 is 2, not -1. A zero divisor is a runtime error.",
420    },
421];
422
423/// Every surface keyword that BEGINS an expression.
424///
425/// Exists so the formatter's corpus can be checked against it. Three separate
426/// times a form was added to this parser, the formatter was not extended, and
427/// all three formatting laws still passed — because the corpus is
428/// hand-maintained and contained no example of the new form. The annotated
429/// `def` rendered as a method send; `defmacro` rendered as
430/// `defmacro(double, x(), …)`, which does not re-parse at all.
431///
432/// A law cannot notice a case nobody wrote down. `blue-lang-fmt`'s
433/// `every_surface_keyword_appears_in_the_corpus` closes that by making the
434/// omission itself the failure, so adding a keyword here forces the corpus
435/// entry that exercises the other three laws over it.
436///
437/// `do` and `end` are absent deliberately: they are block delimiters, not
438/// expression heads, and have no standalone rendering to test.
439/// The callee `assert e` lowers to.
440///
441/// **Owned here, by the lowering itself, and read by every consumer.** It was
442/// briefly a literal here and a separate `const` in `blue-lang-test`, and the
443/// shadowing gate then could not see a parser that lowered to the wrong name:
444/// the gate checked its own copy. Same duplication class as the operator table.
445pub const LOWERED_ASSERT: &str = "blue-assert";
446
447/// The callee a `{...}` map literal lowers to.
448///
449/// **`hash-map`, not `map`.** tatara binds `map` to the higher-order function,
450/// so a literal lowering to `(map …)` called the HOF with the key/value pairs
451/// as arguments and failed with "expected list, got int". Same shadowing class
452/// as [`LOWERED_ASSERT`], caught by the same gate once the name was listed
453/// there — it was not, which is why this shipped.
454pub const LOWERED_MAP: &str = "hash-map";
455
456/// The callee string interpolation lowers to.
457///
458/// blue's own `concat`, which renders either side through `to_s` — that is what
459/// lets `"n=#{42}"` interpolate a number. Not `+`, which is arithmetic.
460pub const LOWERED_CONCAT: &str = "concat";
461
462pub const SURFACE_KEYWORDS: &[&str] = &[
463    "if",
464    "unless",
465    "def",
466    "defmacro",
467    "quote",
468    "unquote",
469    "unquote_splice",
470    "test",
471    "assert",
472    "fn",
473    "case",
474];
475
476/// The words that are reserved but are not heads of a `SURFACE_KEYWORDS` form
477/// — block delimiters and the three literals.
478///
479/// Split out as data rather than inlined in a `matches!` because a reserved
480/// word is not only the parser's business: a highlighter has to paint exactly
481/// this set, and one that reads `SURFACE_KEYWORDS` alone silently under-paints
482/// `do` / `end` / `else` / `elsif`. That was not hypothetical — escriba's
483/// `escriba-render/src/langs.rs` hand-transcribed "SURFACE_KEYWORDS plus the
484/// four block words `is_reserved_word` adds", because the union was
485/// unreachable from outside this module. A transcription is a second
486/// definition, and it drifts the first time this list changes.
487pub const BLOCK_KEYWORDS: &[&str] = &["do", "end", "else", "elsif", "true", "false", "nil"];
488
489/// A surface keyword may not be rebound. `if = 1` is a mistake, not a binding,
490/// and letting it through would shadow the form for the rest of the file.
491///
492/// Public because it is the ONE answer to "is this identifier a keyword" —
493/// the parser's rebinding check and every downstream highlighter read it here
494/// rather than each carrying its own copy of the list.
495pub fn is_reserved_word(name: &str) -> bool {
496    SURFACE_KEYWORDS.contains(&name) || BLOCK_KEYWORDS.contains(&name)
497}
498
499/// Can `name` be written as the head of a call — `name(…)`?
500///
501/// True exactly when the lexer reads `name` as one identifier and it is not
502/// reserved. Answered BY the lexer rather than by a character class copied
503/// from it, so the two cannot disagree: the formatter asks this before
504/// printing a tree as `name(args)`, and a head such as `hash-map`, `not=` or
505/// `if` — which a tree can carry and a call cannot spell — must get a
506/// different rendering or none.
507pub fn is_callable_name(name: &str) -> bool {
508    // `retsu/first` is written `retsu::first(…)`.
509    if qualified(name).is_some() {
510        return true;
511    }
512    !is_reserved_word(name)
513        && matches!(
514            lex(name).as_deref(),
515            Ok([t, e]) if matches!(&t.kind, TokenKind::Ident(n) if n == name)
516                && e.kind == TokenKind::Eof
517        )
518}
519
520/// The character a qualified name's AST symbol joins its two halves with:
521/// `retsu::first` is the symbol `retsu/first`. The surface never writes it
522/// (`/` is division, and no identifier can contain it), which is what makes
523/// a symbol with text on both sides of one `/` unambiguously qualified.
524pub const QUALIFIER: char = '/';
525
526/// The qualifier that names a builtin: `blue::count` is the interpreter's
527/// `count`, whatever a program defines. No package may be named it.
528pub const BUILTIN_QUALIFIER: &str = "blue";
529
530/// The AST symbol for `pkg::name`.
531#[must_use]
532pub fn qualify(pkg: &str, name: &str) -> String {
533    format!("{pkg}{QUALIFIER}{name}")
534}
535
536/// `retsu/first` → `Some(("retsu", "first"))`: the package and the name of a
537/// qualified symbol. `None` for every other symbol, `/` (division) included.
538///
539/// The ONE reader of the shape, so the formatter, the name table and the
540/// runtime's renderer cannot disagree about what counts as qualified.
541#[must_use]
542pub fn qualified(sym: &str) -> Option<(&str, &str)> {
543    let (pkg, name) = sym.split_once(QUALIFIER)?;
544    let ident = |s: &str| {
545        matches!(
546            lex(s).as_deref(),
547            Ok([t, e]) if matches!(&t.kind, TokenKind::Ident(n) if n == s) && e.kind == TokenKind::Eof
548        )
549    };
550    let named = |s: &str| ident(s) || matches!(s, "true" | "false" | "nil");
551    (ident(pkg) && !is_reserved_word(pkg) && named(name)).then_some((pkg, name))
552}
553
554/// One line for a reader on each reserved word: what it writes, and the shape.
555///
556/// Beside the two tables rather than inside them, because both are `&[&str]`
557/// read by the formatter's corpus gate and by highlighters outside this repo;
558/// changing their type would break every one of those readers.
559/// `every_operator_and_keyword_is_described` makes a word without a line here a
560/// red build, so the omission cannot ship. `blue reference` prints these.
561#[must_use]
562pub fn keyword_doc(word: &str) -> &'static str {
563    match word {
564        "if" => "`if c … elsif d … else … end`: an expression; its value is the branch taken, nil when none is.",
565        "unless" => "`unless c … end`: `if !c`. The formatter writes it as `if !c`.",
566        "def" => "`def name(a, b) … end`: a named function; its value is the last expression. Parameters may be typed: `def f(n: Int) -> Int`.",
567        "defmacro" => "`defmacro name(x) quote … unquote(x) … end end`: a macro, a rewrite of the unevaluated form.",
568        "quote" => "`quote … end`: the form itself as data, not its value. Used inside `defmacro`.",
569        "unquote" => "`unquote(x)`: inside `quote`, splice in the value of `x`.",
570        "unquote_splice" => "`unquote_splice(xs)`: inside `quote`, splice the items of the form `xs` in place, not the form itself.",
571        "test" => "`test \"what must hold\" … end`: a test block, run by `blue test` and ignored by `blue run`.",
572        "assert" => "`assert expr`: inside a test, fail it unless `expr` is truthy; the failure quotes the expression.",
573        "fn" => "`fn(x) x * 2 end`: an anonymous function, a value. Blue has no brace or do blocks.",
574        "case" => "`case v when 1 … when 2 … else … end`: compares with `==`; it does not destructure.",
575        "do" => "Reserved and refused: blue has no do-blocks. Pass `fn(x) … end` instead.",
576        "end" => "Closes `def`, `defmacro`, `if`, `case`, `fn`, `test` and `quote`.",
577        "else" => "The branch of `if` or `case` taken when nothing before it matched.",
578        "elsif" => "A further condition in an `if` chain; the formatter writes nested `else if` as `elsif`.",
579        "true" => "The boolean true.",
580        "false" => "The boolean false. Only `false` and `nil` are falsy; 0, \"\" and [] are truthy.",
581        "nil" => "The absent value. It is not `[]` (okite D0004), and only `nil == nil`.",
582        _ => "",
583    }
584}
585
586fn infix(op: &str) -> Option<&'static Infix> {
587    INFIX.iter().find(|i| i.op == op)
588}
589
590const PIPE_POWER: (u8, u8) = (0, 1);
591
592struct Parser {
593    toks: Vec<Token>,
594    pos: usize,
595    /// Current expression-nesting depth, bounded by [`Self::max_depth`].
596    ///
597    /// Without this the parser does not fail on deep input — it **aborts the
598    /// process** with a stack overflow (SIGABRT), which `catch_unwind` cannot
599    /// catch. Measured 2026-08-01: `"(".repeat(2_000)` killed the test runner
600    /// outright. Every consumer inherited it — an LSP parsing a half-typed
601    /// line, a formatter, and shikumi loading a `.b` config off disk.
602    depth: usize,
603    /// The bound [`Self::depth`] is checked against.
604    ///
605    /// Defaults to [`MAX_EXPR_DEPTH`] on every entry point that does not name
606    /// one; `parse_program_with_depth` exists so an operator can raise it
607    /// without recompiling. It is a **bound**, so raising it changes no
608    /// program's meaning — only which pathological inputs are refused.
609    max_depth: usize,
610}
611
612/// Maximum expression nesting before the parser refuses.
613///
614/// Chosen well above anything human-written (blue's own `spec/*.b` peaks in
615/// single digits) and far below the measured overflow point, so the bound is
616/// hit as a typed `Err` long before the stack is at risk. A limit that is
617/// merely *near* the crash point is not a safety bound; it is a race.
618pub const MAX_EXPR_DEPTH: usize = 256;
619
620/// The parser's internal error: a [`ParseError`] behind a pointer.
621///
622/// Every production returns a `Result`, and a debug build keeps several per
623/// frame down the recursion, so the error's size is paid at every level of
624/// nesting whether or not anything fails. Measured 2026-09-29 on a 2 MiB
625/// debug thread, parsing `(((…1…)))` with no bound: the unboxed 40-byte error
626/// overflowed between 255 and 260 levels, barely past [`MAX_EXPR_DEPTH`];
627/// adding `line`/`col` unboxed moved it to 240–250, under the bound, and
628/// `depth_beyond_the_limit_is_an_err_not_an_abort` aborted. Boxed, the
629/// overflow is between 300 and 310. The error path is cold; the pointer is
630/// what keeps the bound a bound.
631type Fail = Box<ParseError>;
632
633fn fail(message: impl Into<String>, span: Span) -> Fail {
634    Box::new(ParseError::new(message, span))
635}
636
637impl Parser {
638    fn peek(&self) -> &TokenKind {
639        &self.toks[self.pos.min(self.toks.len() - 1)].kind
640    }
641
642    fn peek_span(&self) -> Span {
643        self.toks[self.pos.min(self.toks.len() - 1)].span
644    }
645
646    fn bump(&mut self) -> TokenKind {
647        let k = self.toks[self.pos.min(self.toks.len() - 1)].kind.clone();
648        if self.pos < self.toks.len() {
649            self.pos += 1;
650        }
651        k
652    }
653
654    fn at(&self, k: &TokenKind) -> bool {
655        self.peek() == k
656    }
657
658    fn eat(&mut self, k: &TokenKind) -> bool {
659        if self.at(k) {
660            self.bump();
661            true
662        } else {
663            false
664        }
665    }
666
667    fn expect(&mut self, k: &TokenKind, what: &str) -> Result<(), Fail> {
668        if self.eat(k) {
669            Ok(())
670        } else {
671            Err(self.error(format!("expected {what}, found {}", self.peek())))
672        }
673    }
674
675    fn error(&self, message: impl Into<String>) -> Fail {
676        fail(message, self.peek_span())
677    }
678
679    /// Skip statement separators (newlines and semicolon-free layout).
680    fn skip_newlines(&mut self) {
681        while matches!(self.peek(), TokenKind::Newline) {
682            self.bump();
683        }
684    }
685
686    fn at_ident(&self, name: &str) -> bool {
687        matches!(self.peek(), TokenKind::Ident(n) if n == name)
688    }
689
690    /// Byte offset where the next token begins — the anchor a production spans
691    /// *from*. Paired with [`Self::span_since`].
692    fn mark(&self) -> usize {
693        self.peek_span().start
694    }
695
696    /// The span from `start` to the end of the **last consumed** token.
697    ///
698    /// `pos` has already advanced past that token, so this looks one back. Call
699    /// it after a production has consumed everything it owns; calling it early
700    /// yields a span that stops short of the node it describes.
701    fn span_since(&self, start: usize) -> Span {
702        let end = self
703            .toks
704            .get(self.pos.saturating_sub(1))
705            .map_or(start, |t| t.span.end);
706        Span::new(start, end.max(start))
707    }
708
709    fn program(&mut self) -> Result<Vec<Spanned>, Fail> {
710        let mut out = Vec::new();
711        loop {
712            self.skip_newlines();
713            if matches!(self.peek(), TokenKind::Eof) {
714                break;
715            }
716            out.push(self.statement()?);
717            self.end_of_statement(&[])?;
718        }
719        Ok(out)
720    }
721
722    /// **A statement ends at the end of its line.** Refuse anything else.
723    ///
724    /// The lexer has always said newlines "are statement separators", and the
725    /// parser never asked for one: `program` and `body` looped over
726    /// `statement()` and started the next one wherever the last stopped. So a
727    /// form blue does not have became two forms it does, silently —
728    /// `y = xs[0]` parsed as `y = xs` then the list `[0]`, and Ruby's
729    /// `a = f x` as `a = f` then `x`. Both ran; both bound the wrong value.
730    ///
731    /// **Refused rather than given a meaning**, because each would be a second
732    /// spelling of a tree blue already spells one way (§0's FORM axis):
733    /// indexing is the call `nth(i, xs)`, and a call has parentheses. `;` is
734    /// refused by the lexer for the same reason — one separator, the newline.
735    ///
736    /// What may follow a statement: a newline, the end of the file, or — in a
737    /// body — the keyword that closes it, which is what keeps
738    /// `fn(x) x + 1 end` a one-line lambda.
739    fn end_of_statement(&self, terminators: &[&str]) -> Result<(), Fail> {
740        let next = self.peek();
741        if matches!(next, TokenKind::Newline | TokenKind::Eof)
742            || terminators.iter().any(|t| self.at_ident(t))
743        {
744            return Ok(());
745        }
746        let prev = self.toks.get(self.pos.wrapping_sub(1)).map(|t| &t.kind);
747        let prev_name = match prev {
748            Some(TokenKind::Ident(n)) => Some(n.as_str()),
749            _ => None,
750        };
751        let callee = prev_name.unwrap_or("f");
752        let why = match (prev, next) {
753            (Some(TokenKind::Ident(t)), _) if t == "then" => {
754                "blue's `if` has no `then`: put the branch on the next line".to_string()
755            }
756            (_, TokenKind::Ident(d)) if d == "do" => "blue has no `do` blocks: pass a function \
757                 as an argument, `f(xs, fn(x) … end)`"
758                .to_string(),
759            (_, TokenKind::LBracket) => {
760                let index = match (self.toks.get(self.pos + 1), self.toks.get(self.pos + 2)) {
761                    (Some(i), Some(close)) if close.kind == TokenKind::RBracket => match &i.kind {
762                        TokenKind::Int(n) => n.to_string(),
763                        TokenKind::Ident(n) => n.clone(),
764                        _ => "i".to_string(),
765                    },
766                    _ => "i".to_string(),
767                };
768                let list = prev_name.unwrap_or("xs");
769                format!(
770                    "blue has no `xs[i]` indexing: write `nth({index}, {list})` \
771                     (or `first({list})`, `last({list})`)"
772                )
773            }
774            (_, TokenKind::LBrace) => "blue has no brace blocks: pass a function as an \
775                 argument, `map(xs, fn(x) … end)`"
776                .to_string(),
777            (
778                Some(TokenKind::Ident(_)),
779                TokenKind::Ident(_)
780                | TokenKind::Int(_)
781                | TokenKind::Float(_)
782                | TokenKind::Str(_)
783                | TokenKind::InterpolatedStr { .. }
784                | TokenKind::Sym(_)
785                | TokenKind::True
786                | TokenKind::False
787                | TokenKind::Nil,
788            ) => {
789                let arg = match next {
790                    TokenKind::Ident(a) => a.clone(),
791                    _ => "x".to_string(),
792                };
793                format!("a call needs parentheses: write `{callee}({arg})`")
794            }
795            _ => "put each statement on its own line".to_string(),
796        };
797        Err(self.error(format!("expected the end of the line, found {next}: {why}")))
798    }
799
800    /// A statement: either a binding or an expression.
801    ///
802    /// `x = 5` lowers to `(define x 5)`. Blue had NO way to name a value — a
803    /// capability probe found `x = 5` was a parse error, which makes every
804    /// program a single expression. That is more fundamental than anything else
805    /// the probe found.
806    ///
807    /// Only at STATEMENT position, never inside an expression, so `f(x = 1)` is
808    /// still an error rather than a silent binding. Ruby allows assignment as an
809    /// expression and it is a well-known footgun — `if x = 1` where `==` was
810    /// meant. Blue declines it, and the cost is only that a walrus-style idiom
811    /// has to be two lines.
812    fn statement(&mut self) -> Result<Spanned, Fail> {
813        // The SECOND recursion cycle, and it needs the same guard as `expr`.
814        //
815        // Guarding `expr` alone was not enough — measured 2026-08-01: with the
816        // expression bound in place, `"def a\n".repeat(2_000)` STILL aborted
817        // the process. Block nesting (`def` opening a body that contains more
818        // statements) recurses through here, not through `expr`, so a fix
819        // applied to one cycle silently left the other reachable. Two paths to
820        // the same crash; one guard covered one of them.
821        //
822        // Shares `self.depth` with `expr` on purpose: what the stack cares
823        // about is TOTAL nesting, not which grammar production produced it, so
824        // two independent counters would each permit their own full budget and
825        // together exceed what the stack can hold.
826        let max = self.max_depth;
827        if self.depth >= max {
828            return Err(self.error(format!(
829                "statement nests deeper than {max}; refusing to \
830                 recurse further (this is a limit, not a syntax error)"
831            )));
832        }
833        self.depth += 1;
834        let r = self.statement_inner();
835        self.depth -= 1;
836        r
837    }
838
839    fn statement_inner(&mut self) -> Result<Spanned, Fail> {
840        if let TokenKind::Ident(name) = self.peek().clone() {
841            if self.peek_at(1) == "=" && !is_reserved_word(&name) {
842                let name_span = self.peek_span();
843                self.bump(); // name
844                self.bump(); // =
845                self.skip_newlines();
846                let value = self.expr(0)?;
847                // `define` is synthesized — `x = 5` contains no such word — so
848                // it takes the span of the name being bound, which is what a
849                // reader looks at when told something about the binding.
850                return Ok(list_at(
851                    self.span_since(name_span.start),
852                    vec![sym_at(name_span, "define"), sym_at(name_span, &name), value],
853                ));
854            }
855        }
856        self.expr(0)
857    }
858
859    /// The token `n` positions ahead, for the two-token lookahead a binding
860    /// needs. Returns `Eof` past the end rather than panicking.
861    fn peek_at(&self, n: usize) -> String {
862        match self.toks.get(self.pos + n).map(|t| &t.kind) {
863            Some(TokenKind::Op(o)) => o.clone(),
864            _ => String::new(),
865        }
866    }
867
868    /// Pratt loop.
869    fn expr(&mut self, min_bp: u8) -> Result<Spanned, Fail> {
870        // Depth guard at the single recursion cycle (`expr` -> `prefix` ->
871        // `expr`). Returning an Err here converts an UNRECOVERABLE abort into
872        // an ordinary parse failure a caller can render — the difference
873        // between an LSP showing a squiggle and an LSP being gone.
874        //
875        // The decrement is deliberately not RAII: every exit from this
876        // function is via `?` or a normal return, and both are covered by the
877        // explicit decrements below. A guard object would be tidier but would
878        // also hide the invariant this comment is here to state.
879        let max = self.max_depth;
880        if self.depth >= max {
881            return Err(self.error(format!(
882                "expression nests deeper than {max}; refusing to \
883                 recurse further (this is a limit, not a syntax error)"
884            )));
885        }
886        self.depth += 1;
887        let r = self.expr_inner(min_bp);
888        self.depth -= 1;
889        r
890    }
891
892    fn expr_inner(&mut self, min_bp: u8) -> Result<Spanned, Fail> {
893        let start = self.mark();
894        let mut lhs = self.prefix()?;
895
896        loop {
897            // Postfix: `.name`, `.name(args)`, `(args)`
898            match self.peek() {
899                TokenKind::Dot => {
900                    self.bump();
901                    lhs = self.finish_send(start, lhs)?;
902                    continue;
903                }
904                TokenKind::LParen => {
905                    // A call on an expression already parsed: `f(x)`.
906                    let args = self.paren_args()?;
907                    let mut list = vec![lhs];
908                    list.extend(args);
909                    lhs = list_at(self.span_since(start), list);
910                    continue;
911                }
912                _ => {}
913            }
914
915            // Infix
916            // `callee == None` marks the pipeline, which is a rewrite rather
917            // than a call.
918            let op_span = self.peek_span();
919            let (callee, (lbp, rbp)) = match self.peek() {
920                TokenKind::Pipe => (None, PIPE_POWER),
921                TokenKind::Op(o) => match infix(o) {
922                    Some(i) => (Some(i.callee), i.power),
923                    None => break,
924                },
925                _ => break,
926            };
927            if lbp < min_bp {
928                break;
929            }
930            self.bump();
931            self.skip_newlines();
932            let rhs = self.expr(rbp)?;
933
934            lhs = if callee.is_none() {
935                // `x |> f`      => (f x)
936                // `x |> f(a)`   => (f x a)   — the pipeline threads into
937                //                  the FIRST argument position, as Elixir's
938                //                  does; that is what makes it composable.
939                let rhs_span = rhs.span;
940                match rhs.form {
941                    SpannedForm::List(mut items) if !items.is_empty() => {
942                        items.insert(1, lhs);
943                        list_at(self.span_since(start), items)
944                    }
945                    callee => list_at(
946                        self.span_since(start),
947                        vec![Spanned::new(rhs_span, callee), lhs],
948                    ),
949                }
950            } else {
951                // The callee symbol takes the OPERATOR's span, not the whole
952                // expression's: `a + b` lowers to `(+ a b)` and the `+` node is
953                // the one byte-range in the source that means it. Handing it the
954                // whole expression's span would make a diagnostic about the
955                // operator underline both operands too.
956                list_at(
957                    self.span_since(start),
958                    vec![sym_at(op_span, callee.unwrap()), lhs, rhs],
959                )
960            };
961        }
962
963        Ok(lhs)
964    }
965
966    fn prefix(&mut self) -> Result<Spanned, Fail> {
967        let span = self.peek_span();
968        match self.bump() {
969            TokenKind::Int(v) => Ok(atom_at(span, Atom::Int(v))),
970            TokenKind::Float(v) => Ok(atom_at(span, Atom::Float(v))),
971            TokenKind::Str(s) => Ok(atom_at(span, Atom::Str(s))),
972
973            // `"a#{x}b"` → `(concat (concat "a" x) "b")`.
974            //
975            // Lowered to `concat`, not to `+`: blue's `+` is arithmetic (see
976            // the INFIX table), and interpolation must render a value of ANY
977            // type — `concat` goes through `to_s`, which is what makes
978            // `"n=#{42}"` work.
979            //
980            // The expression source is parsed HERE with the ordinary parser
981            // rather than lexed inside the string, so an interpolation can hold
982            // anything an expression can and the two can never drift.
983            TokenKind::InterpolatedStr { parts, exprs } => {
984                // Every node here carries the WHOLE string literal's span.
985                //
986                // The inner expression is parsed from `raw` — a substring — so
987                // its own offsets index that substring, not `src`. Carrying them
988                // through would produce spans that look real and point into a
989                // buffer nobody has: for `"a#{x}"` at offset 40, `x`'s span
990                // would be 0..1, i.e. the start of the file. The lexer does not
991                // record where each part sat in the outer source, so the string
992                // literal is the most precise honest answer available.
993                let mut acc = atom_at(span, Atom::Str(parts[0].clone()));
994                for (i, raw) in exprs.iter().enumerate() {
995                    let inner = parse_expr(raw).map_err(|e| {
996                        fail(
997                            format!("in interpolation `#{{{raw}}}`: {}", e.message),
998                            span,
999                        )
1000                    })?;
1001                    let inner = Spanned::from_sexp_at(&inner, span);
1002                    acc = list_at(span, vec![sym_at(span, LOWERED_CONCAT), acc, inner]);
1003                    // `parts.len() == exprs.len() + 1` by construction, so this
1004                    // index is always in range.
1005                    acc = list_at(
1006                        span,
1007                        vec![
1008                            sym_at(span, LOWERED_CONCAT),
1009                            acc,
1010                            atom_at(span, Atom::Str(parts[i + 1].clone())),
1011                        ],
1012                    );
1013                }
1014                Ok(acc)
1015            }
1016            TokenKind::Sym(s) => Ok(atom_at(span, Atom::Keyword(s))),
1017            TokenKind::True => Ok(atom_at(span, Atom::Bool(true))),
1018            TokenKind::False => Ok(atom_at(span, Atom::Bool(false))),
1019            TokenKind::Nil => Ok(Spanned::new(span, SpannedForm::Nil)),
1020
1021            TokenKind::Op(o) if o == "-" => {
1022                let rhs = self.expr(11)?; // binds tighter than `*`
1023                Ok(list_at(
1024                    self.span_since(span.start),
1025                    vec![sym_at(span, "-"), atom_at(span, Atom::Int(0)), rhs],
1026                ))
1027            }
1028            TokenKind::Op(o) if o == "!" => {
1029                let rhs = self.expr(11)?;
1030                Ok(list_at(
1031                    self.span_since(span.start),
1032                    vec![sym_at(span, "not"), rhs],
1033                ))
1034            }
1035
1036            TokenKind::LParen => {
1037                self.skip_newlines();
1038                let inner = self.expr(0)?;
1039                self.skip_newlines();
1040                self.expect(&TokenKind::RParen, "`)`")?;
1041                // The inner expression's own span, NOT one widened to include
1042                // the parentheses: grouping is not part of the expression's
1043                // meaning, and `(a + b)` should underline `a + b`.
1044                Ok(inner)
1045            }
1046
1047            TokenKind::LBracket => self.list_literal(span),
1048            TokenKind::LBrace => self.map_literal(span),
1049
1050            TokenKind::Ident(name) if self.at(&TokenKind::PathSep) => {
1051                self.qualified_name(span, &name)
1052            }
1053            TokenKind::PathSep => Err(fail(
1054                "a qualified name is `package::name`, with the package written before `::`",
1055                span,
1056            )),
1057            TokenKind::Ident(name) => match name.as_str() {
1058                "if" => self.if_form(span, false),
1059                "unless" => self.if_form(span, true),
1060                "def" => self.def_form(span),
1061                "defmacro" => self.defmacro_form(span),
1062                "case" => self.case_form(span),
1063                "fn" => self.lambda_form(span),
1064                "test" => self.test_form(span),
1065                "assert" => self.assert_form(span),
1066                "quote" => self.quote_form(span),
1067                "unquote" => self.unquote_form(span, false),
1068                "unquote_splice" => self.unquote_form(span, true),
1069                "do" => Err(fail(
1070                    "blue has no `do` blocks: pass a function as an argument, \
1071                     `f(xs, fn(x) … end)`",
1072                    span,
1073                )),
1074                "end" => Err(fail(
1075                    "unexpected `end`: nothing open here for it to close",
1076                    span,
1077                )),
1078                _ => Ok(sym_at(span, &name)),
1079            },
1080
1081            other => Err(fail(format!("expected an expression, found {other}"), span)),
1082        }
1083    }
1084
1085    /// After `package`, at `::`: `retsu::first` lowers to the ONE symbol
1086    /// `retsu/first` — tatara's `require` shape, so a qualified name means the
1087    /// same thing in both lisps.
1088    ///
1089    /// After `::` any identifier is a name, reserved words included:
1090    /// `kueri::if` names kueri's definition, never the `if` form, because a
1091    /// reserved word is reserved only where it could start a form. The
1092    /// package side is never a reserved word, and a name has exactly one
1093    /// qualifier (`a::b::c` is refused), since a package is one level deep.
1094    fn qualified_name(&mut self, pkg_span: Span, pkg: &str) -> Result<Spanned, Fail> {
1095        if is_reserved_word(pkg) {
1096            return Err(fail(
1097                format!("`{pkg}` is a reserved word and cannot name a package"),
1098                pkg_span,
1099            ));
1100        }
1101        self.bump(); // ::
1102        let name_span = self.peek_span();
1103        let name = match self.bump() {
1104            TokenKind::Ident(n) => n,
1105            // `true`, `false` and `nil` lex as their own tokens.
1106            TokenKind::True => "true".to_string(),
1107            TokenKind::False => "false".to_string(),
1108            TokenKind::Nil => "nil".to_string(),
1109            other => {
1110                return Err(fail(
1111                    format!("expected a name after `{pkg}::`, found {other}"),
1112                    name_span,
1113                ))
1114            }
1115        };
1116        if self.at(&TokenKind::PathSep) {
1117            return Err(self.error(format!(
1118                "a qualified name has one qualifier: `package::name`, and `{pkg}::{name}::…` has two"
1119            )));
1120        }
1121        Ok(sym_at(
1122            self.span_since(pkg_span.start),
1123            &qualify(pkg, &name),
1124        ))
1125    }
1126
1127    /// After a `.`: `recv.name` or `recv.name(args)`.
1128    ///
1129    /// **A bare `recv.name` is a SEND, not a field read.** Blue commits to
1130    /// the uniform access principle here: a structure exposes no public
1131    /// fields, so a field can later become a computed method without
1132    /// breaking a caller.
1133    fn finish_send(&mut self, start: usize, recv: Spanned) -> Result<Spanned, Fail> {
1134        let name_span = self.peek_span();
1135        let name = match self.bump() {
1136            // A RESERVED WORD is not a method name.
1137            //
1138            // Found by the formatter property suite, minimal input `1.def`.
1139            // The send parsed happily into `(def 1)` — `def` is just an
1140            // identifier to the lexer — and the formatter then rendered that
1141            // as `def(1)`, which the parser rejects. So `format` turned valid
1142            // source into source that does not parse: corruption, not a style
1143            // choice, and silent until a round-trip property looked.
1144            //
1145            // Rejecting here rather than teaching the formatter to quote it
1146            // fixes the class instead of the symptom: `x.if`, `x.end` and
1147            // `x.case` all lower onto special forms the same way, and none of
1148            // them is a method anyone meant to call.
1149            TokenKind::Ident(n) if is_reserved_word(&n) => {
1150                return Err(self.error(format!(
1151                    "`{n}` is a reserved word and cannot be a method name — \
1152                     `recv.{n}` would lower onto the `{n}` form itself"
1153                )))
1154            }
1155            TokenKind::Ident(n) => n,
1156            other => {
1157                return Err(fail(
1158                    format!("expected a method name after `.`, found {other}"),
1159                    name_span,
1160                ))
1161            }
1162        };
1163        let mut list = vec![sym_at(name_span, &name), recv];
1164        if self.at(&TokenKind::LParen) {
1165            list.extend(self.paren_args()?);
1166        }
1167        Ok(list_at(self.span_since(start), list))
1168    }
1169
1170    fn paren_args(&mut self) -> Result<Vec<Spanned>, Fail> {
1171        self.expect(&TokenKind::LParen, "`(`")?;
1172        let mut args = Vec::new();
1173        self.skip_newlines();
1174        if self.eat(&TokenKind::RParen) {
1175            return Ok(args);
1176        }
1177        loop {
1178            self.skip_newlines();
1179            args.push(self.expr(0)?);
1180            self.skip_newlines();
1181            if self.eat(&TokenKind::Comma) {
1182                continue;
1183            }
1184            self.expect(&TokenKind::RParen, "`,` or `)`")?;
1185            break;
1186        }
1187        Ok(args)
1188    }
1189
1190    fn list_literal(&mut self, open: Span) -> Result<Spanned, Fail> {
1191        let mut items = vec![sym_at(open, "list")];
1192        self.skip_newlines();
1193        if self.eat(&TokenKind::RBracket) {
1194            return Ok(list_at(self.span_since(open.start), items));
1195        }
1196        loop {
1197            self.skip_newlines();
1198            items.push(self.expr(0)?);
1199            self.skip_newlines();
1200            if self.eat(&TokenKind::Comma) {
1201                continue;
1202            }
1203            self.expect(&TokenKind::RBracket, "`,` or `]`")?;
1204            break;
1205        }
1206        Ok(list_at(self.span_since(open.start), items))
1207    }
1208
1209    /// `{a: 1, "k" => v}` — both spellings, one tree.
1210    ///
1211    /// This is §V.13's rendering law at the parser: `a: 1` and `:a => 1`
1212    /// produce the *same* s-expression, which is precisely why the
1213    /// formatter may always choose the shorthand. The rocket survives only
1214    /// where the key is not a plain symbol.
1215    fn map_literal(&mut self, open: Span) -> Result<Spanned, Fail> {
1216        let mut items = vec![sym_at(open, LOWERED_MAP)];
1217        self.skip_newlines();
1218        if self.eat(&TokenKind::RBrace) {
1219            return Ok(list_at(self.span_since(open.start), items));
1220        }
1221        loop {
1222            self.skip_newlines();
1223            match self.peek().clone() {
1224                TokenKind::Label(name) => {
1225                    let label_span = self.peek_span();
1226                    self.bump();
1227                    self.skip_newlines();
1228                    items.push(atom_at(label_span, Atom::Keyword(name)));
1229                    items.push(self.expr(0)?);
1230                }
1231                _ => {
1232                    let k = self.expr(0)?;
1233                    self.skip_newlines();
1234                    self.expect(&TokenKind::Rocket, "`=>` in a map literal")?;
1235                    self.skip_newlines();
1236                    items.push(k);
1237                    items.push(self.expr(0)?);
1238                }
1239            }
1240            self.skip_newlines();
1241            if self.eat(&TokenKind::Comma) {
1242                continue;
1243            }
1244            self.expect(&TokenKind::RBrace, "`,` or `}`")?;
1245            break;
1246        }
1247        Ok(list_at(self.span_since(open.start), items))
1248    }
1249
1250    /// `if c ... [else ...] end`, and `unless` as its negation.
1251    ///
1252    /// `unless` lowers to `(if (not c) ...)` rather than to a distinct
1253    /// form: one tree per meaning, so the formatter and every downstream
1254    /// tool see exactly one shape.
1255    /// An `elsif` arm: an `if` that does NOT consume the closing `end`.
1256    ///
1257    /// `if / elsif / elsif / else / end` is one `end` for the whole chain, so
1258    /// every arm but the first must leave it for its parent.
1259    fn if_chain(&mut self, head: Span) -> Result<Spanned, Fail> {
1260        let cond = self.expr(0)?;
1261        let then = self.body(&["elsif", "else", "end"])?;
1262        let els = if self.at_ident("elsif") {
1263            let elsif = self.peek_span();
1264            self.bump();
1265            Some(self.if_chain(elsif)?)
1266        } else if self.at_ident("else") {
1267            self.bump();
1268            let e = self.body(&["end"])?;
1269            self.expect_ident("end")?;
1270            Some(e)
1271        } else {
1272            self.expect_ident("end")?;
1273            None
1274        };
1275        let mut out = vec![sym_at(head, "if"), cond, then];
1276        if let Some(e) = els {
1277            out.push(e);
1278        }
1279        Ok(list_at(self.span_since(head.start), out))
1280    }
1281
1282    fn if_form(&mut self, head: Span, negate: bool) -> Result<Spanned, Fail> {
1283        let cond = self.expr(0)?;
1284        let cond = if negate {
1285            // `unless c` lowers to `(if (not c) …)`. The synthesized `not` takes
1286            // the `unless` keyword's span — that word is what put it there.
1287            let inner = cond.span;
1288            list_at(head.merge(inner), vec![sym_at(head, "not"), cond])
1289        } else {
1290            cond
1291        };
1292        // `elsif` terminates the then-body too, or the chain is swallowed.
1293        //
1294        // It used to be absent entirely — not a keyword, not reserved, nowhere
1295        // in this file — so `elsif cond` parsed as a stray expression inside
1296        // the then-body and the branch it guarded VANISHED. No error: the
1297        // program ran and returned the `else` value. Measured:
1298        //
1299        //   if a < 1 then 1 elsif a < 5 then 2 else 3 end   with a = 3
1300        //     wanted 2, returned 3
1301        //
1302        // A silent wrong answer in the most-used control form in the language,
1303        // reachable by writing the Ruby every blue user already knows.
1304        let then = self.body(&["elsif", "else", "end"])?;
1305        let els = if self.at_ident("elsif") {
1306            let elsif = self.peek_span();
1307            self.bump();
1308            // The chain shares ONE `end`, so recurse without consuming it and
1309            // let the outermost `if` close the whole thing.
1310            Some(self.if_chain(elsif)?)
1311        } else if self.at_ident("else") {
1312            self.bump();
1313            let e = self.body(&["end"])?;
1314            self.expect_ident("end")?;
1315            Some(e)
1316        } else {
1317            self.expect_ident("end")?;
1318            None
1319        };
1320        let mut out = vec![sym_at(head, "if"), cond, then];
1321        if let Some(e) = els {
1322            out.push(e);
1323        }
1324        Ok(list_at(self.span_since(head.start), out))
1325    }
1326
1327    /// `def name(a, b) ... end`            => `(define (name a b) body)`
1328    /// `def name(a: T, b: T) -> R ... end`  => `(define-typed (name (a T) (b T)) R body)`
1329    ///
1330    /// **The two shapes are deliberately different heads.** §0 says an
1331    /// unannotated program gets ZERO analysis, and the cleanest way to
1332    /// mean that is for untyped code not to reach the typing machinery at
1333    /// all — not to reach it and be waved through. A checker that must
1334    /// walk every node to discover there is nothing to check has already
1335    /// paid the cost the ladder exists to avoid.
1336    ///
1337    /// Annotations are per-parameter, so a signature may be partially
1338    /// annotated. That is the ladder at its finest grain: `a: Int` is
1339    /// checked and a bare `b` stays `dyn`, in the same signature.
1340    /// `case subject / when a / … / else / … / end` => a `cond` over equality.
1341    ///
1342    /// **Value matching, not destructuring.** Elixir's `case` binds pattern
1343    /// variables; blue's compares with the same `equal?` the `==` operator uses,
1344    /// so `when [1, 2]` matches a list by value. Destructuring needs a pattern
1345    /// language and a binder, which blue does not have — and a `case` that
1346    /// *looked* like Elixir's while silently only comparing would be worse than
1347    /// one that plainly compares.
1348    ///
1349    /// The subject is evaluated ONCE, into a binding, so `case expensive()` does
1350    /// not re-run per arm. That is a correctness property, not an optimisation:
1351    /// a subject with a side effect would fire once per `when`.
1352    fn case_form(&mut self, head: Span) -> Result<Spanned, Fail> {
1353        let subject = self.expr(0)?;
1354        self.skip_newlines();
1355
1356        // A fresh name the surface cannot spell, so it cannot capture a user
1357        // binding of the same name.
1358        let subject_var = "case-subject";
1359        let mut arms: Vec<Spanned> = Vec::new();
1360        let mut otherwise: Option<Spanned> = None;
1361        // The `else` keyword's own span, so the synthesized `(else body)` clause
1362        // sits where the author wrote it rather than back at `case`.
1363        let mut else_kw: Option<Span> = None;
1364
1365        loop {
1366            self.skip_newlines();
1367            if self.at_ident("end") {
1368                break;
1369            }
1370            let maybe_else = self.peek_span();
1371            if self.eat_ident("else") {
1372                else_kw = Some(maybe_else);
1373                otherwise = Some(self.body(&["end"])?);
1374                continue;
1375            }
1376            let when = self.peek_span();
1377            if !self.eat_ident("when") {
1378                return Err(self.error(format!(
1379                    "expected `when`, `else` or `end` in a case, found {}",
1380                    self.peek()
1381                )));
1382            }
1383            self.skip_newlines();
1384            let pattern = self.expr(0)?;
1385            let body = self.body(&["when", "else", "end"])?;
1386            // The synthesized comparison takes the arm's `when` span: that word
1387            // is what put an `equal?` there, and it is where a reader looks when
1388            // told an arm cannot match.
1389            let test = list_at(
1390                when.merge(pattern.span),
1391                vec![sym_at(when, "equal?"), sym_at(when, subject_var), pattern],
1392            );
1393            arms.push(list_at(when.merge(body.span), vec![test, body]));
1394        }
1395        self.expect_ident("end")?;
1396
1397        if arms.is_empty() && otherwise.is_none() {
1398            return Err(self.error("a case needs at least one `when` or an `else`".to_string()));
1399        }
1400
1401        // A case with no matching arm and no else is NIL, matching Ruby. Elixir
1402        // raises CaseClauseError; blue follows Ruby because its `if` without an
1403        // else is already nil, and having two different answers to "no branch
1404        // taken" in one language is the inconsistency.
1405        let mut cond = vec![sym_at(head, "cond")];
1406        cond.extend(arms);
1407        // The `else` clause spans its own keyword through its body. Anchoring it
1408        // at `head` — the `case` keyword — put a node whose subtree sits at the
1409        // END of the form at a span near its START, so the else body escaped its
1410        // parent's range entirely. `every_node_sits_inside_its_parents_span`
1411        // caught that; it is the kind of mistake no output-shape test can see,
1412        // because the tree is correct and only the positions are wrong.
1413        let else_span = else_kw.unwrap_or(head);
1414        let else_body = otherwise.unwrap_or_else(|| Spanned::new(else_span, SpannedForm::Nil));
1415        cond.push(list_at(
1416            else_span.merge(else_body.span),
1417            vec![sym_at(else_span, "else"), else_body],
1418        ));
1419
1420        let whole = self.span_since(head.start);
1421        let binding = list_at(
1422            head.merge(subject.span),
1423            vec![sym_at(head, subject_var), subject],
1424        );
1425        Ok(list_at(
1426            whole,
1427            vec![
1428                sym_at(head, "let"),
1429                list_at(binding.span, vec![binding]),
1430                list_at(whole, cond),
1431            ],
1432        ))
1433    }
1434
1435    /// `fn(a, b) ... end` => `(lambda (a b) body)`
1436    ///
1437    /// Without this the higher-order functions are unreachable in practice:
1438    /// `map(inc, xs)` works only because `inc` happens to be a named stdlib
1439    /// function, and there was no way to write the one-off the call site
1440    /// actually wants.
1441    ///
1442    /// `fn` rather than Ruby's `->` or `lambda`: `->` collides with the return-
1443    /// type arrow the typed `def` already uses, and reusing one glyph for two
1444    /// unrelated things is the ambiguity the FORM axis exists to prevent.
1445    fn lambda_form(&mut self, head: Span) -> Result<Spanned, Fail> {
1446        let mut params: Vec<Spanned> = Vec::new();
1447        if self.at(&TokenKind::LParen) {
1448            self.bump();
1449            self.skip_newlines();
1450            if !self.eat(&TokenKind::RParen) {
1451                loop {
1452                    self.skip_newlines();
1453                    let p_span = self.peek_span();
1454                    match self.bump() {
1455                        TokenKind::Ident(p) => params.push(sym_at(p_span, &p)),
1456                        other => {
1457                            return Err(fail(
1458                                format!("expected a parameter name, found {other}"),
1459                                p_span,
1460                            ))
1461                        }
1462                    }
1463                    self.skip_newlines();
1464                    if self.eat(&TokenKind::Comma) {
1465                        continue;
1466                    }
1467                    self.expect(&TokenKind::RParen, "`,` or `)`")?;
1468                    break;
1469                }
1470            }
1471        }
1472        let params_span = cover(&params, head);
1473        let body = self.body(&["end"])?;
1474        self.expect_ident("end")?;
1475        Ok(list_at(
1476            self.span_since(head.start),
1477            vec![sym_at(head, "lambda"), list_at(params_span, params), body],
1478        ))
1479    }
1480
1481    /// `test "name" ... end` => `(deftest "name" body)`
1482    ///
1483    /// A string, not an identifier: a test name is prose for a human report,
1484    /// and forcing it into an identifier is how test names become
1485    /// `test_adds_two_numbers_correctly`.
1486    fn test_form(&mut self, head: Span) -> Result<Spanned, Fail> {
1487        let name_span = self.peek_span();
1488        let name = match self.bump() {
1489            TokenKind::Str(s) => s,
1490            other => {
1491                return Err(fail(
1492                    format!("expected a string name after `test`, found {other}"),
1493                    name_span,
1494                ))
1495            }
1496        };
1497        let body = self.body(&["end"])?;
1498        self.expect_ident("end")?;
1499        Ok(list_at(
1500            self.span_since(head.start),
1501            vec![
1502                sym_at(head, "deftest"),
1503                atom_at(name_span, Atom::Str(name)),
1504                body,
1505            ],
1506        ))
1507    }
1508
1509    /// `assert expr` => `(blue-assert 'expr expr)`
1510    ///
1511    /// **`blue-assert`, not `assert`.** tatara-lisp's stdlib already defines
1512    /// `assert` as a macro — `(defmacro assert (pred message) …)` — and a macro
1513    /// in the expander is consulted before any primitive in the registry. So
1514    /// lowering to `assert` bound `pred` to the *quoted form*, which is
1515    /// truthy, and **every assertion silently passed**. A test framework whose
1516    /// assertions always pass is the worst defect it can have: every test in
1517    /// the suite goes green.
1518    ///
1519    /// The lesson generalizes: any name blue lowers to that tatara already
1520    /// binds is silently captured. `blue_lang_test`'s
1521    /// `no_lowered_name_is_shadowed_by_the_runtime` gates the whole class.
1522    ///
1523    /// **Both the form and the value.** A test framework whose failure says
1524    /// only "assertion failed" makes the author re-derive what they were
1525    /// checking; one that shows the expression does not. The quoted form is
1526    /// the expression as DATA, so the runner can render it — and it renders it
1527    /// through `blue-lang-fmt`, meaning the failure message is in canonical
1528    /// blue syntax rather than the underlying tatara-lisp.
1529    ///
1530    /// This is homoiconicity paying for itself: the capture needs no source
1531    /// map, no macro hygiene, and no string of the original text.
1532    fn assert_form(&mut self, head: Span) -> Result<Spanned, Fail> {
1533        let e = self.expr(0)?;
1534        let quoted = Spanned::new(e.span, SpannedForm::Quote(Box::new(e.clone())));
1535        Ok(list_at(
1536            self.span_since(head.start),
1537            vec![sym_at(head, LOWERED_ASSERT), quoted, e],
1538        ))
1539    }
1540
1541    /// `defmacro name(a, b) ... end` => `(defmacro name (a b) body)`
1542    ///
1543    /// **Deliberately untyped.** A macro's parameters are *source forms*, not
1544    /// values, so `a: Int` would be a category error: the argument at expansion
1545    /// time is a fragment of syntax. §IV's ladder types values; macro
1546    /// parameters are not on it. Annotating one is rejected rather than
1547    /// silently ignored — an ignored annotation is how an author comes to
1548    /// believe a check is running.
1549    fn defmacro_form(&mut self, head: Span) -> Result<Spanned, Fail> {
1550        let name_span = self.peek_span();
1551        let name = match self.bump() {
1552            TokenKind::Ident(n) => n,
1553            other => {
1554                return Err(fail(
1555                    format!("expected a name after `defmacro`, found {other}"),
1556                    name_span,
1557                ))
1558            }
1559        };
1560        let mut params: Vec<Spanned> = Vec::new();
1561        if self.at(&TokenKind::LParen) {
1562            self.bump();
1563            self.skip_newlines();
1564            if !self.eat(&TokenKind::RParen) {
1565                loop {
1566                    self.skip_newlines();
1567                    let p_span = self.peek_span();
1568                    match self.bump() {
1569                        TokenKind::Ident(p) => params.push(sym_at(p_span, &p)),
1570                        TokenKind::Label(p) => {
1571                            return Err(self.error(format!(
1572                                "macro parameter `{p}` cannot be typed: a macro receives \
1573                                 source forms, not values"
1574                            )))
1575                        }
1576                        other => {
1577                            return Err(fail(
1578                                format!("expected a parameter name, found {other}"),
1579                                p_span,
1580                            ))
1581                        }
1582                    }
1583                    self.skip_newlines();
1584                    if self.eat(&TokenKind::Comma) {
1585                        continue;
1586                    }
1587                    self.expect(&TokenKind::RParen, "`,` or `)`")?;
1588                    break;
1589                }
1590            }
1591        }
1592        if matches!(self.peek(), TokenKind::Op(o) if o == "->") {
1593            return Err(self.error(
1594                "a macro has no return type: it produces source forms, not values".to_string(),
1595            ));
1596        }
1597
1598        let params_span = cover(&params, name_span);
1599        let body = self.body(&["end"])?;
1600        self.expect_ident("end")?;
1601
1602        // `(defmacro name (params) body)` — tatara-lisp's own shape, so blue
1603        // registers into the SAME expander rather than a parallel one.
1604        Ok(list_at(
1605            self.span_since(head.start),
1606            vec![
1607                sym_at(head, "defmacro"),
1608                sym_at(name_span, &name),
1609                list_at(params_span, params),
1610                body,
1611            ],
1612        ))
1613    }
1614
1615    /// `quote ... end` => `` `body `` (a quasiquote).
1616    ///
1617    /// Quasiquote rather than plain quote, because a macro body that could not
1618    /// splice its arguments in would be useless — this is Elixir's `quote do`,
1619    /// which is likewise a template and not inert data.
1620    fn quote_form(&mut self, head: Span) -> Result<Spanned, Fail> {
1621        let body = self.body(&["end"])?;
1622        self.expect_ident("end")?;
1623        // `Sexp::Quasiquote`, NOT `(quasiquote body)` as a list.
1624        //
1625        // The list form Displays as the text `(quasiquote …)`, which the
1626        // tatara-lisp reader reads back as an ordinary list whose head happens
1627        // to be the symbol `quasiquote` — losing the structure. The evaluator
1628        // then reached the inner `,x` with no enclosing quasiquote and rejected
1629        // it: "unquote outside of quasiquote". Building the real variant makes
1630        // it Display as `` ` `` and survive the round trip.
1631        Ok(Spanned::new(
1632            self.span_since(head.start),
1633            SpannedForm::Quasiquote(Box::new(body)),
1634        ))
1635    }
1636
1637    /// `unquote(expr)` => `,expr`; `unquote_splice(expr)` => `,@expr`.
1638    fn unquote_form(&mut self, head: Span, splice: bool) -> Result<Spanned, Fail> {
1639        self.expect(&TokenKind::LParen, "`(` after unquote")?;
1640        self.skip_newlines();
1641        let inner = self.expr(0)?;
1642        self.skip_newlines();
1643        self.expect(&TokenKind::RParen, "`)`")?;
1644        let span = self.span_since(head.start);
1645        Ok(Spanned::new(
1646            span,
1647            if splice {
1648                SpannedForm::UnquoteSplice(Box::new(inner))
1649            } else {
1650                SpannedForm::Unquote(Box::new(inner))
1651            },
1652        ))
1653    }
1654
1655    fn def_form(&mut self, head: Span) -> Result<Spanned, Fail> {
1656        let name_span = self.peek_span();
1657        let name = match self.bump() {
1658            TokenKind::Ident(n) => n,
1659            other => {
1660                return Err(fail(
1661                    format!("expected a name after `def`, found {other}"),
1662                    name_span,
1663                ))
1664            }
1665        };
1666        let mut params: Vec<(String, Span, Option<Spanned>)> = Vec::new();
1667        if self.at(&TokenKind::LParen) {
1668            self.bump();
1669            self.skip_newlines();
1670            if !self.eat(&TokenKind::RParen) {
1671                loop {
1672                    self.skip_newlines();
1673                    let p_span = self.peek_span();
1674                    match self.bump() {
1675                        // `a` — unannotated
1676                        TokenKind::Ident(p) => params.push((p, p_span, None)),
1677                        // `a:` came through as one token, so a type follows
1678                        TokenKind::Label(p) => {
1679                            self.skip_newlines();
1680                            let ty = self.type_expr()?;
1681                            params.push((p, p_span, Some(ty)));
1682                        }
1683                        other => {
1684                            return Err(fail(
1685                                format!("expected a parameter name, found {other}"),
1686                                p_span,
1687                            ))
1688                        }
1689                    }
1690                    self.skip_newlines();
1691                    if self.eat(&TokenKind::Comma) {
1692                        continue;
1693                    }
1694                    self.expect(&TokenKind::RParen, "`,` or `)`")?;
1695                    break;
1696                }
1697            }
1698        }
1699        // The signature list spans the name through the closing `)`.
1700        let sig_span = self.span_since(name_span.start);
1701
1702        // Optional `-> R`
1703        let ret = if matches!(self.peek(), TokenKind::Op(o) if o == "->") {
1704            self.bump();
1705            self.skip_newlines();
1706            Some(self.type_expr()?)
1707        } else {
1708            None
1709        };
1710
1711        let body = self.body(&["end"])?;
1712        self.expect_ident("end")?;
1713        let whole = self.span_since(head.start);
1714
1715        let annotated = ret.is_some() || params.iter().any(|(_, _, t)| t.is_some());
1716        if !annotated {
1717            let mut sig = vec![sym_at(name_span, &name)];
1718            sig.extend(params.into_iter().map(|(p, s, _)| sym_at(s, &p)));
1719            return Ok(list_at(
1720                whole,
1721                vec![sym_at(head, "define"), list_at(sig_span, sig), body],
1722            ));
1723        }
1724
1725        // Typed shape. An un-annotated parameter in an otherwise annotated
1726        // signature is written `(p dyn)` so the checker sees the ladder
1727        // position explicitly rather than inferring it from absence.
1728        //
1729        // A `dyn` written by the parser rather than the author takes the
1730        // parameter's own span, so a diagnostic about it points at the
1731        // parameter — the only thing in the source it could mean.
1732        let mut sig = vec![sym_at(name_span, &name)];
1733        for (p, s, t) in params {
1734            let ty = t.unwrap_or_else(|| sym_at(s, "dyn"));
1735            sig.push(list_at(s.merge(ty.span), vec![sym_at(s, &p), ty]));
1736        }
1737        Ok(list_at(
1738            whole,
1739            vec![
1740                sym_at(head, "define-typed"),
1741                list_at(sig_span, sig),
1742                ret.unwrap_or_else(|| sym_at(head, "dyn")),
1743                body,
1744            ],
1745        ))
1746    }
1747
1748    /// A type expression. Currently a bare name (`Int`, `Str`, `dyn`) or a
1749    /// one-argument constructor (`List(Int)`).
1750    fn type_expr(&mut self) -> Result<Spanned, Fail> {
1751        let name_span = self.peek_span();
1752        let name = match self.bump() {
1753            TokenKind::Ident(n) => n,
1754            other => {
1755                return Err(fail(
1756                    format!("expected a type name, found {other}"),
1757                    name_span,
1758                ))
1759            }
1760        };
1761        if self.at(&TokenKind::LParen) {
1762            let args = self.paren_args()?;
1763            let mut list = vec![sym_at(name_span, &name)];
1764            list.extend(args);
1765            return Ok(list_at(self.span_since(name_span.start), list));
1766        }
1767        Ok(sym_at(name_span, &name))
1768    }
1769
1770    /// Consume `name` if it is the next token, else leave the position alone.
1771    fn eat_ident(&mut self, name: &str) -> bool {
1772        if self.at_ident(name) {
1773            self.bump();
1774            true
1775        } else {
1776            false
1777        }
1778    }
1779
1780    fn expect_ident(&mut self, name: &str) -> Result<(), Fail> {
1781        if self.at_ident(name) {
1782            self.bump();
1783            Ok(())
1784        } else {
1785            Err(self.error(format!("expected `{name}`, found {}", self.peek())))
1786        }
1787    }
1788
1789    /// A sequence of expressions up to one of `terminators`, wrapped in
1790    /// `(begin ...)` when there is more than one.
1791    fn body(&mut self, terminators: &[&str]) -> Result<Spanned, Fail> {
1792        // Anchored BEFORE the leading newlines are skipped, so an empty body's
1793        // span sits where the body would have started.
1794        let start = self.mark();
1795        let mut forms = Vec::new();
1796        loop {
1797            self.skip_newlines();
1798            if matches!(self.peek(), TokenKind::Eof) {
1799                return Err(self.error(format!(
1800                    "unterminated block: expected {} before the end of the file",
1801                    one_of(terminators)
1802                )));
1803            }
1804            if terminators.iter().any(|t| self.at_ident(t)) {
1805                break;
1806            }
1807            forms.push(self.statement()?);
1808            self.end_of_statement(terminators)?;
1809        }
1810        Ok(match forms.len() {
1811            0 => Spanned::new(Span::new(start, start), SpannedForm::Nil),
1812            1 => forms.into_iter().next().expect("checked len"),
1813            _ => {
1814                // `cover`, NOT `span_since`: the loop breaks *before* consuming
1815                // the terminator but *after* `skip_newlines`, so the last token
1816                // behind `pos` can be a newline and `span_since` would stretch
1817                // the body across trailing blank lines. The forms know exactly
1818                // where they are.
1819                let span = cover(&forms, Span::new(start, start));
1820                // The synthesized `begin` is zero-width at the FIRST FORM's
1821                // start, not at `start`. `start` is anchored before the leading
1822                // newlines are skipped, so it sits earlier than the body itself
1823                // — and a head node outside its own list's span is a span bug
1824                // that `every_node_sits_inside_its_parents_span` reports.
1825                let head = Span::new(span.start, span.start);
1826                let mut list = vec![sym_at(head, "begin")];
1827                list.extend(forms);
1828                list_at(span, list)
1829            }
1830        })
1831    }
1832}
1833
1834/// `` `a` ``, `` `a` or `b` ``, `` `a`, `b` or `c` `` — a list of keywords as
1835/// a sentence says it.
1836fn one_of(words: &[&str]) -> String {
1837    let quoted: Vec<String> = words.iter().map(|w| format!("`{w}`")).collect();
1838    match quoted.split_last() {
1839        None => String::new(),
1840        Some((last, [])) => last.clone(),
1841        Some((last, rest)) => format!("{} or {last}", rest.join(", ")),
1842    }
1843}
1844
1845/// A symbol node at `span`.
1846fn sym_at(span: Span, s: &str) -> Spanned {
1847    Spanned::new(span, SpannedForm::Atom(Atom::Symbol(s.to_string())))
1848}
1849
1850/// An atom node at `span`.
1851fn atom_at(span: Span, a: Atom) -> Spanned {
1852    Spanned::new(span, SpannedForm::Atom(a))
1853}
1854
1855/// A list node at `span`.
1856fn list_at(span: Span, items: Vec<Spanned>) -> Spanned {
1857    Spanned::new(span, SpannedForm::List(items))
1858}
1859
1860/// The smallest span covering every node in `items`, or `fallback` when there
1861/// are none.
1862///
1863/// The fallback is a parameter rather than `Span::synthetic()` because a
1864/// synthetic span is a claim that a node has no source origin at all — and an
1865/// empty parameter list in `def f()` very much has one. Defaulting to synthetic
1866/// here is how "no span" leaks into a tree where every node does have a place.
1867fn cover(items: &[Spanned], fallback: Span) -> Span {
1868    let merged = items
1869        .iter()
1870        .fold(Span::synthetic(), |acc, s| acc.merge(s.span));
1871    if merged.is_synthetic() {
1872        fallback
1873    } else {
1874        merged
1875    }
1876}
1877
1878#[cfg(test)]
1879mod tests {
1880    use super::*;
1881
1882    /// Render an `Sexp` to canonical text so tests can state the expected
1883    /// quoted form as a string. This is `Display`, which tatara-lisp owns —
1884    /// blue does not build Lisp syntax by concatenation.
1885    fn q(src: &str) -> String {
1886        parse_expr(src)
1887            .map(|s| s.to_string())
1888            .unwrap_or_else(|e| panic!("{src:?}: {e}"))
1889    }
1890
1891    // ---- the thesis: Ruby-shaped source becomes tatara-lisp ----------
1892
1893    #[test]
1894    fn arithmetic_respects_precedence() {
1895        assert_eq!(q("1 + 2 * 3"), "(+ 1 (* 2 3))");
1896        assert_eq!(q("(1 + 2) * 3"), "(* (+ 1 2) 3)");
1897    }
1898
1899    #[test]
1900    fn comparison_binds_looser_than_arithmetic() {
1901        assert_eq!(q("a + 1 < b"), "(< (+ a 1) b)");
1902    }
1903
1904    /// The expected tree names `or`/`and`, not `||`/`&&`: the surface
1905    /// spelling is the SURFACE's, and lowering renames it to the form
1906    /// tatara-lisp actually has. This test previously asserted the verbatim
1907    /// spelling, which is how `(== a b)` — a symbol nothing binds — shipped.
1908    #[test]
1909    fn logical_operators_bind_loosest_and_lower_to_tataras_names() {
1910        assert_eq!(q("a && b || c"), "(or (and a b) c)");
1911    }
1912
1913    #[test]
1914    fn left_associativity() {
1915        assert_eq!(q("1 - 2 - 3"), "(- (- 1 2) 3)");
1916    }
1917
1918    /// A bare `recv.name` is a SEND. Blue commits to uniform access here,
1919    /// so a field can later become a computed method without breaking
1920    /// callers.
1921    #[test]
1922    fn method_call_without_parens_is_a_send() {
1923        assert_eq!(q("user.name"), "(name user)");
1924    }
1925
1926    #[test]
1927    fn method_call_with_args() {
1928        assert_eq!(q("user.greet(1, 2)"), "(greet user 1 2)");
1929    }
1930
1931    #[test]
1932    fn chained_sends_read_left_to_right() {
1933        assert_eq!(q("a.b.c"), "(c (b a))");
1934    }
1935
1936    #[test]
1937    fn plain_call() {
1938        assert_eq!(q("f(1, 2)"), "(f 1 2)");
1939    }
1940
1941    /// The pipeline threads into the FIRST argument, as Elixir's does —
1942    /// that is what makes `|>` composable rather than decorative.
1943    #[test]
1944    fn pipeline_threads_into_first_argument() {
1945        assert_eq!(q("x |> f"), "(f x)");
1946        assert_eq!(q("x |> f(1)"), "(f x 1)");
1947        assert_eq!(q("x |> f |> g"), "(g (f x))");
1948    }
1949
1950    #[test]
1951    fn pipeline_binds_looser_than_arithmetic() {
1952        assert_eq!(q("1 + 2 |> f"), "(f (+ 1 2))");
1953    }
1954
1955    // ---- §V.13's rendering law, enforced at the parser ---------------
1956
1957    /// `a: 1` and `:a => 1` are the SAME TREE. That is exactly why the
1958    /// formatter may always render the shorthand: they are not two
1959    /// spellings of two things, they are two spellings of one thing.
1960    #[test]
1961    fn label_and_rocket_produce_the_same_tree_for_a_symbol_key() {
1962        assert_eq!(q("{a: 1}"), q("{:a => 1}"));
1963        assert_eq!(q("{a: 1}"), "(hash-map :a 1)");
1964    }
1965
1966    /// And where the key is NOT a plain symbol, the rocket is the only
1967    /// spelling — so it survives because it must, never as a style choice.
1968    #[test]
1969    fn a_string_key_has_no_shorthand() {
1970        assert_eq!(q(r#"{"k" => 1}"#), r#"(hash-map "k" 1)"#);
1971    }
1972
1973    #[test]
1974    fn list_literal() {
1975        assert_eq!(q("[1, 2, 3]"), "(list 1 2 3)");
1976        assert_eq!(q("[]"), "(list)");
1977    }
1978
1979    // ---- blocks ------------------------------------------------------
1980
1981    #[test]
1982    fn if_else_end() {
1983        assert_eq!(q("if a\n  1\nelse\n  2\nend"), "(if a 1 2)");
1984    }
1985
1986    #[test]
1987    fn if_without_else() {
1988        assert_eq!(q("if a\n  1\nend"), "(if a 1)");
1989    }
1990
1991    /// `unless` lowers to `(if (not c) …)` — one tree per meaning, so
1992    /// every downstream tool sees exactly one shape.
1993    #[test]
1994    fn unless_is_a_negated_if() {
1995        assert_eq!(q("unless a\n  1\nend"), "(if (not a) 1)");
1996    }
1997
1998    #[test]
1999    fn multi_statement_body_becomes_begin() {
2000        assert_eq!(q("if a\n  1\n  2\nend"), "(if a (begin 1 2))");
2001    }
2002
2003    #[test]
2004    fn def_lowers_to_define() {
2005        assert_eq!(
2006            q("def add(a, b)\n  a + b\nend"),
2007            "(define (add a b) (+ a b))"
2008        );
2009    }
2010
2011    #[test]
2012    fn def_with_no_params() {
2013        assert_eq!(q("def zero()\n  0\nend"), "(define (zero) 0)");
2014    }
2015
2016    // ---- literals ----------------------------------------------------
2017
2018    #[test]
2019    fn literals_lower_to_atoms() {
2020        assert_eq!(q("42"), "42");
2021        assert_eq!(q("true"), "#t");
2022        assert_eq!(q(":ok"), ":ok");
2023        assert_eq!(q(r#""hi""#), r#""hi""#);
2024    }
2025
2026    #[test]
2027    fn unary_minus_and_not() {
2028        assert_eq!(q("-x"), "(- 0 x)");
2029        assert_eq!(q("!x"), "(not x)");
2030    }
2031
2032    // ---- programs and errors -----------------------------------------
2033
2034    #[test]
2035    fn a_program_is_a_sequence_of_forms() {
2036        let forms = parse_program("def f()\n  1\nend\nf()").expect("parse");
2037        assert_eq!(forms.len(), 2);
2038        assert_eq!(forms[1].to_string(), "(f)");
2039    }
2040
2041    #[test]
2042    fn unterminated_block_is_an_error_naming_what_was_expected() {
2043        let e = parse_program("if a\n  1").expect_err("must fail");
2044        assert!(e.message.contains("unterminated"), "{}", e.message);
2045    }
2046
2047    #[test]
2048    fn a_parse_error_carries_a_span_into_the_source() {
2049        let src = "1 + )";
2050        let e = parse_program(src).expect_err("must fail");
2051        assert!(e.span.start < src.len(), "span {:?} outside source", e.span);
2052    }
2053
2054    // ---- one statement per line -------------------------------------
2055
2056    /// `y = xs[0]` was TWO statements, `(define y xs)` and `(list 0)`: nothing
2057    /// required a separator between statements, so the parser finished `xs`,
2058    /// saw a list literal and started a new statement on the same line. The
2059    /// program ran and `y` was the whole list. Found by the 2026-09-29 audit.
2060    #[test]
2061    fn postfix_indexing_is_refused_naming_nth() {
2062        let e = parse_program("y = xs[0]").expect_err("must not split into two statements");
2063        assert!(e.message.contains("`nth(0, xs)`"), "{}", e.message);
2064        assert_eq!((e.line, e.col), (1, 7), "{e}");
2065    }
2066
2067    /// `a = f x` — Ruby's command call — parsed as `a = f` and then `x`.
2068    #[test]
2069    fn a_call_without_parentheses_is_refused_naming_the_call() {
2070        let e = parse_program("a = f x").expect_err("must not split into two statements");
2071        assert!(e.message.contains("`f(x)`"), "{}", e.message);
2072        assert_eq!((e.line, e.col), (1, 7), "{e}");
2073    }
2074
2075    /// The same rule inside a block body, and in every body shape.
2076    #[test]
2077    fn two_statements_on_one_line_are_refused_in_a_body() {
2078        for src in [
2079            "def f(xs)\n  xs[0]\nend",
2080            "if a\n  f x\nend",
2081            "fn(x) x 1 end",
2082            "test \"t\"\n  assert a b\nend",
2083            "1 2",
2084        ] {
2085            assert!(parse_program(src).is_err(), "{src:?} must not parse");
2086        }
2087    }
2088
2089    /// The controls: every place a statement legitimately ends on its line.
2090    /// A block keyword closing the body on the same line is one of them, which
2091    /// is what keeps a one-line lambda one line.
2092    #[test]
2093    fn a_statement_may_end_at_a_newline_the_end_or_a_closing_keyword() {
2094        for src in [
2095            "fn(x) x + 1 end",
2096            "map(fn(x) x end, xs)",
2097            "if a\n  1\nelse\n  2\nend",
2098            "if a 1 else 2 end",
2099            "case x\nwhen 1 :a\nwhen 2 :b\nelse :c\nend",
2100            "x = 1\ny = 2",
2101            "x = 1",
2102        ] {
2103            assert!(
2104                parse_program(src).is_ok(),
2105                "{src:?}: {:?}",
2106                parse_program(src)
2107            );
2108        }
2109    }
2110
2111    /// `if c then a else b end` read `then` as a statement and `a` as another.
2112    #[test]
2113    fn then_is_refused_by_name() {
2114        let e = parse_program("x = if c then 1 else 2 end").expect_err("then is not a keyword");
2115        assert!(e.message.contains("`then`"), "{}", e.message);
2116    }
2117
2118    /// A brace block after a send is not a map literal the author meant.
2119    #[test]
2120    fn a_brace_block_is_refused_naming_fn() {
2121        let e = parse_program("xs.map { |x| x }").expect_err("no brace blocks");
2122        assert!(e.message.contains("fn(x)"), "{}", e.message);
2123    }
2124
2125    // ---- qualified names ------------------------------------------------
2126
2127    /// `retsu::first(xs)` is one symbol, tatara's `require` shape.
2128    ///
2129    /// Red run (2026-09-29), before the `::` token: `expected an expression,
2130    /// found `:``.
2131    #[test]
2132    fn a_qualified_name_is_one_symbol() {
2133        assert_eq!(q("retsu::first(xs)"), "(retsu/first xs)");
2134        assert_eq!(q("retsu::first"), "retsu/first");
2135        assert_eq!(q("xs |> retsu::first"), "(retsu/first xs)");
2136        assert_eq!(q("blue::length(xs)"), "(blue/length xs)");
2137    }
2138
2139    /// Whitespace around `::` is layout, not meaning.
2140    #[test]
2141    fn whitespace_around_the_qualifier_is_accepted() {
2142        assert_eq!(q("retsu :: first(xs)"), "(retsu/first xs)");
2143    }
2144
2145    /// After `::` a reserved word is a name: `kueri::if` is kueri's
2146    /// definition, not the `if` form.
2147    #[test]
2148    fn a_reserved_word_after_the_qualifier_is_a_name() {
2149        assert_eq!(q("kueri::if(a, b)"), "(kueri/if a b)");
2150        assert_eq!(q("heni::test(x)"), "(heni/test x)");
2151        assert_eq!(q("sabi::fn"), "sabi/fn");
2152    }
2153
2154    #[test]
2155    fn a_qualified_name_has_exactly_one_qualifier() {
2156        for (src, says) in [
2157            ("::first(xs)", "`package::name`"),
2158            ("a::b::c", "one qualifier"),
2159            ("if::x", "reserved word"),
2160            ("retsu::1", "expected a name after `retsu::`"),
2161        ] {
2162            let e = parse_program(src).expect_err(src);
2163            assert!(e.message.contains(says), "{src}: {}", e.message);
2164        }
2165    }
2166
2167    /// The one reader of the shape: division is not qualified, and neither
2168    /// is anything the surface could not have written.
2169    #[test]
2170    fn qualified_reads_only_the_package_name_shape() {
2171        assert_eq!(qualified("retsu/first"), Some(("retsu", "first")));
2172        assert_eq!(qualified("kueri/if"), Some(("kueri", "if")));
2173        assert_eq!(qualified("nisshi/pair?"), Some(("nisshi", "pair?")));
2174        for not in ["/", "a/", "/a", "a/b/c", "if/x", "hash-map", "a"] {
2175            assert_eq!(qualified(not), None, "{not}");
2176        }
2177        assert!(is_callable_name("kueri/if"));
2178    }
2179
2180    /// `:pair?` is the symbol `pair?`: an import list can name a predicate.
2181    ///
2182    /// Red run (2026-09-29), before: `unexpected character `?``, found by
2183    /// `blue migrate` listing nisshi's `pair?`.
2184    #[test]
2185    fn a_symbol_may_end_in_a_question_or_bang() {
2186        assert_eq!(q("[:pair?, :set!]"), "(list :pair? :set!)");
2187        assert_eq!(q("use(\"n\", [:empty?])"), "(use \"n\" (list :empty?))");
2188        assert_eq!(q(":a != :b"), "(not= :a :b)");
2189    }
2190
2191    // ---- errors a person can read -------------------------------------
2192
2193    /// `line:col` and blue's words, never `at 10..11` and `Ident("y")`.
2194    #[test]
2195    fn a_parse_error_displays_line_col_and_the_source_text() {
2196        let e = parse_program("x = 1\ny = [1, 2 y]").expect_err("must fail");
2197        assert_eq!(e.to_string(), "2:11: expected `,` or `]`, found `y`");
2198        assert_eq!(e.span, Span::new(16, 17), "the span stays, as data");
2199    }
2200
2201    /// Every token kind renders without Rust's `Debug` shape.
2202    #[test]
2203    fn no_error_message_names_a_rust_variant() {
2204        for src in [
2205            "f(1 2)",
2206            "{a: 1 b}",
2207            "def 1\nend",
2208            "x.1",
2209            "if a\n",
2210            "fn(1) 2 end",
2211            "[1 :b]",
2212            "1 +",
2213            "(1",
2214        ] {
2215            let e = parse_program(src).expect_err(src);
2216            let shown = e.to_string();
2217            for tell in [
2218                "Ident(", "Int(", "Op(", "Label(", "Sym(", "Str(", "LParen", "RBracket", "Eof",
2219                "Newline", "..",
2220            ] {
2221                assert!(!shown.contains(tell), "{src:?} -> {shown}");
2222            }
2223            assert!(e.line >= 1 && e.col >= 1, "{src:?} not located: {shown}");
2224        }
2225    }
2226
2227    /// A lex error reaches the author through the same door, located.
2228    #[test]
2229    fn a_lex_error_is_located_too() {
2230        let e = parse_program("x = 1\ny = 2; z = 3").expect_err("no semicolons");
2231        assert_eq!((e.line, e.col), (2, 6), "{e}");
2232        assert!(e.message.contains("new line"), "{e}");
2233    }
2234
2235    /// Anti-vacuity: `q` must be able to FAIL. If every input parsed, the
2236    /// assertions above would be worthless.
2237    #[test]
2238    fn the_parser_rejects_garbage() {
2239        assert!(parse_program("def").is_err());
2240        assert!(parse_program("(1").is_err());
2241        assert!(parse_program("end").is_err());
2242    }
2243}