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