use tatara_lisp::{Atom, Sexp, Spanned, SpannedForm};
use crate::lex::{lex, Span, Token, TokenKind};
use crate::spelling::{Note, Spelling};
#[derive(Clone, Debug, PartialEq)]
pub struct ParseError {
pub message: String,
pub span: Span,
pub line: usize,
pub col: usize,
}
impl ParseError {
fn new(message: impl Into<String>, span: Span) -> Self {
Self {
message: message.into(),
span,
line: 0,
col: 0,
}
}
fn located(mut self, src: &str) -> Self {
let (line, col) = Span::line_col(src, self.span.start.min(src.len()));
self.line = line;
self.col = col;
self
}
}
impl std::fmt::Display for ParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}:{}: {}", self.line, self.col, self.message)
}
}
impl std::error::Error for ParseError {}
impl From<crate::lex::LexError> for ParseError {
fn from(e: crate::lex::LexError) -> Self {
Self::new(e.message, e.span)
}
}
thread_local! {
static PARSES: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
}
#[must_use]
pub fn parses_on_this_thread() -> u64 {
PARSES.with(std::cell::Cell::get)
}
pub fn parse_program(src: &str) -> Result<Vec<Sexp>, ParseError> {
parse_program_with_depth(src, MAX_EXPR_DEPTH)
}
pub fn parse_program_tree(src: &str) -> Result<Vec<Spanned>, ParseError> {
parse_program_tree_with_depth(src, MAX_EXPR_DEPTH)
}
pub fn parse_program_tree_with_depth(
src: &str,
max_depth: usize,
) -> Result<Vec<Spanned>, ParseError> {
PARSES.with(|n| n.set(n.get() + 1));
let mut p = Parser::over(
src,
lex(src).map_err(|e| ParseError::from(e).located(src))?,
max_depth,
);
p.program().map_err(|e| e.located(src))
}
pub fn spelling_notes(src: &str) -> Result<Vec<Note>, ParseError> {
let mut p = Parser::over(
src,
lex(src).map_err(|e| ParseError::from(e).located(src))?,
MAX_EXPR_DEPTH,
);
p.program().map_err(|e| e.located(src))?;
Ok(p.notes)
}
pub fn parse_program_in(src: &str, pack: &crate::yakugo::Yakugo) -> Result<Vec<Sexp>, ParseError> {
Ok(parse_program_tree_in(src, pack)?
.iter()
.map(Spanned::to_sexp)
.collect())
}
pub fn parse_program_tree_in(
src: &str,
pack: &crate::yakugo::Yakugo,
) -> Result<Vec<Spanned>, ParseError> {
let toks =
crate::yakugo::canonical_tokens(src, pack).map_err(|e| ParseError::from(e).located(src))?;
PARSES.with(|n| n.set(n.get() + 1));
let mut p = Parser::over(src, toks, MAX_EXPR_DEPTH);
p.program().map_err(|e| e.located(src))
}
pub fn parse_program_with_depth(src: &str, max_depth: usize) -> Result<Vec<Sexp>, ParseError> {
Ok(parse_program_tree_with_depth(src, max_depth)?
.iter()
.map(Spanned::to_sexp)
.collect())
}
pub fn parse_program_spanned(src: &str) -> Result<Vec<(Sexp, Span)>, ParseError> {
parse_program_spanned_with_depth(src, MAX_EXPR_DEPTH)
}
pub fn parse_program_spanned_with_depth(
src: &str,
max_depth: usize,
) -> Result<Vec<(Sexp, Span)>, ParseError> {
Ok(parse_program_tree_with_depth(src, max_depth)?
.iter()
.map(|f| (f.to_sexp(), f.span))
.collect())
}
pub fn comments(src: &str) -> Vec<Comment> {
lex(src)
.map(|toks| {
toks.iter()
.filter_map(|t| match &t.kind {
TokenKind::Comment(text) => Some(Comment {
text: text.clone(),
span: t.span,
own_line: line_before_is_blank(src, t.span.start),
}),
_ => None,
})
.collect()
})
.unwrap_or_default()
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Comment {
pub text: String,
pub span: Span,
pub own_line: bool,
}
fn line_before_is_blank(src: &str, start: usize) -> bool {
src[..start.min(src.len())]
.rsplit('\n')
.next()
.is_some_and(|prefix| prefix.trim().is_empty())
}
pub fn parse_expr(src: &str) -> Result<Sexp, ParseError> {
let forms = parse_program(src)?;
match forms.len() {
1 => Ok(forms.into_iter().next().expect("checked len")),
n => Err(ParseError::new(
format!("expected exactly one expression, found {n}"),
Span::new(0, src.len()),
)
.located(src)),
}
}
#[derive(Clone, Copy, Debug)]
pub struct Infix {
pub op: &'static str,
pub power: (u8, u8),
pub callee: &'static str,
pub doc: &'static str,
}
pub const INFIX: &[Infix] = &[
Infix {
op: "or",
power: (1, 2),
callee: "or",
doc: "Logical or: the left value if truthy, otherwise the right; the right is evaluated only when needed. `||` is read as `or` and written `or` (B0025).",
},
Infix {
op: "and",
power: (3, 4),
callee: "and",
doc: "Logical and: the right value if the left is truthy, otherwise the left; short-circuits. `&&` is read as `and` and written `and` (B0025).",
},
Infix {
op: "==",
power: (5, 6),
callee: "equal?",
doc: "Structural equality over every value (okite D0001): 4 == 4.0, lists and maps by content, nil only equals nil.",
},
Infix {
op: "!=",
power: (5, 6),
callee: "not=",
doc: "Exactly not(a == b) (okite D0002).",
},
Infix {
op: "<",
power: (5, 6),
callee: "<",
doc: "Numeric less-than. A string operand is a type error: order text with compare(a, b).",
},
Infix {
op: "<=",
power: (5, 6),
callee: "<=",
doc: "Numeric less-than-or-equal. Numbers only, like <.",
},
Infix {
op: ">",
power: (5, 6),
callee: ">",
doc: "Numeric greater-than. Numbers only, like <.",
},
Infix {
op: ">=",
power: (5, 6),
callee: ">=",
doc: "Numeric greater-than-or-equal. Numbers only, like <.",
},
Infix {
op: "+",
power: (7, 8),
callee: "+",
doc: "Numeric addition; an Int and a Float give a Float. Not string concatenation: interpolate instead.",
},
Infix {
op: "-",
power: (7, 8),
callee: "-",
doc: "Numeric subtraction. Written before an operand with nothing on its left, it is unary minus: -n.",
},
Infix {
op: "*",
power: (9, 10),
callee: "*",
doc: "Numeric multiplication.",
},
Infix {
op: "/",
power: (9, 10),
callee: "/",
doc: "Division that never truncates (okite D0008): 7 / 2 is 3.5, while 6 / 2 stays the Int 3. Integer division is floor(a / b).",
},
Infix {
op: "%",
power: (9, 10),
callee: "mod",
doc: "Euclidean remainder: never negative, so (0 - 7) % 3 is 2, not -1. A zero divisor is a runtime error.",
},
];
pub const LOWERED_ASSERT: &str = "blue-assert";
pub const LOWERED_MAP: &str = "hash-map";
pub const LOWERED_CONCAT: &str = "concat";
pub const LOWERED_FIELD: &str = "blue-field";
pub const LOWERED_ERROR_MATCH: &str = "blue-rescues?";
pub const LOWERED_ENSURE: &str = "blue-ensure";
pub const LOWERED_RERAISE: &str = "blue-reraise";
pub const LOWERED_ATTEMPTED: &str = "blue-attempted";
pub const LOWERED_FAILED: &str = "blue-failed";
pub const RESCUED: &str = "rescued-error";
pub const SURFACE_KEYWORDS: &[&str] = &[
"if",
"begin",
"unless",
"def",
"defmacro",
"quote",
"unquote",
"unquote_splice",
"test",
"assert",
"fn",
"case",
];
pub const BLOCK_KEYWORDS: &[&str] = &[
"do", "end", "else", "elsif", "rescue", "ensure", "true", "false", "nil",
];
pub const OPERATOR_KEYWORDS: &[&str] = &["and", "or", "not"];
pub fn is_reserved_word(name: &str) -> bool {
SURFACE_KEYWORDS.contains(&name)
|| BLOCK_KEYWORDS.contains(&name)
|| OPERATOR_KEYWORDS.contains(&name)
}
pub fn is_callable_name(name: &str) -> bool {
if qualified(name).is_some() {
return true;
}
!is_reserved_word(name)
&& matches!(
lex(name).as_deref(),
Ok([t, e]) if matches!(&t.kind, TokenKind::Ident(n) if n == name)
&& e.kind == TokenKind::Eof
)
}
pub const QUALIFIER: char = '/';
pub const BUILTIN_QUALIFIER: &str = "blue";
#[must_use]
pub fn qualify(pkg: &str, name: &str) -> String {
format!("{pkg}{QUALIFIER}{name}")
}
#[must_use]
pub fn qualified(sym: &str) -> Option<(&str, &str)> {
let (pkg, name) = sym.split_once(QUALIFIER)?;
let ident = |s: &str| {
matches!(
lex(s).as_deref(),
Ok([t, e]) if matches!(&t.kind, TokenKind::Ident(n) if n == s) && e.kind == TokenKind::Eof
)
};
let named = |s: &str| ident(s) || matches!(s, "true" | "false" | "nil");
(ident(pkg) && !is_reserved_word(pkg) && named(name)).then_some((pkg, name))
}
#[must_use]
pub fn keyword_doc(word: &str) -> &'static str {
match word {
"if" => "`if c … elsif d … else … end`: an expression; its value is the branch taken, nil when none is. An `else` that is empty or `nil` is dropped (B0035).",
"unless" => "`unless c … end` is read as `if not c` and written that way (B0026).",
"begin" => "`begin … rescue :kind as e … rescue e … ensure … end`: run the statements; a `rescue` clause catches an error by kind, binding it with `as`, and `rescue e` catches every error; `ensure` always runs. A def body takes the same clauses without `begin`.",
"rescue" => "A clause of `begin` or `def`: `rescue :a, :b as e`, `rescue e` (every error, last). Inline, `x rescue fallback` is fallback when x raises; it binds loosest, below `|>`.",
"ensure" => "The last clause of `begin` or `def`: statements that run whether the block returned or raised; the block's value or error is kept.",
"and" => "`a and b`: logical and; `&&` is read as `and` and written `and` (B0025).",
"or" => "`a or b`: logical or; `||` is read as `or` and written `or` (B0025).",
"not" => "`not x`: true when x is false or nil; `!x` is read as `not x` and written `not x` (B0025). `not (a == b)` is written `a != b` (B0034).",
"def" => "`def name(a, b) … end`: a named function; its value is the last expression. Parameters may be typed: `def f(n: Int) -> Int`.",
"defmacro" => "`defmacro name(x) quote … unquote(x) … end end`: a macro, a rewrite of the unevaluated form.",
"quote" => "`quote … end`: the form itself as data, not its value. Used inside `defmacro`.",
"unquote" => "`unquote(x)`: inside `quote`, splice in the value of `x`.",
"unquote_splice" => "`unquote_splice(xs)`: inside `quote`, splice the items of the form `xs` in place, not the form itself.",
"test" => "`test \"what must hold\" … end`: a test block, run by `blue test` and ignored by `blue run`.",
"assert" => "`assert expr`: inside a test, fail it unless `expr` is truthy; the failure quotes the expression.",
"fn" => "`fn(x) x * 2 end`: an anonymous function, a value. Blue has no brace or do blocks.",
"case" => "`case v when 1 … when 2 … else … end`: compares with `==`; it does not destructure.",
"do" => "Reserved and refused: blue has no do-blocks. Pass `fn(x) … end` instead.",
"end" => "Closes `def`, `defmacro`, `if`, `case`, `fn`, `test` and `quote`.",
"else" => "The branch of `if` or `case` taken when nothing before it matched.",
"elsif" => "A further condition in an `if` chain; an `else` holding only an `if` is written `elsif` (B0027).",
"true" => "The boolean true.",
"false" => "The boolean false. Only `false` and `nil` are falsy; 0, \"\" and [] are truthy.",
"nil" => "The absent value. It is not `[]` (okite D0004), and only `nil == nil`.",
_ => "",
}
}
fn infix(op: &str) -> Option<&'static Infix> {
INFIX.iter().find(|i| i.op == op)
}
pub const SYMBOL_SPELLINGS: &[(&str, &str)] = &[("&&", "and"), ("||", "or"), ("!", "not")];
fn word_for(op: &str) -> &str {
SYMBOL_SPELLINGS
.iter()
.find(|(sym, _)| *sym == op)
.map_or(op, |(_, word)| word)
}
fn infix_of(kind: &TokenKind) -> Option<&'static Infix> {
match kind {
TokenKind::Op(o) => infix(word_for(o)),
TokenKind::Ident(w) if w == "and" || w == "or" => infix(w),
_ => None,
}
}
const PIPE_POWER: (u8, u8) = (0, 1);
const RESCUE_POWER: (u8, u8) = (0, 1);
struct Parser {
toks: Vec<Token>,
pos: usize,
depth: usize,
max_depth: usize,
src: String,
comment_spans: Vec<Span>,
notes: Vec<Note>,
}
pub const MAX_EXPR_DEPTH: usize = 256;
type Fail = Box<ParseError>;
fn fail(message: impl Into<String>, span: Span) -> Fail {
Box::new(ParseError::new(message, span))
}
impl Parser {
fn over(src: &str, all: Vec<Token>, max_depth: usize) -> Self {
let comment_spans = all
.iter()
.filter(|t| matches!(t.kind, TokenKind::Comment(_)))
.map(|t| t.span)
.collect();
let toks = all
.into_iter()
.filter(|t| !matches!(t.kind, TokenKind::Comment(_)))
.collect();
Parser {
toks,
pos: 0,
depth: 0,
max_depth,
src: src.to_string(),
comment_spans,
notes: Vec::new(),
}
}
fn note(&mut self, spelling: Spelling, span: Span, written: &str, canonical: &str) {
self.notes.push(Note {
spelling,
span,
written: written.to_string(),
canonical: canonical.to_string(),
});
}
fn text(&self, span: Span) -> &str {
self.src.get(span.start..span.end).unwrap_or("")
}
fn comment_within(&self, lo: usize, hi: usize) -> bool {
self.comment_spans
.iter()
.any(|c| c.start >= lo && c.start < hi)
}
fn touches_previous(&self) -> bool {
let here = self.peek_span();
self.toks
.get(self.pos.wrapping_sub(1))
.is_some_and(|prev| prev.span.end == here.start)
}
fn peek(&self) -> &TokenKind {
&self.toks[self.pos.min(self.toks.len() - 1)].kind
}
fn peek_span(&self) -> Span {
self.toks[self.pos.min(self.toks.len() - 1)].span
}
fn bump(&mut self) -> TokenKind {
let k = self.toks[self.pos.min(self.toks.len() - 1)].kind.clone();
if self.pos < self.toks.len() {
self.pos += 1;
}
k
}
fn at(&self, k: &TokenKind) -> bool {
self.peek() == k
}
fn eat(&mut self, k: &TokenKind) -> bool {
if self.at(k) {
self.bump();
true
} else {
false
}
}
fn expect(&mut self, k: &TokenKind, what: &str) -> Result<(), Fail> {
if self.eat(k) {
Ok(())
} else {
Err(self.error(format!("expected {what}, found {}", self.peek())))
}
}
fn error(&self, message: impl Into<String>) -> Fail {
fail(message, self.peek_span())
}
fn skip_newlines(&mut self) {
while matches!(self.peek(), TokenKind::Newline) {
self.bump();
}
}
fn at_ident(&self, name: &str) -> bool {
matches!(self.peek(), TokenKind::Ident(n) if n == name)
}
fn mark(&self) -> usize {
self.peek_span().start
}
fn span_since(&self, start: usize) -> Span {
let end = self
.toks
.get(self.pos.saturating_sub(1))
.map_or(start, |t| t.span.end);
Span::new(start, end.max(start))
}
fn program(&mut self) -> Result<Vec<Spanned>, Fail> {
let mut out = Vec::new();
loop {
self.skip_newlines();
if matches!(self.peek(), TokenKind::Eof) {
break;
}
out.push(self.statement()?);
self.end_of_statement(&[])?;
}
Ok(out)
}
fn end_of_statement(&self, terminators: &[&str]) -> Result<(), Fail> {
let next = self.peek();
if matches!(next, TokenKind::Newline | TokenKind::Eof)
|| terminators.iter().any(|t| self.at_ident(t))
{
return Ok(());
}
let prev = self.toks.get(self.pos.wrapping_sub(1)).map(|t| &t.kind);
let prev_name = match prev {
Some(TokenKind::Ident(n)) => Some(n.as_str()),
_ => None,
};
let callee = prev_name.unwrap_or("f");
let why = match (prev, next) {
(Some(TokenKind::Ident(t)), _) if t == "then" => {
"blue's `if` has no `then`: put the branch on the next line".to_string()
}
(_, TokenKind::Ident(d)) if d == "do" => "blue has no `do` blocks: pass a function \
as an argument, `f(xs, fn(x) … end)`"
.to_string(),
(_, TokenKind::LBracket) => {
let index = match (self.toks.get(self.pos + 1), self.toks.get(self.pos + 2)) {
(Some(i), Some(close)) if close.kind == TokenKind::RBracket => match &i.kind {
TokenKind::Int(n) => n.to_string(),
TokenKind::Ident(n) => n.clone(),
_ => "i".to_string(),
},
_ => "i".to_string(),
};
let list = prev_name.unwrap_or("xs");
format!(
"blue has no `xs[i]` indexing: write `nth({index}, {list})` \
(or `first({list})`, `last({list})`)"
)
}
(_, TokenKind::LBrace) => "blue has no brace blocks: pass a function as an \
argument, `map(xs, fn(x) … end)`"
.to_string(),
(
Some(TokenKind::Ident(_)),
TokenKind::Ident(_)
| TokenKind::Int(_)
| TokenKind::Float(_)
| TokenKind::Str(_)
| TokenKind::InterpolatedStr { .. }
| TokenKind::Sym(_)
| TokenKind::True
| TokenKind::False
| TokenKind::Nil,
) => {
let arg = match next {
TokenKind::Ident(a) => a.clone(),
_ => "x".to_string(),
};
format!("a call needs parentheses: write `{callee}({arg})`")
}
_ => "put each statement on its own line".to_string(),
};
Err(self.error(format!("expected the end of the line, found {next}: {why}")))
}
fn statement(&mut self) -> Result<Spanned, Fail> {
let max = self.max_depth;
if self.depth >= max {
return Err(self.error(format!(
"statement nests deeper than {max}; refusing to \
recurse further (this is a limit, not a syntax error)"
)));
}
self.depth += 1;
let r = self.statement_inner();
self.depth -= 1;
r
}
fn statement_inner(&mut self) -> Result<Spanned, Fail> {
if let TokenKind::Ident(name) = self.peek().clone() {
if self.peek_at(1) == "=" && !is_reserved_word(&name) {
let name_span = self.peek_span();
self.bump(); self.bump(); self.skip_newlines();
let value = self.expr(0)?;
return Ok(list_at(
self.span_since(name_span.start),
vec![sym_at(name_span, "define"), sym_at(name_span, &name), value],
));
}
}
let e = self.expr(0)?;
if let SpannedForm::List(items) = &e.form {
if items.len() == 3
&& items[0].as_symbol() == Some("define")
&& items[1].as_symbol().is_some()
&& self.text(items[0].span) == "define"
{
self.note(Spelling::OperatorSpelling, e.span, "define(x, v)", "x = v");
}
}
Ok(e)
}
fn peek_at(&self, n: usize) -> String {
match self.toks.get(self.pos + n).map(|t| &t.kind) {
Some(TokenKind::Op(o)) => o.clone(),
_ => String::new(),
}
}
fn expr(&mut self, min_bp: u8) -> Result<Spanned, Fail> {
let max = self.max_depth;
if self.depth >= max {
return Err(self.error(format!(
"expression nests deeper than {max}; refusing to \
recurse further (this is a limit, not a syntax error)"
)));
}
self.depth += 1;
let r = self.expr_inner(min_bp);
self.depth -= 1;
r
}
#[inline(never)]
fn rescue_node(&self, start: usize, op_span: Span, lhs: Spanned, fallback: Spanned) -> Spanned {
list_at(
self.span_since(start),
vec![
sym_at(op_span, "try"),
lhs,
list_at(
op_span.merge(fallback.span),
vec![
sym_at(op_span, "catch"),
list_at(op_span, vec![sym_at(op_span, "_")]),
fallback,
],
),
],
)
}
#[inline(never)]
fn infix_spelling(&mut self, callee: Option<&str>, op_span: Span) {
let written = self.text(op_span).to_string();
match (callee, self.peek().clone()) {
(None, _) => self.note(Spelling::CallForm, op_span, "|>", "a call"),
(Some(c), TokenKind::Op(o)) if word_for(&o) != o => {
self.note(Spelling::LogicalSymbol, op_span, &written, c);
}
(Some(_), TokenKind::Op(o)) if written != o => {
self.note(Spelling::OperatorSpelling, op_span, &written, &o);
}
_ => {}
}
}
#[inline(never)]
fn nil_test(&mut self, start: usize, op_span: Span, lhs: &Spanned, rhs: &Spanned) -> Option<Spanned> {
let (subject, written) = match (&lhs.form, &rhs.form) {
(_, SpannedForm::Nil) if !matches!(lhs.form, SpannedForm::Nil) => (lhs, "x == nil"),
(SpannedForm::Nil, _) if !matches!(rhs.form, SpannedForm::Nil) => (rhs, "nil == x"),
_ => return None,
};
let span = self.span_since(start);
self.note(Spelling::NilTest, span, written, "x.nil?");
Some(list_at(
span,
vec![
sym_at(op_span, LOWERED_FIELD),
subject.clone(),
atom_at(op_span, Atom::Keyword("nil?".to_string())),
],
))
}
#[inline(never)]
fn infix_node(
&mut self,
start: usize,
callee: Option<&str>,
op_span: Span,
lhs: Spanned,
rhs: Spanned,
) -> Spanned {
if callee == Some("-") && matches!(lhs.form, SpannedForm::Atom(Atom::Int(0))) {
self.note(
Spelling::OperatorSpelling,
lhs.span.merge(op_span),
"0 - x",
"-x",
);
}
if callee == Some("equal?") {
if let Some(test) = self.nil_test(start, op_span, &lhs, &rhs) {
return test;
}
}
if callee.is_none() {
let rhs_span = rhs.span;
let threaded = match rhs.form {
SpannedForm::List(mut items) if !items.is_empty() => {
items.insert(1, lhs);
list_at(self.span_since(start), items)
}
callee => list_at(
self.span_since(start),
vec![Spanned::new(rhs_span, callee), lhs],
),
};
match &threaded.form {
SpannedForm::List(items)
if items.len() == 2 && items[0].as_symbol() == Some("not") =>
{
let (head, operand) = (items[0].span, items[1].clone());
self.negation(head, threaded.span, operand)
}
SpannedForm::List(items)
if items.len() == 4
&& items[0].as_symbol() == Some("if")
&& matches!(items[3].form, SpannedForm::Nil) =>
{
list_at(threaded.span, items[..3].to_vec())
}
SpannedForm::List(items)
if items.len() == 3 && items[0].as_symbol() == Some("equal?") =>
{
let (op, a, b) = (items[0].span, items[1].clone(), items[2].clone());
match self.nil_test(start, op, &a, &b) {
Some(test) => test,
None => threaded,
}
}
_ => threaded,
}
} else {
list_at(
self.span_since(start),
vec![sym_at(op_span, callee.unwrap_or_default()), lhs, rhs],
)
}
}
fn expr_inner(&mut self, min_bp: u8) -> Result<Spanned, Fail> {
let start = self.mark();
let mut lhs = self.prefix()?;
loop {
match self.peek() {
TokenKind::Dot => {
let dot = self.peek_span();
self.bump();
lhs = self.finish_send(start, lhs, dot)?;
continue;
}
TokenKind::LParen => {
let (args, splat) = self.call_args()?;
let call = self.call_node(self.span_since(start), vec![lhs], args, splat);
lhs = self.call_spelling(call);
continue;
}
_ => {}
}
let op_span = self.peek_span();
if self.at_ident("rescue") {
if RESCUE_POWER.0 < min_bp {
break;
}
self.bump();
self.skip_newlines();
let fallback = self.expr(RESCUE_POWER.1)?;
lhs = self.rescue_node(start, op_span, lhs, fallback);
continue;
}
let (callee, (lbp, rbp)) = match self.peek() {
TokenKind::Pipe => (None, PIPE_POWER),
other => match infix_of(other) {
Some(i) => (Some(i.callee), i.power),
None => break,
},
};
if lbp < min_bp {
break;
}
self.infix_spelling(callee, op_span);
self.bump();
self.skip_newlines();
let rhs = self.expr(rbp)?;
lhs = self.infix_node(start, callee, op_span, lhs, rhs);
}
Ok(lhs)
}
#[inline(never)]
fn interpolation(span: Span, parts: &[String], exprs: &[String]) -> Result<Spanned, Fail> {
let mut acc = atom_at(span, Atom::Str(parts[0].clone()));
for (i, raw) in exprs.iter().enumerate() {
let inner = parse_expr(raw).map_err(|e| {
fail(
format!("in interpolation `#{{{raw}}}`: {}", e.message),
span,
)
})?;
let inner = Spanned::from_sexp_at(&inner, span);
acc = list_at(span, vec![sym_at(span, LOWERED_CONCAT), acc, inner]);
acc = list_at(
span,
vec![
sym_at(span, LOWERED_CONCAT),
acc,
atom_at(span, Atom::Str(parts[i + 1].clone())),
],
);
}
Ok(acc)
}
#[inline(never)]
fn not_prefix(&mut self, span: Span) -> Result<Spanned, Fail> {
if self.at(&TokenKind::LParen) && self.touches_previous() {
self.note(Spelling::OperatorSpelling, span, "not(x)", "not x");
let (args, splat) = self.call_args()?;
if args.len() != 1 || splat.is_some() {
let call = self.call_node(
self.span_since(span.start),
vec![sym_at(span, "not")],
args,
splat,
);
return Ok(call);
}
let operand = args.into_iter().next().expect("one argument");
return Ok(self.negation(span, self.span_since(span.start), operand));
}
let rhs = self.expr(11)?;
Ok(self.negation(span, self.span_since(span.start), rhs))
}
fn prefix(&mut self) -> Result<Spanned, Fail> {
if self.at(&TokenKind::LParen) {
let span = self.peek_span();
self.bump();
if self.arrow_lambda_ahead() {
return self.arrow_lambda(span);
}
return self.group();
}
self.prefix_token()
}
fn group(&mut self) -> Result<Spanned, Fail> {
self.skip_newlines();
let inner = self.expr(0)?;
self.skip_newlines();
self.expect(&TokenKind::RParen, "`)`")?;
Ok(inner)
}
#[inline(never)]
fn prefix_token(&mut self) -> Result<Spanned, Fail> {
let span = self.peek_span();
match self.bump() {
TokenKind::Int(v) => Ok(atom_at(span, Atom::Int(v))),
TokenKind::Float(v) => Ok(atom_at(span, Atom::Float(v))),
TokenKind::Str(s) => Ok(atom_at(span, Atom::Str(s))),
TokenKind::InterpolatedStr { parts, exprs } => Self::interpolation(span, &parts, &exprs),
TokenKind::Sym(s) => Ok(atom_at(span, Atom::Keyword(s))),
TokenKind::True => Ok(atom_at(span, Atom::Bool(true))),
TokenKind::False => Ok(atom_at(span, Atom::Bool(false))),
TokenKind::Nil => Ok(Spanned::new(span, SpannedForm::Nil)),
TokenKind::Op(o) if o == "-" => {
let rhs = self.expr(11)?; Ok(list_at(
self.span_since(span.start),
vec![sym_at(span, "-"), atom_at(span, Atom::Int(0)), rhs],
))
}
TokenKind::Op(o) if o == "!" => {
let written = self.text(span).to_string();
self.note(Spelling::LogicalSymbol, span, &written, "not");
let rhs = self.expr(11)?;
Ok(self.negation(span, self.span_since(span.start), rhs))
}
TokenKind::Ident(w) if w == "not" => self.not_prefix(span),
TokenKind::Ident(w) if (w == "and" || w == "or") && self.at(&TokenKind::LParen) => {
Ok(sym_at(span, &w))
}
TokenKind::Ident(w) if w == "and" || w == "or" => Err(fail(
format!("`{w}` joins two expressions: `a {w} b`"),
span,
)),
TokenKind::LParen => {
self.skip_newlines();
let inner = self.expr(0)?;
self.skip_newlines();
self.expect(&TokenKind::RParen, "`)`")?;
Ok(inner)
}
TokenKind::LBracket => self.list_literal(span),
TokenKind::LBrace => self.map_literal(span),
TokenKind::Ident(name) if self.at(&TokenKind::PathSep) => {
self.qualified_name(span, &name)
}
TokenKind::PathSep => Err(fail(
"a qualified name is `package::name`, with the package written before `::`",
span,
)),
TokenKind::Ident(name) => match name.as_str() {
"if" => self.if_form(span, false),
"begin" if !(self.at(&TokenKind::LParen) && self.touches_previous()) => {
self.begin_form(span)
}
"rescue" | "ensure" => Err(fail(
format!("`{name}` belongs to a `begin … end` block or a `def` body, after the statements it protects"),
span,
)),
"unless" => {
self.note(Spelling::Unless, span, "unless", "if not");
self.if_form(span, true)
}
"def" => self.def_form(span),
"defmacro" => self.defmacro_form(span),
"case" => self.case_form(span),
"fn" => self.lambda_form(span),
"test" => self.test_form(span),
"assert" => self.assert_form(span),
"quote" => self.quote_form(span),
"unquote" => self.unquote_form(span, false),
"unquote_splice" => self.unquote_form(span, true),
"do" => Err(fail(
"blue has no `do` blocks: pass a function as an argument, \
`f(xs, fn(x) … end)`",
span,
)),
"end" => Err(fail(
"unexpected `end`: nothing open here for it to close",
span,
)),
_ => Ok(sym_at(span, &name)),
},
other => Err(fail(format!("expected an expression, found {other}"), span)),
}
}
fn qualified_name(&mut self, pkg_span: Span, pkg: &str) -> Result<Spanned, Fail> {
if is_reserved_word(pkg) {
return Err(fail(
format!("`{pkg}` is a reserved word and cannot name a package"),
pkg_span,
));
}
self.bump(); let name_span = self.peek_span();
let name = match self.bump() {
TokenKind::Ident(n) => n,
TokenKind::True => "true".to_string(),
TokenKind::False => "false".to_string(),
TokenKind::Nil => "nil".to_string(),
other => {
return Err(fail(
format!("expected a name after `{pkg}::`, found {other}"),
name_span,
))
}
};
if self.at(&TokenKind::PathSep) {
return Err(self.error(format!(
"a qualified name has one qualifier: `package::name`, and `{pkg}::{name}::…` has two"
)));
}
Ok(sym_at(
self.span_since(pkg_span.start),
&qualify(pkg, &name),
))
}
#[inline(never)]
fn negation(&mut self, head: Span, whole: Span, operand: Spanned) -> Spanned {
if let SpannedForm::List(items) = &operand.form {
let flipped = match items.first().and_then(Spanned::as_symbol) {
Some("equal?") => Some("not="),
Some("not=") => Some("equal?"),
_ => None,
};
if let (Some(to), 3) = (flipped, items.len()) {
let (written, canonical) = if to == "not=" {
("not (a == b)", "a != b")
} else {
("not (a != b)", "a == b")
};
self.note(Spelling::NegatedComparison, whole, written, canonical);
let mut out = items.clone();
out[0] = sym_at(items[0].span, to);
if to == "equal?" {
if let Some(test) =
self.nil_test(whole.start, items[0].span, &items[1], &items[2])
{
return test;
}
}
return list_at(whole, out);
}
}
list_at(whole, vec![sym_at(head, "not"), operand])
}
#[inline(never)]
fn call_spelling(&mut self, call: Spanned) -> Spanned {
let SpannedForm::List(items) = &call.form else {
return call;
};
let Some(head) = items.first().and_then(Spanned::as_symbol) else {
return call;
};
let argc = items.len() - 1;
if head == "equal?" && argc == 2 {
let (op_span, start) = (items[0].span, call.span.start);
if let Some(test) = self.nil_test(start, op_span, &items[1], &items[2]) {
return test;
}
}
let operator = match (head, argc) {
("and" | "or" | "mod" | "equal?", 2) => Some(head),
("list", _) => Some("list"),
_ => None,
};
if let Some(op) = operator {
let canonical = match op {
"list" => "[…]".to_string(),
"mod" => "a % b".to_string(),
"equal?" => "a == b".to_string(),
other => format!("a {other} b"),
};
self.note(
Spelling::OperatorSpelling,
call.span,
&format!("{head}(…)"),
&canonical,
);
return call;
}
if head == "apply"
&& self.text(items[0].span) == "apply"
&& argc >= 2
&& items[1].as_symbol().is_some_and(is_callable_name)
{
self.note(Spelling::Apply, call.span, "apply(f, …, xs)", "f(…, *xs)");
return call;
}
if head == "try" {
return self.try_spelling(call);
}
call
}
fn try_spelling(&mut self, call: Spanned) -> Spanned {
let SpannedForm::List(items) = &call.form else {
return call;
};
let Some((catch, body)) = items[1..].split_last() else {
return call;
};
let Some(clause) = catch.as_list() else {
return call;
};
let binder = match clause {
[head, binding, handler @ ..]
if head.as_symbol() == Some("catch") && !handler.is_empty() =>
{
match binding.as_list() {
Some([b]) => b.as_symbol().map(|n| (n.to_string(), b.span)),
_ => None,
}
}
_ => None,
};
let Some((name, binder_span)) = binder else {
return call;
};
if body.is_empty() {
return call;
}
self.note(Spelling::TryCatch, call.span, "try(…, catch(…))", "rescue");
let handler = &clause[2..];
let rename =
name.starts_with('_') && name != "_" && !handler.iter().any(|h| mentions(h, &name));
let body = if body.len() == 1 {
body[0].clone()
} else {
let span = cover(body, call.span);
let mut list = vec![sym_at(Span::new(span.start, span.start), "begin")];
list.extend(body.iter().cloned());
list_at(span, list)
};
let handler = if handler.len() == 1 {
handler[0].clone()
} else {
let span = cover(handler, catch.span);
let mut list = vec![sym_at(Span::new(span.start, span.start), "begin")];
list.extend(handler.iter().cloned());
list_at(span, list)
};
let binder = sym_at(binder_span, if rename { "_" } else { &name });
list_at(
call.span,
vec![
items[0].clone(),
body,
list_at(
catch.span,
vec![
clause[0].clone(),
list_at(clause[1].span, vec![binder]),
handler,
],
),
],
)
}
fn finish_send(&mut self, start: usize, recv: Spanned, dot: Span) -> Result<Spanned, Fail> {
let name_span = self.peek_span();
let name = match self.bump() {
TokenKind::Ident(n) if is_reserved_word(&n) => {
return Err(self.error(format!(
"`{n}` is a reserved word and cannot be a method name — \
`recv.{n}` would lower onto the `{n}` form itself"
)))
}
TokenKind::Ident(n) => n,
other => {
return Err(fail(
format!("expected a method name after `.`, found {other}"),
name_span,
))
}
};
if !self.at(&TokenKind::LParen) {
return Ok(list_at(
self.span_since(start),
vec![
sym_at(dot, LOWERED_FIELD),
recv,
atom_at(name_span, Atom::Keyword(name)),
],
));
}
self.note(
Spelling::CallForm,
dot.merge(name_span),
&format!("x.{name}(…)"),
&format!("{name}(x, …)"),
);
let (args, splat) = self.call_args()?;
Ok(self.call_node(
self.span_since(start),
vec![sym_at(name_span, &name), recv],
args,
splat,
))
}
fn call_node(
&self,
span: Span,
head: Vec<Spanned>,
args: Vec<Spanned>,
splat: Option<Span>,
) -> Spanned {
let mut list = Vec::with_capacity(head.len() + args.len() + 1);
if let Some(at) = splat {
list.push(sym_at(at, "apply"));
}
list.extend(head);
list.extend(args);
list_at(span, list)
}
fn paren_args(&mut self) -> Result<Vec<Spanned>, Fail> {
let (args, splat) = self.call_args()?;
if let Some(at) = splat {
return Err(fail(
"a splat `*xs` passes a list as a call's last arguments, and this is not a call",
at,
));
}
Ok(args)
}
fn call_args(&mut self) -> Result<(Vec<Spanned>, Option<Span>), Fail> {
self.expect(&TokenKind::LParen, "`(`")?;
let mut args = Vec::new();
let mut splat = None;
self.skip_newlines();
if self.eat(&TokenKind::RParen) {
return Ok((args, splat));
}
loop {
self.skip_newlines();
if splat.is_some() {
return Err(
self.error("a splat `*xs` passes a list as the LAST arguments: write it last")
);
}
if matches!(self.peek(), TokenKind::Op(o) if o == "*") {
splat = Some(self.peek_span());
self.bump();
}
args.push(self.expr(0)?);
self.skip_newlines();
if self.eat(&TokenKind::Comma) {
if self.trailing_comma(&TokenKind::RParen) {
break;
}
continue;
}
self.expect(&TokenKind::RParen, "`,` or `)`")?;
break;
}
Ok((args, splat))
}
fn trailing_comma(&mut self, closer: &TokenKind) -> bool {
let comma = self
.toks
.get(self.pos.wrapping_sub(1))
.map_or(self.peek_span(), |t| t.span);
self.skip_newlines();
if self.at(closer) {
self.note(Spelling::TrailingComma, comma, ",", "");
self.bump();
return true;
}
false
}
fn list_literal(&mut self, open: Span) -> Result<Spanned, Fail> {
let mut items = vec![sym_at(open, "list")];
self.skip_newlines();
if self.eat(&TokenKind::RBracket) {
return Ok(list_at(self.span_since(open.start), items));
}
loop {
self.skip_newlines();
items.push(self.expr(0)?);
self.skip_newlines();
if self.eat(&TokenKind::Comma) {
if self.trailing_comma(&TokenKind::RBracket) {
break;
}
continue;
}
self.expect(&TokenKind::RBracket, "`,` or `]`")?;
break;
}
Ok(list_at(self.span_since(open.start), items))
}
fn map_literal(&mut self, open: Span) -> Result<Spanned, Fail> {
let mut items = vec![sym_at(open, LOWERED_MAP)];
self.skip_newlines();
if self.eat(&TokenKind::RBrace) {
return Ok(list_at(self.span_since(open.start), items));
}
loop {
self.skip_newlines();
match self.peek().clone() {
TokenKind::Label(name) => {
let label_span = self.peek_span();
self.bump();
self.skip_newlines();
items.push(atom_at(label_span, Atom::Keyword(name.clone())));
if matches!(self.peek(), TokenKind::Comma | TokenKind::RBrace) {
items.push(sym_at(label_span, &name));
} else {
let value = self.expr(0)?;
if matches!(&value.form, SpannedForm::Atom(Atom::Symbol(s)) if *s == name)
&& label_name(&name)
{
let written = format!("{name}: {name}");
let canonical = format!("{name}:");
self.note(
Spelling::MapPun,
label_span.merge(value.span),
&written,
&canonical,
);
}
items.push(value);
}
}
_ => {
let k = self.expr(0)?;
self.skip_newlines();
if let SpannedForm::Atom(Atom::Keyword(name)) = &k.form {
if blue_lang_name(name) {
let written = format!(":{name} =>");
let canonical = format!("{name}:");
self.note(Spelling::KeywordRocket, k.span, &written, &canonical);
}
}
self.expect(&TokenKind::Rocket, "`=>` in a map literal")?;
self.skip_newlines();
items.push(k);
items.push(self.expr(0)?);
}
}
self.skip_newlines();
if self.eat(&TokenKind::Comma) {
if self.trailing_comma(&TokenKind::RBrace) {
break;
}
continue;
}
self.expect(&TokenKind::RBrace, "`,` or `}`")?;
break;
}
Ok(list_at(self.span_since(open.start), items))
}
fn if_chain(&mut self, head: Span) -> Result<Spanned, Fail> {
let cond = self.expr(0)?;
let then = self.body(&["elsif", "else", "end"])?;
let els = self.else_part()?;
let mut out = vec![sym_at(head, "if"), cond, then];
if let Some(e) = els {
out.push(e);
}
Ok(list_at(self.span_since(head.start), out))
}
fn else_part(&mut self) -> Result<Option<Spanned>, Fail> {
if self.at_ident("elsif") {
let elsif = self.peek_span();
self.bump();
return Ok(Some(self.if_chain(elsif)?));
}
if !self.at_ident("else") {
self.expect_ident("end")?;
return Ok(None);
}
let else_span = self.peek_span();
self.bump();
let save = self.pos;
self.skip_newlines();
let leads_with_if = self.at_ident("if") || self.at_ident("unless");
self.pos = save;
let e = self.body(&["end"])?;
let end_at = self.peek_span().start;
self.expect_ident("end")?;
if matches!(e.form, SpannedForm::Nil) && !self.comment_within(else_span.start, end_at) {
self.note(Spelling::ElseNil, else_span, "else nil", "no else");
return Ok(None);
}
let single_if = matches!(&e.form, SpannedForm::List(items)
if (items.len() == 3 || items.len() == 4) && items[0].as_symbol() == Some("if"));
if leads_with_if && single_if {
self.note(Spelling::ElseIf, else_span, "else if", "elsif");
}
Ok(Some(e))
}
fn if_form(&mut self, head: Span, negate: bool) -> Result<Spanned, Fail> {
let cond = self.expr(0)?;
let cond = if negate {
let inner = cond.span;
self.negation(head, head.merge(inner), cond)
} else {
cond
};
let then = self.body(&["elsif", "else", "end"])?;
let els = self.else_part()?;
let mut out = vec![sym_at(head, "if"), cond, then];
if let Some(e) = els {
out.push(e);
}
Ok(list_at(self.span_since(head.start), out))
}
fn case_form(&mut self, head: Span) -> Result<Spanned, Fail> {
let subject = self.expr(0)?;
self.skip_newlines();
let subject_var = "case-subject";
let mut arms: Vec<Spanned> = Vec::new();
let mut otherwise: Option<Spanned> = None;
let mut else_kw: Option<Span> = None;
loop {
self.skip_newlines();
if self.at_ident("end") {
break;
}
let maybe_else = self.peek_span();
if self.eat_ident("else") {
else_kw = Some(maybe_else);
otherwise = Some(self.body(&["end"])?);
continue;
}
let when = self.peek_span();
if !self.eat_ident("when") {
return Err(self.error(format!(
"expected `when`, `else` or `end` in a case, found {}",
self.peek()
)));
}
self.skip_newlines();
let pattern = self.expr(0)?;
let body = self.body(&["when", "else", "end"])?;
let test = list_at(
when.merge(pattern.span),
vec![sym_at(when, "equal?"), sym_at(when, subject_var), pattern],
);
arms.push(list_at(when.merge(body.span), vec![test, body]));
}
self.expect_ident("end")?;
if arms.is_empty() && otherwise.is_none() {
return Err(self.error("a case needs at least one `when` or an `else`".to_string()));
}
let mut cond = vec![sym_at(head, "cond")];
cond.extend(arms);
let else_span = else_kw.unwrap_or(head);
let else_body = otherwise.unwrap_or_else(|| Spanned::new(else_span, SpannedForm::Nil));
cond.push(list_at(
else_span.merge(else_body.span),
vec![sym_at(else_span, "else"), else_body],
));
let whole = self.span_since(head.start);
let binding = list_at(
head.merge(subject.span),
vec![sym_at(head, subject_var), subject],
);
Ok(list_at(
whole,
vec![
sym_at(head, "let"),
list_at(binding.span, vec![binding]),
list_at(whole, cond),
],
))
}
fn arrow_lambda_ahead(&self) -> bool {
let mut i = self.pos;
let kind = |i: usize| self.toks.get(i).map(|t| &t.kind);
if !matches!(kind(i), Some(TokenKind::RParen)) {
loop {
if !matches!(kind(i), Some(TokenKind::Ident(_))) {
return false;
}
i += 1;
match kind(i) {
Some(TokenKind::Comma) => i += 1,
Some(TokenKind::RParen) => break,
_ => return false,
}
}
}
matches!(kind(i + 1), Some(TokenKind::Op(o)) if o == "->")
}
fn arrow_lambda(&mut self, head: Span) -> Result<Spanned, Fail> {
let mut params: Vec<Spanned> = Vec::new();
while !self.at(&TokenKind::RParen) {
let p_span = self.peek_span();
match self.bump() {
TokenKind::Ident(p) => params.push(sym_at(p_span, &p)),
other => {
return Err(fail(
format!("expected a parameter name, found {other}"),
p_span,
))
}
}
self.eat(&TokenKind::Comma);
}
self.bump();
self.bump();
self.skip_newlines();
let params_span = cover(¶ms, head);
let body = self.expr(0)?;
Ok(list_at(
self.span_since(head.start),
vec![sym_at(head, "lambda"), list_at(params_span, params), body],
))
}
fn lambda_form(&mut self, head: Span) -> Result<Spanned, Fail> {
let mut params: Vec<Spanned> = Vec::new();
if !self.at(&TokenKind::LParen) {
self.note(Spelling::ParameterList, head, "fn", "fn()");
}
if self.at(&TokenKind::LParen) {
self.bump();
self.skip_newlines();
if !self.eat(&TokenKind::RParen) {
loop {
self.skip_newlines();
let p_span = self.peek_span();
match self.bump() {
TokenKind::Ident(p) => params.push(sym_at(p_span, &p)),
other => {
return Err(fail(
format!("expected a parameter name, found {other}"),
p_span,
))
}
}
self.skip_newlines();
if self.eat(&TokenKind::Comma) {
if self.trailing_comma(&TokenKind::RParen) {
break;
}
continue;
}
self.expect(&TokenKind::RParen, "`,` or `)`")?;
break;
}
}
}
let params_span = cover(¶ms, head);
let body = self.body(&["end"])?;
self.expect_ident("end")?;
Ok(list_at(
self.span_since(head.start),
vec![sym_at(head, "lambda"), list_at(params_span, params), body],
))
}
fn test_form(&mut self, head: Span) -> Result<Spanned, Fail> {
let name_span = self.peek_span();
let name = match self.bump() {
TokenKind::Str(s) => s,
other => {
return Err(fail(
format!("expected a string name after `test`, found {other}"),
name_span,
))
}
};
let body = self.body(&["end"])?;
self.expect_ident("end")?;
Ok(list_at(
self.span_since(head.start),
vec![
sym_at(head, "deftest"),
atom_at(name_span, Atom::Str(name)),
body,
],
))
}
fn assert_form(&mut self, head: Span) -> Result<Spanned, Fail> {
let e = self.expr(0)?;
let quoted = Spanned::new(e.span, SpannedForm::Quote(Box::new(e.clone())));
Ok(list_at(
self.span_since(head.start),
vec![sym_at(head, LOWERED_ASSERT), quoted, e],
))
}
fn defmacro_form(&mut self, head: Span) -> Result<Spanned, Fail> {
let name_span = self.peek_span();
let name = match self.bump() {
TokenKind::Ident(n) => n,
other => {
return Err(fail(
format!("expected a name after `defmacro`, found {other}"),
name_span,
))
}
};
let mut params: Vec<Spanned> = Vec::new();
if self.at(&TokenKind::LParen) {
self.bump();
self.skip_newlines();
if !self.eat(&TokenKind::RParen) {
loop {
self.skip_newlines();
let p_span = self.peek_span();
match self.bump() {
TokenKind::Ident(p) => params.push(sym_at(p_span, &p)),
TokenKind::Label(p) => {
return Err(self.error(format!(
"macro parameter `{p}` cannot be typed: a macro receives \
source forms, not values"
)))
}
other => {
return Err(fail(
format!("expected a parameter name, found {other}"),
p_span,
))
}
}
self.skip_newlines();
if self.eat(&TokenKind::Comma) {
if self.trailing_comma(&TokenKind::RParen) {
break;
}
continue;
}
self.expect(&TokenKind::RParen, "`,` or `)`")?;
break;
}
}
}
if matches!(self.peek(), TokenKind::Op(o) if o == "->") {
return Err(self.error(
"a macro has no return type: it produces source forms, not values".to_string(),
));
}
let params_span = cover(¶ms, name_span);
let body = self.body(&["end"])?;
self.expect_ident("end")?;
Ok(list_at(
self.span_since(head.start),
vec![
sym_at(head, "defmacro"),
sym_at(name_span, &name),
list_at(params_span, params),
body,
],
))
}
fn quote_form(&mut self, head: Span) -> Result<Spanned, Fail> {
let body = self.body(&["end"])?;
self.expect_ident("end")?;
Ok(Spanned::new(
self.span_since(head.start),
SpannedForm::Quasiquote(Box::new(body)),
))
}
fn unquote_form(&mut self, head: Span, splice: bool) -> Result<Spanned, Fail> {
self.expect(&TokenKind::LParen, "`(` after unquote")?;
self.skip_newlines();
let inner = self.expr(0)?;
self.skip_newlines();
self.expect(&TokenKind::RParen, "`)`")?;
let span = self.span_since(head.start);
Ok(Spanned::new(
span,
if splice {
SpannedForm::UnquoteSplice(Box::new(inner))
} else {
SpannedForm::Unquote(Box::new(inner))
},
))
}
fn protected_body(&mut self, head: Span) -> Result<Spanned, Fail> {
let body = self.body(&["rescue", "ensure", "end"])?;
if self.at_ident("rescue") || self.at_ident("ensure") {
return self.rescue_tail(head, body);
}
Ok(body)
}
fn begin_form(&mut self, head: Span) -> Result<Spanned, Fail> {
let body = self.body(&["rescue", "ensure", "end"])?;
if !(self.at_ident("rescue") || self.at_ident("ensure")) {
return Err(self.error(
"a `begin` block protects its statements with `rescue` or `ensure`; without either, write the statements directly",
));
}
let tree = self.rescue_tail(head, body)?;
self.expect_ident("end")?;
Ok(Spanned::new(self.span_since(head.start), tree.form))
}
fn rescue_tail(&mut self, head: Span, body: Spanned) -> Result<Spanned, Fail> {
let mut clauses: Vec<Spanned> = Vec::new();
let mut catch_all: Option<(Spanned, Spanned, Span)> = None;
while self.at_ident("rescue") {
let kw = self.peek_span();
self.bump();
if catch_all.is_some() {
return Err(fail(
"a `rescue NAME` catches every error, so it is the last `rescue`; put the kinds it would shadow above it",
kw,
));
}
match self.peek().clone() {
TokenKind::Sym(_) => {
let mut kinds = Vec::new();
loop {
let k_span = self.peek_span();
match self.bump() {
TokenKind::Sym(k) => kinds.push(atom_at(k_span, Atom::Keyword(k))),
other => {
return Err(fail(
format!("expected an error kind such as `:not_found`, found {other}"),
k_span,
))
}
}
if self.eat(&TokenKind::Comma) {
self.skip_newlines();
continue;
}
break;
}
let binder = if self.eat_ident("as") {
let b_span = self.peek_span();
match self.bump() {
TokenKind::Ident(b) if !is_reserved_word(&b) => Some(sym_at(b_span, &b)),
other => {
return Err(fail(
format!("expected the name the error is bound to after `as`, found {other}"),
b_span,
))
}
}
} else {
None
};
let handler = self.body(&["rescue", "ensure", "end"])?;
let mut test = vec![sym_at(kw, LOWERED_ERROR_MATCH), sym_at(kw, RESCUED)];
test.extend(kinds);
let test = list_at(cover(&test, kw), test);
let handler = match binder {
Some(b) => let_bound(b, handler, kw),
None => handler,
};
clauses.push(list_at(kw.merge(handler.span), vec![test, handler]));
}
TokenKind::Ident(b) if !is_reserved_word(&b) => {
let b_span = self.peek_span();
self.bump();
let handler = self.body(&["rescue", "ensure", "end"])?;
catch_all = Some((sym_at(b_span, &b), handler, kw));
}
_ => {
return Err(fail(
"`rescue` names what it catches: `rescue :kind`, `rescue :a, :b as e`, or `rescue e` for every error (`rescue _` discards it)",
kw,
))
}
}
}
let ensure = if self.at_ident("ensure") {
self.bump();
let cleanup = self.body(&["rescue", "end"])?;
if self.at_ident("rescue") {
return Err(self.error("`ensure` comes after every `rescue` of its block"));
}
Some(cleanup)
} else {
None
};
let whole = self.span_since(head.start);
let protected = if clauses.is_empty() {
match catch_all {
None => body,
Some((binder, handler, kw)) => list_at(
whole,
vec![
sym_at(head, "try"),
body,
list_at(
kw.merge(handler.span),
vec![
sym_at(kw, "catch"),
list_at(binder.span, vec![binder]),
handler,
],
),
],
),
}
} else {
let first_kw = clauses[0].span;
let otherwise = match catch_all {
Some((binder, handler, kw)) => {
let bound = let_bound(binder, handler, kw);
list_at(kw.merge(bound.span), vec![sym_at(kw, "else"), bound])
}
None => list_at(
head,
vec![
sym_at(head, "else"),
list_at(
head,
vec![sym_at(head, LOWERED_RERAISE), sym_at(head, RESCUED)],
),
],
),
};
let mut cond = vec![sym_at(first_kw, "cond")];
cond.extend(clauses);
cond.push(otherwise);
let cond_span = cover(&cond, first_kw);
list_at(
whole,
vec![
sym_at(head, "try"),
body,
list_at(
cond_span,
vec![
sym_at(first_kw, "catch"),
list_at(first_kw, vec![sym_at(first_kw, RESCUED)]),
list_at(cond_span, cond),
],
),
],
)
};
Ok(match ensure {
None => protected,
Some(cleanup) => {
let attempted = list_at(
protected.span,
vec![sym_at(head, LOWERED_ATTEMPTED), protected],
);
let failed = list_at(
head,
vec![sym_at(head, LOWERED_FAILED), sym_at(head, RESCUED)],
);
let caught = list_at(
whole,
vec![
sym_at(head, "try"),
attempted,
list_at(
head,
vec![
sym_at(head, "catch"),
list_at(head, vec![sym_at(head, RESCUED)]),
failed,
],
),
],
);
list_at(
whole,
vec![sym_at(head, LOWERED_ENSURE), caught, thunk(cleanup, head)],
)
}
})
}
fn def_form(&mut self, head: Span) -> Result<Spanned, Fail> {
let name_span = self.peek_span();
let name = match self.bump() {
TokenKind::Ident(n) => n,
other => {
return Err(fail(
format!("expected a name after `def`, found {other}"),
name_span,
))
}
};
let mut params: Vec<(String, Span, Option<Spanned>)> = Vec::new();
if !self.at(&TokenKind::LParen) {
self.note(
Spelling::ParameterList,
name_span,
&format!("def {name}"),
&format!("def {name}()"),
);
}
if self.at(&TokenKind::LParen) {
self.bump();
self.skip_newlines();
if !self.eat(&TokenKind::RParen) {
loop {
self.skip_newlines();
let p_span = self.peek_span();
match self.bump() {
TokenKind::Ident(p) => params.push((p, p_span, None)),
TokenKind::Label(p) => {
self.skip_newlines();
let ty = self.type_expr()?;
params.push((p, p_span, Some(ty)));
}
other => {
return Err(fail(
format!("expected a parameter name, found {other}"),
p_span,
))
}
}
self.skip_newlines();
if self.eat(&TokenKind::Comma) {
if self.trailing_comma(&TokenKind::RParen) {
break;
}
continue;
}
self.expect(&TokenKind::RParen, "`,` or `)`")?;
break;
}
}
}
let sig_span = self.span_since(name_span.start);
let ret = if matches!(self.peek(), TokenKind::Op(o) if o == "->") {
self.bump();
self.skip_newlines();
Some(self.type_expr()?)
} else {
None
};
let begins = {
let save = self.pos;
self.skip_newlines();
let at = self.at_ident("begin").then(|| self.peek_span());
self.pos = save;
at
};
let body = self.protected_body(head)?;
if let Some(b) = begins {
let protects = matches!(&body.form, SpannedForm::List(items)
if matches!(items.first().and_then(Spanned::as_symbol), Some("try") | Some(LOWERED_ENSURE)));
if protects && body.span.start == b.start {
self.note(
Spelling::TryCatch,
b,
"begin … end as a whole def body",
"the clauses on the def itself",
);
}
}
self.expect_ident("end")?;
let whole = self.span_since(head.start);
let annotated = ret.is_some() || params.iter().any(|(_, _, t)| t.is_some());
if !annotated {
let mut sig = vec![sym_at(name_span, &name)];
sig.extend(params.into_iter().map(|(p, s, _)| sym_at(s, &p)));
return Ok(list_at(
whole,
vec![sym_at(head, "define"), list_at(sig_span, sig), body],
));
}
let mut sig = vec![sym_at(name_span, &name)];
for (p, s, t) in params {
let ty = t.unwrap_or_else(|| sym_at(s, "dyn"));
sig.push(list_at(s.merge(ty.span), vec![sym_at(s, &p), ty]));
}
Ok(list_at(
whole,
vec![
sym_at(head, "define-typed"),
list_at(sig_span, sig),
ret.unwrap_or_else(|| sym_at(head, "dyn")),
body,
],
))
}
fn type_expr(&mut self) -> Result<Spanned, Fail> {
let name_span = self.peek_span();
let name = match self.bump() {
TokenKind::Ident(n) => n,
other => {
return Err(fail(
format!("expected a type name, found {other}"),
name_span,
))
}
};
if self.at(&TokenKind::LParen) {
let args = self.paren_args()?;
let mut list = vec![sym_at(name_span, &name)];
list.extend(args);
return Ok(list_at(self.span_since(name_span.start), list));
}
Ok(sym_at(name_span, &name))
}
fn eat_ident(&mut self, name: &str) -> bool {
if self.at_ident(name) {
self.bump();
true
} else {
false
}
}
fn expect_ident(&mut self, name: &str) -> Result<(), Fail> {
if self.at_ident(name) {
self.bump();
Ok(())
} else {
Err(self.error(format!("expected `{name}`, found {}", self.peek())))
}
}
fn body(&mut self, terminators: &[&str]) -> Result<Spanned, Fail> {
let start = self.mark();
let mut forms = Vec::new();
loop {
self.skip_newlines();
if matches!(self.peek(), TokenKind::Eof) {
return Err(self.error(format!(
"unterminated block: expected {} before the end of the file",
one_of(terminators)
)));
}
if terminators.iter().any(|t| self.at_ident(t)) {
break;
}
forms.push(self.statement()?);
self.end_of_statement(terminators)?;
}
Ok(match forms.len() {
0 => Spanned::new(Span::new(start, start), SpannedForm::Nil),
1 => forms.into_iter().next().expect("checked len"),
_ => {
let span = cover(&forms, Span::new(start, start));
let head = Span::new(span.start, span.start);
let mut list = vec![sym_at(head, "begin")];
list.extend(forms);
list_at(span, list)
}
})
}
}
fn one_of(words: &[&str]) -> String {
let quoted: Vec<String> = words.iter().map(|w| format!("`{w}`")).collect();
match quoted.split_last() {
None => String::new(),
Some((last, [])) => last.clone(),
Some((last, rest)) => format!("{} or {last}", rest.join(", ")),
}
}
fn let_bound(binder: Spanned, body: Spanned, at: Span) -> Spanned {
let span = at.merge(body.span);
let pair = list_at(binder.span, vec![binder, sym_at(at, RESCUED)]);
list_at(
span,
vec![sym_at(at, "let"), list_at(pair.span, vec![pair]), body],
)
}
fn thunk(body: Spanned, at: Span) -> Spanned {
let span = at.merge(body.span);
list_at(
span,
vec![sym_at(at, "lambda"), list_at(at, Vec::new()), body],
)
}
#[must_use]
pub fn label_name(name: &str) -> bool {
matches!(
lex(&format!("{name}:")).as_deref(),
Ok([t, e]) if matches!(&t.kind, TokenKind::Label(n) if n == name) && e.kind == TokenKind::Eof
)
}
fn blue_lang_name(name: &str) -> bool {
matches!(
lex(name).as_deref(),
Ok([t, e]) if matches!(&t.kind, TokenKind::Ident(n) if n == name) && e.kind == TokenKind::Eof
) && !name.ends_with(['?', '!'])
}
fn mentions(form: &Spanned, name: &str) -> bool {
match &form.form {
SpannedForm::Atom(Atom::Symbol(s)) => s == name,
SpannedForm::List(items) => items.iter().any(|i| mentions(i, name)),
SpannedForm::Quote(i)
| SpannedForm::Quasiquote(i)
| SpannedForm::Unquote(i)
| SpannedForm::UnquoteSplice(i) => mentions(i, name),
_ => false,
}
}
fn sym_at(span: Span, s: &str) -> Spanned {
Spanned::new(span, SpannedForm::Atom(Atom::Symbol(s.to_string())))
}
fn atom_at(span: Span, a: Atom) -> Spanned {
Spanned::new(span, SpannedForm::Atom(a))
}
fn list_at(span: Span, items: Vec<Spanned>) -> Spanned {
Spanned::new(span, SpannedForm::List(items))
}
fn cover(items: &[Spanned], fallback: Span) -> Span {
let merged = items
.iter()
.fold(Span::synthetic(), |acc, s| acc.merge(s.span));
if merged.is_synthetic() {
fallback
} else {
merged
}
}
#[cfg(test)]
mod tests {
use super::*;
fn q(src: &str) -> String {
parse_expr(src)
.map(|s| s.to_string())
.unwrap_or_else(|e| panic!("{src:?}: {e}"))
}
#[test]
fn arithmetic_respects_precedence() {
assert_eq!(q("1 + 2 * 3"), "(+ 1 (* 2 3))");
assert_eq!(q("(1 + 2) * 3"), "(* (+ 1 2) 3)");
}
#[test]
fn comparison_binds_looser_than_arithmetic() {
assert_eq!(q("a + 1 < b"), "(< (+ a 1) b)");
}
#[test]
fn logical_operators_bind_loosest_and_lower_to_tataras_names() {
assert_eq!(q("a && b || c"), "(or (and a b) c)");
}
#[test]
fn left_associativity() {
assert_eq!(q("1 - 2 - 3"), "(- (- 1 2) 3)");
}
#[test]
fn a_dot_without_parentheses_reads_a_field() {
assert_eq!(q("user.name"), "(blue-field user :name)");
}
#[test]
fn method_call_with_args() {
assert_eq!(q("user.greet(1, 2)"), "(greet user 1 2)");
}
#[test]
fn chained_fields_read_left_to_right() {
assert_eq!(q("a.b.c"), "(blue-field (blue-field a :b) :c)");
assert_eq!(q("a.b(1).c"), "(blue-field (b a 1) :c)");
}
#[test]
fn plain_call() {
assert_eq!(q("f(1, 2)"), "(f 1 2)");
}
#[test]
fn pipeline_threads_into_first_argument() {
assert_eq!(q("x |> f"), "(f x)");
assert_eq!(q("x |> f(1)"), "(f x 1)");
assert_eq!(q("x |> f |> g"), "(g (f x))");
}
#[test]
fn pipeline_binds_looser_than_arithmetic() {
assert_eq!(q("1 + 2 |> f"), "(f (+ 1 2))");
}
#[test]
fn label_and_rocket_produce_the_same_tree_for_a_symbol_key() {
assert_eq!(q("{a: 1}"), q("{:a => 1}"));
assert_eq!(q("{a: 1}"), "(hash-map :a 1)");
}
#[test]
fn a_string_key_has_no_shorthand() {
assert_eq!(q(r#"{"k" => 1}"#), r#"(hash-map "k" 1)"#);
}
#[test]
fn list_literal() {
assert_eq!(q("[1, 2, 3]"), "(list 1 2 3)");
assert_eq!(q("[]"), "(list)");
}
#[test]
fn if_else_end() {
assert_eq!(q("if a\n 1\nelse\n 2\nend"), "(if a 1 2)");
}
#[test]
fn if_without_else() {
assert_eq!(q("if a\n 1\nend"), "(if a 1)");
}
#[test]
fn unless_is_a_negated_if() {
assert_eq!(q("unless a\n 1\nend"), "(if (not a) 1)");
}
#[test]
fn multi_statement_body_becomes_begin() {
assert_eq!(q("if a\n 1\n 2\nend"), "(if a (begin 1 2))");
}
#[test]
fn def_lowers_to_define() {
assert_eq!(
q("def add(a, b)\n a + b\nend"),
"(define (add a b) (+ a b))"
);
}
#[test]
fn def_with_no_params() {
assert_eq!(q("def zero()\n 0\nend"), "(define (zero) 0)");
}
#[test]
fn literals_lower_to_atoms() {
assert_eq!(q("42"), "42");
assert_eq!(q("true"), "#t");
assert_eq!(q(":ok"), ":ok");
assert_eq!(q(r#""hi""#), r#""hi""#);
}
#[test]
fn unary_minus_and_not() {
assert_eq!(q("-x"), "(- 0 x)");
assert_eq!(q("!x"), "(not x)");
}
#[test]
fn a_program_is_a_sequence_of_forms() {
let forms = parse_program("def f()\n 1\nend\nf()").expect("parse");
assert_eq!(forms.len(), 2);
assert_eq!(forms[1].to_string(), "(f)");
}
#[test]
fn unterminated_block_is_an_error_naming_what_was_expected() {
let e = parse_program("if a\n 1").expect_err("must fail");
assert!(e.message.contains("unterminated"), "{}", e.message);
}
#[test]
fn a_parse_error_carries_a_span_into_the_source() {
let src = "1 + )";
let e = parse_program(src).expect_err("must fail");
assert!(e.span.start < src.len(), "span {:?} outside source", e.span);
}
#[test]
fn postfix_indexing_is_refused_naming_nth() {
let e = parse_program("y = xs[0]").expect_err("must not split into two statements");
assert!(e.message.contains("`nth(0, xs)`"), "{}", e.message);
assert_eq!((e.line, e.col), (1, 7), "{e}");
}
#[test]
fn a_call_without_parentheses_is_refused_naming_the_call() {
let e = parse_program("a = f x").expect_err("must not split into two statements");
assert!(e.message.contains("`f(x)`"), "{}", e.message);
assert_eq!((e.line, e.col), (1, 7), "{e}");
}
#[test]
fn two_statements_on_one_line_are_refused_in_a_body() {
for src in [
"def f(xs)\n xs[0]\nend",
"if a\n f x\nend",
"fn(x) x 1 end",
"test \"t\"\n assert a b\nend",
"1 2",
] {
assert!(parse_program(src).is_err(), "{src:?} must not parse");
}
}
#[test]
fn an_arrow_lambda_lowers_to_the_same_tree_as_fn() {
for (arrow, block) in [
("(x) -> x * 2", "fn(x) x * 2 end"),
("(a, b) -> a + b", "fn(a, b) a + b end"),
("() -> 1", "fn() 1 end"),
("map(xs, (x) -> x + 1)", "map(xs, fn(x) x + 1 end)"),
] {
let a = parse_program(arrow).expect(arrow);
let b = parse_program(block).expect(block);
assert_eq!(format!("{a:?}"), format!("{b:?}"), "{arrow}");
}
assert_eq!(
parse_program("(x)").map(|p| p.len()).ok(),
Some(1),
"a parenthesised name is still a group"
);
}
#[test]
fn a_statement_may_end_at_a_newline_the_end_or_a_closing_keyword() {
for src in [
"fn(x) x + 1 end",
"map(fn(x) x end, xs)",
"if a\n 1\nelse\n 2\nend",
"if a 1 else 2 end",
"case x\nwhen 1 :a\nwhen 2 :b\nelse :c\nend",
"x = 1\ny = 2",
"x = 1",
] {
assert!(
parse_program(src).is_ok(),
"{src:?}: {:?}",
parse_program(src)
);
}
}
#[test]
fn then_is_refused_by_name() {
let e = parse_program("x = if c then 1 else 2 end").expect_err("then is not a keyword");
assert!(e.message.contains("`then`"), "{}", e.message);
}
#[test]
fn a_brace_block_is_refused_naming_fn() {
let e = parse_program("xs.map { |x| x }").expect_err("no brace blocks");
assert!(e.message.contains("fn(x)"), "{}", e.message);
}
#[test]
fn a_qualified_name_is_one_symbol() {
assert_eq!(q("retsu::first(xs)"), "(retsu/first xs)");
assert_eq!(q("retsu::first"), "retsu/first");
assert_eq!(q("xs |> retsu::first"), "(retsu/first xs)");
assert_eq!(q("blue::length(xs)"), "(blue/length xs)");
}
#[test]
fn whitespace_around_the_qualifier_is_accepted() {
assert_eq!(q("retsu :: first(xs)"), "(retsu/first xs)");
}
#[test]
fn a_reserved_word_after_the_qualifier_is_a_name() {
assert_eq!(q("kueri::if(a, b)"), "(kueri/if a b)");
assert_eq!(q("heni::test(x)"), "(heni/test x)");
assert_eq!(q("sabi::fn"), "sabi/fn");
}
#[test]
fn a_qualified_name_has_exactly_one_qualifier() {
for (src, says) in [
("::first(xs)", "`package::name`"),
("a::b::c", "one qualifier"),
("if::x", "reserved word"),
("retsu::1", "expected a name after `retsu::`"),
] {
let e = parse_program(src).expect_err(src);
assert!(e.message.contains(says), "{src}: {}", e.message);
}
}
#[test]
fn qualified_reads_only_the_package_name_shape() {
assert_eq!(qualified("retsu/first"), Some(("retsu", "first")));
assert_eq!(qualified("kueri/if"), Some(("kueri", "if")));
assert_eq!(qualified("nisshi/pair?"), Some(("nisshi", "pair?")));
for not in ["/", "a/", "/a", "a/b/c", "if/x", "hash-map", "a"] {
assert_eq!(qualified(not), None, "{not}");
}
assert!(is_callable_name("kueri/if"));
}
#[test]
fn a_symbol_may_end_in_a_question_or_bang() {
assert_eq!(q("[:pair?, :set!]"), "(list :pair? :set!)");
assert_eq!(q("use(\"n\", [:empty?])"), "(use \"n\" (list :empty?))");
assert_eq!(q(":a != :b"), "(not= :a :b)");
}
#[test]
fn a_parse_error_displays_line_col_and_the_source_text() {
let e = parse_program("x = 1\ny = [1, 2 y]").expect_err("must fail");
assert_eq!(e.to_string(), "2:11: expected `,` or `]`, found `y`");
assert_eq!(e.span, Span::new(16, 17), "the span stays, as data");
}
#[test]
fn no_error_message_names_a_rust_variant() {
for src in [
"f(1 2)",
"{a: 1 b}",
"def 1\nend",
"x.1",
"if a\n",
"fn(1) 2 end",
"[1 :b]",
"1 +",
"(1",
] {
let e = parse_program(src).expect_err(src);
let shown = e.to_string();
for tell in [
"Ident(", "Int(", "Op(", "Label(", "Sym(", "Str(", "LParen", "RBracket", "Eof",
"Newline", "..",
] {
assert!(!shown.contains(tell), "{src:?} -> {shown}");
}
assert!(e.line >= 1 && e.col >= 1, "{src:?} not located: {shown}");
}
}
#[test]
fn a_lex_error_is_located_too() {
let e = parse_program("x = 1\ny = 2; z = 3").expect_err("no semicolons");
assert_eq!((e.line, e.col), (2, 6), "{e}");
assert!(e.message.contains("new line"), "{e}");
}
#[test]
fn the_parser_rejects_garbage() {
assert!(parse_program("def").is_err());
assert!(parse_program("(1").is_err());
assert!(parse_program("end").is_err());
}
}