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