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