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