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