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