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