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