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