use tatara_lisp::{Atom, Sexp};
use crate::lex::{lex, Span, Token, TokenKind};
#[derive(Clone, Debug, PartialEq)]
pub struct ParseError {
pub message: String,
pub span: Span,
}
impl std::fmt::Display for ParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{} at {}..{}",
self.message, self.span.start, self.span.end
)
}
}
impl std::error::Error for ParseError {}
impl From<crate::lex::LexError> for ParseError {
fn from(e: crate::lex::LexError) -> Self {
Self {
message: e.message,
span: e.span,
}
}
}
pub fn parse_program(src: &str) -> Result<Vec<Sexp>, ParseError> {
Ok(parse_program_spanned(src)?
.into_iter()
.map(|(form, _)| form)
.collect())
}
pub fn parse_program_spanned(src: &str) -> Result<Vec<(Sexp, Span)>, ParseError> {
let toks: Vec<Token> = lex(src)?
.into_iter()
.filter(|t| !matches!(t.kind, TokenKind::Comment(_)))
.collect();
let mut p = Parser {
toks,
pos: 0,
depth: 0,
};
p.program_spanned()
}
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 {
message: format!("expected exactly one expression, found {n}"),
span: Span::new(0, src.len()),
}),
}
}
#[derive(Clone, Copy, Debug)]
pub struct Infix {
pub op: &'static str,
pub power: (u8, u8),
pub callee: &'static str,
}
pub const INFIX: &[Infix] = &[
Infix {
op: "||",
power: (1, 2),
callee: "or",
},
Infix {
op: "&&",
power: (3, 4),
callee: "and",
},
Infix {
op: "==",
power: (5, 6),
callee: "equal?",
},
Infix {
op: "!=",
power: (5, 6),
callee: "not=",
},
Infix {
op: "<",
power: (5, 6),
callee: "<",
},
Infix {
op: "<=",
power: (5, 6),
callee: "<=",
},
Infix {
op: ">",
power: (5, 6),
callee: ">",
},
Infix {
op: ">=",
power: (5, 6),
callee: ">=",
},
Infix {
op: "+",
power: (7, 8),
callee: "+",
},
Infix {
op: "-",
power: (7, 8),
callee: "-",
},
Infix {
op: "*",
power: (9, 10),
callee: "*",
},
Infix {
op: "/",
power: (9, 10),
callee: "/",
},
Infix {
op: "%",
power: (9, 10),
callee: "mod",
},
];
pub const LOWERED_ASSERT: &str = "blue-assert";
pub const LOWERED_MAP: &str = "hash-map";
pub const LOWERED_CONCAT: &str = "concat";
pub const SURFACE_KEYWORDS: &[&str] = &[
"if",
"unless",
"def",
"defmacro",
"quote",
"unquote",
"unquote_splice",
"test",
"assert",
"fn",
"case",
];
fn is_reserved_word(name: &str) -> bool {
SURFACE_KEYWORDS.contains(&name)
|| matches!(name, "do" | "end" | "else" | "true" | "false" | "nil")
}
fn infix(op: &str) -> Option<&'static Infix> {
INFIX.iter().find(|i| i.op == op)
}
const PIPE_POWER: (u8, u8) = (0, 1);
struct Parser {
toks: Vec<Token>,
pos: usize,
depth: usize,
}
pub const MAX_EXPR_DEPTH: usize = 256;
impl Parser {
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<(), ParseError> {
if self.eat(k) {
Ok(())
} else {
Err(self.error(format!("expected {what}, found {:?}", self.peek())))
}
}
fn error(&self, message: impl Into<String>) -> ParseError {
ParseError {
message: message.into(),
span: 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 program_spanned(&mut self) -> Result<Vec<(Sexp, Span)>, ParseError> {
let mut out = Vec::new();
loop {
self.skip_newlines();
if matches!(self.peek(), TokenKind::Eof) {
break;
}
let start = self.peek_span().start;
let form = self.statement()?;
let end = self
.toks
.get(self.pos.saturating_sub(1))
.map_or(start, |t| t.span.end);
out.push((form, Span::new(start, end)));
}
Ok(out)
}
fn statement(&mut self) -> Result<Sexp, ParseError> {
if self.depth >= MAX_EXPR_DEPTH {
return Err(self.error(format!(
"statement nests deeper than {MAX_EXPR_DEPTH}; 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<Sexp, ParseError> {
if let TokenKind::Ident(name) = self.peek().clone() {
if self.peek_at(1) == "=" && !is_reserved_word(&name) {
self.bump(); self.bump(); self.skip_newlines();
let value = self.expr(0)?;
return Ok(Sexp::List(vec![sym("define"), sym(&name), value]));
}
}
self.expr(0)
}
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<Sexp, ParseError> {
if self.depth >= MAX_EXPR_DEPTH {
return Err(self.error(format!(
"expression nests deeper than {MAX_EXPR_DEPTH}; 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
}
fn expr_inner(&mut self, min_bp: u8) -> Result<Sexp, ParseError> {
let mut lhs = self.prefix()?;
loop {
match self.peek() {
TokenKind::Dot => {
self.bump();
lhs = self.finish_send(lhs)?;
continue;
}
TokenKind::LParen => {
let args = self.paren_args()?;
let mut list = vec![lhs];
list.extend(args);
lhs = Sexp::List(list);
continue;
}
_ => {}
}
let (callee, (lbp, rbp)) = match self.peek() {
TokenKind::Pipe => (None, PIPE_POWER),
TokenKind::Op(o) => match infix(o) {
Some(i) => (Some(i.callee), i.power),
None => break,
},
_ => break,
};
if lbp < min_bp {
break;
}
self.bump();
self.skip_newlines();
let rhs = self.expr(rbp)?;
lhs = if callee.is_none() {
match rhs {
Sexp::List(mut items) if !items.is_empty() => {
items.insert(1, lhs);
Sexp::List(items)
}
callee => Sexp::List(vec![callee, lhs]),
}
} else {
Sexp::List(vec![sym(callee.unwrap()), lhs, rhs])
};
}
Ok(lhs)
}
fn prefix(&mut self) -> Result<Sexp, ParseError> {
let span = self.peek_span();
match self.bump() {
TokenKind::Int(v) => Ok(Sexp::Atom(Atom::Int(v))),
TokenKind::Float(v) => Ok(Sexp::Atom(Atom::Float(v))),
TokenKind::Str(s) => Ok(Sexp::Atom(Atom::Str(s))),
TokenKind::InterpolatedStr { parts, exprs } => {
let mut acc = Sexp::Atom(Atom::Str(parts[0].clone()));
for (i, raw) in exprs.iter().enumerate() {
let inner = parse_expr(raw).map_err(|e| ParseError {
message: format!("in interpolation `#{{{raw}}}`: {}", e.message),
span,
})?;
acc = Sexp::List(vec![sym(LOWERED_CONCAT), acc, inner]);
acc = Sexp::List(vec![
sym(LOWERED_CONCAT),
acc,
Sexp::Atom(Atom::Str(parts[i + 1].clone())),
]);
}
Ok(acc)
}
TokenKind::Sym(s) => Ok(Sexp::Atom(Atom::Keyword(s))),
TokenKind::True => Ok(Sexp::Atom(Atom::Bool(true))),
TokenKind::False => Ok(Sexp::Atom(Atom::Bool(false))),
TokenKind::Nil => Ok(Sexp::Nil),
TokenKind::Op(o) if o == "-" => {
let rhs = self.expr(11)?; Ok(Sexp::List(vec![sym("-"), Sexp::Atom(Atom::Int(0)), rhs]))
}
TokenKind::Op(o) if o == "!" => {
let rhs = self.expr(11)?;
Ok(Sexp::List(vec![sym("not"), rhs]))
}
TokenKind::LParen => {
self.skip_newlines();
let inner = self.expr(0)?;
self.skip_newlines();
self.expect(&TokenKind::RParen, "`)`")?;
Ok(inner)
}
TokenKind::LBracket => self.list_literal(),
TokenKind::LBrace => self.map_literal(),
TokenKind::Ident(name) => match name.as_str() {
"if" => self.if_form(false),
"unless" => self.if_form(true),
"def" => self.def_form(),
"defmacro" => self.defmacro_form(),
"case" => self.case_form(),
"fn" => self.lambda_form(),
"test" => self.test_form(),
"assert" => self.assert_form(),
"quote" => self.quote_form(),
"unquote" => self.unquote_form(false),
"unquote_splice" => self.unquote_form(true),
"do" => Err(ParseError {
message: "`do` without a preceding call".into(),
span,
}),
"end" => Err(ParseError {
message: "unexpected `end`".into(),
span,
}),
_ => Ok(sym(&name)),
},
other => Err(ParseError {
message: format!("expected an expression, found {other:?}"),
span,
}),
}
}
fn finish_send(&mut self, recv: Sexp) -> Result<Sexp, ParseError> {
let name = match self.bump() {
TokenKind::Ident(n) => n,
other => {
return Err(self.error(format!("expected a method name after `.`, found {other:?}")))
}
};
let mut list = vec![sym(&name), recv];
if self.at(&TokenKind::LParen) {
list.extend(self.paren_args()?);
}
Ok(Sexp::List(list))
}
fn paren_args(&mut self) -> Result<Vec<Sexp>, ParseError> {
self.expect(&TokenKind::LParen, "`(`")?;
let mut args = Vec::new();
self.skip_newlines();
if self.eat(&TokenKind::RParen) {
return Ok(args);
}
loop {
self.skip_newlines();
args.push(self.expr(0)?);
self.skip_newlines();
if self.eat(&TokenKind::Comma) {
continue;
}
self.expect(&TokenKind::RParen, "`,` or `)`")?;
break;
}
Ok(args)
}
fn list_literal(&mut self) -> Result<Sexp, ParseError> {
let mut items = vec![sym("list")];
self.skip_newlines();
if self.eat(&TokenKind::RBracket) {
return Ok(Sexp::List(items));
}
loop {
self.skip_newlines();
items.push(self.expr(0)?);
self.skip_newlines();
if self.eat(&TokenKind::Comma) {
continue;
}
self.expect(&TokenKind::RBracket, "`,` or `]`")?;
break;
}
Ok(Sexp::List(items))
}
fn map_literal(&mut self) -> Result<Sexp, ParseError> {
let mut items = vec![sym(LOWERED_MAP)];
self.skip_newlines();
if self.eat(&TokenKind::RBrace) {
return Ok(Sexp::List(items));
}
loop {
self.skip_newlines();
match self.peek().clone() {
TokenKind::Label(name) => {
self.bump();
self.skip_newlines();
items.push(Sexp::Atom(Atom::Keyword(name)));
items.push(self.expr(0)?);
}
_ => {
let k = self.expr(0)?;
self.skip_newlines();
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) {
continue;
}
self.expect(&TokenKind::RBrace, "`,` or `}`")?;
break;
}
Ok(Sexp::List(items))
}
fn if_form(&mut self, negate: bool) -> Result<Sexp, ParseError> {
let cond = self.expr(0)?;
let cond = if negate {
Sexp::List(vec![sym("not"), cond])
} else {
cond
};
let then = self.body(&["else", "end"])?;
let els = if self.at_ident("else") {
self.bump();
let e = self.body(&["end"])?;
self.expect_ident("end")?;
Some(e)
} else {
self.expect_ident("end")?;
None
};
let mut out = vec![sym("if"), cond, then];
if let Some(e) = els {
out.push(e);
}
Ok(Sexp::List(out))
}
fn case_form(&mut self) -> Result<Sexp, ParseError> {
let subject = self.expr(0)?;
self.skip_newlines();
let subject_var = "case-subject";
let mut arms: Vec<Sexp> = Vec::new();
let mut otherwise: Option<Sexp> = None;
loop {
self.skip_newlines();
if self.at_ident("end") {
break;
}
if self.eat_ident("else") {
otherwise = Some(self.body(&["end"])?);
continue;
}
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"])?;
arms.push(Sexp::List(vec![
Sexp::List(vec![sym("equal?"), sym(subject_var), pattern]),
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("cond")];
cond.extend(arms);
cond.push(Sexp::List(vec![
sym("else"),
otherwise.unwrap_or(Sexp::Nil),
]));
Ok(Sexp::List(vec![
sym("let"),
Sexp::List(vec![Sexp::List(vec![sym(subject_var), subject])]),
Sexp::List(cond),
]))
}
fn lambda_form(&mut self) -> Result<Sexp, ParseError> {
let mut params: Vec<String> = Vec::new();
if self.at(&TokenKind::LParen) {
self.bump();
self.skip_newlines();
if !self.eat(&TokenKind::RParen) {
loop {
self.skip_newlines();
match self.bump() {
TokenKind::Ident(p) => params.push(p),
other => {
return Err(
self.error(format!("expected a parameter name, found {other:?}"))
)
}
}
self.skip_newlines();
if self.eat(&TokenKind::Comma) {
continue;
}
self.expect(&TokenKind::RParen, "`,` or `)`")?;
break;
}
}
}
let body = self.body(&["end"])?;
self.expect_ident("end")?;
Ok(Sexp::List(vec![
sym("lambda"),
Sexp::List(params.iter().map(|p| sym(p)).collect()),
body,
]))
}
fn test_form(&mut self) -> Result<Sexp, ParseError> {
let name = match self.bump() {
TokenKind::Str(s) => s,
other => {
return Err(self.error(format!(
"expected a string name after `test`, found {other:?}"
)))
}
};
let body = self.body(&["end"])?;
self.expect_ident("end")?;
Ok(Sexp::List(vec![
sym("deftest"),
Sexp::Atom(Atom::Str(name)),
body,
]))
}
fn assert_form(&mut self) -> Result<Sexp, ParseError> {
let e = self.expr(0)?;
Ok(Sexp::List(vec![
sym(LOWERED_ASSERT),
Sexp::Quote(Box::new(e.clone())),
e,
]))
}
fn defmacro_form(&mut self) -> Result<Sexp, ParseError> {
let name = match self.bump() {
TokenKind::Ident(n) => n,
other => {
return Err(self.error(format!("expected a name after `defmacro`, found {other:?}")))
}
};
let mut params: Vec<String> = Vec::new();
if self.at(&TokenKind::LParen) {
self.bump();
self.skip_newlines();
if !self.eat(&TokenKind::RParen) {
loop {
self.skip_newlines();
match self.bump() {
TokenKind::Ident(p) => params.push(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(
self.error(format!("expected a parameter name, found {other:?}"))
)
}
}
self.skip_newlines();
if self.eat(&TokenKind::Comma) {
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 body = self.body(&["end"])?;
self.expect_ident("end")?;
Ok(Sexp::List(vec![
sym("defmacro"),
sym(&name),
Sexp::List(params.iter().map(|p| sym(p)).collect()),
body,
]))
}
fn quote_form(&mut self) -> Result<Sexp, ParseError> {
let body = self.body(&["end"])?;
self.expect_ident("end")?;
Ok(Sexp::Quasiquote(Box::new(body)))
}
fn unquote_form(&mut self, splice: bool) -> Result<Sexp, ParseError> {
self.expect(&TokenKind::LParen, "`(` after unquote")?;
self.skip_newlines();
let inner = self.expr(0)?;
self.skip_newlines();
self.expect(&TokenKind::RParen, "`)`")?;
Ok(if splice {
Sexp::UnquoteSplice(Box::new(inner))
} else {
Sexp::Unquote(Box::new(inner))
})
}
fn def_form(&mut self) -> Result<Sexp, ParseError> {
let name = match self.bump() {
TokenKind::Ident(n) => n,
other => {
return Err(self.error(format!("expected a name after `def`, found {other:?}")))
}
};
let mut params: Vec<(String, Option<Sexp>)> = Vec::new();
if self.at(&TokenKind::LParen) {
self.bump();
self.skip_newlines();
if !self.eat(&TokenKind::RParen) {
loop {
self.skip_newlines();
match self.bump() {
TokenKind::Ident(p) => params.push((p, None)),
TokenKind::Label(p) => {
self.skip_newlines();
let ty = self.type_expr()?;
params.push((p, Some(ty)));
}
other => {
return Err(
self.error(format!("expected a parameter name, found {other:?}"))
)
}
}
self.skip_newlines();
if self.eat(&TokenKind::Comma) {
continue;
}
self.expect(&TokenKind::RParen, "`,` or `)`")?;
break;
}
}
}
let ret = if matches!(self.peek(), TokenKind::Op(o) if o == "->") {
self.bump();
self.skip_newlines();
Some(self.type_expr()?)
} else {
None
};
let body = self.body(&["end"])?;
self.expect_ident("end")?;
let annotated = ret.is_some() || params.iter().any(|(_, t)| t.is_some());
if !annotated {
let mut sig = vec![sym(&name)];
sig.extend(params.into_iter().map(|(p, _)| sym(&p)));
return Ok(Sexp::List(vec![sym("define"), Sexp::List(sig), body]));
}
let mut sig = vec![sym(&name)];
for (p, t) in params {
let ty = t.unwrap_or_else(|| sym("dyn"));
sig.push(Sexp::List(vec![sym(&p), ty]));
}
Ok(Sexp::List(vec![
sym("define-typed"),
Sexp::List(sig),
ret.unwrap_or_else(|| sym("dyn")),
body,
]))
}
fn type_expr(&mut self) -> Result<Sexp, ParseError> {
let name = match self.bump() {
TokenKind::Ident(n) => n,
other => return Err(self.error(format!("expected a type name, found {other:?}"))),
};
if self.at(&TokenKind::LParen) {
let args = self.paren_args()?;
let mut list = vec![sym(&name)];
list.extend(args);
return Ok(Sexp::List(list));
}
Ok(sym(&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<(), ParseError> {
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<Sexp, ParseError> {
let mut forms = Vec::new();
loop {
self.skip_newlines();
if matches!(self.peek(), TokenKind::Eof) {
return Err(self.error(format!(
"unterminated block: expected one of {terminators:?}"
)));
}
if terminators.iter().any(|t| self.at_ident(t)) {
break;
}
forms.push(self.statement()?);
}
Ok(match forms.len() {
0 => Sexp::Nil,
1 => forms.into_iter().next().expect("checked len"),
_ => {
let mut list = vec![sym("begin")];
list.extend(forms);
Sexp::List(list)
}
})
}
}
fn sym(s: &str) -> Sexp {
Sexp::Atom(Atom::Symbol(s.to_string()))
}
#[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 method_call_without_parens_is_a_send() {
assert_eq!(q("user.name"), "(name user)");
}
#[test]
fn method_call_with_args() {
assert_eq!(q("user.greet(1, 2)"), "(greet user 1 2)");
}
#[test]
fn chained_sends_read_left_to_right() {
assert_eq!(q("a.b.c"), "(c (b a))");
}
#[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 the_parser_rejects_garbage() {
assert!(parse_program("def").is_err());
assert!(parse_program("(1").is_err());
assert!(parse_program("end").is_err());
}
}