Fix todo/error parsing

This was a bit more tricky than anticipated but played out nicely in
  the end. Now we have one holistic way of parsing todos and errors
  instead of it being duplicated between when/clause and sequence. The
  error/todo parser has been moved up to the expression part rather than
  being managed when parsing sequences. Not sure what motivated that to
  begin with.

  Fixes #621.
This commit is contained in:
KtorZ 2023-07-05 20:12:57 +02:00
parent 6d7aec804c
commit ed85cb1c00
No known key found for this signature in database
GPG Key ID: 33173CB6F77F4277
15 changed files with 525 additions and 209 deletions

View File

@ -540,7 +540,7 @@ pub const DEFAULT_TODO_STR: &str = "aiken::todo";
pub const DEFAULT_ERROR_STR: &str = "aiken::error";
impl UntypedExpr {
pub fn todo(location: Span, reason: Option<Self>) -> Self {
pub fn todo(reason: Option<Self>, location: Span) -> Self {
UntypedExpr::Trace {
location,
kind: TraceKind::Todo,
@ -552,7 +552,7 @@ impl UntypedExpr {
}
}
pub fn error(location: Span, reason: Option<Self>) -> Self {
pub fn error(reason: Option<Self>, location: Span) -> Self {
UntypedExpr::Trace {
location,
kind: TraceKind::Error,

View File

@ -27,7 +27,7 @@ pub fn parser() -> impl Parser<Token, ast::UntypedDefinition, Error = ParseError
|((((public, name), (arguments, args_span)), return_annotation), body), span| {
ast::UntypedDefinition::Fn(ast::Function {
arguments,
body: body.unwrap_or_else(|| UntypedExpr::todo(span, None)),
body: body.unwrap_or_else(|| UntypedExpr::todo(None, span)),
doc: None,
location: ast::Span {
start: span.start,

View File

@ -23,7 +23,7 @@ pub fn parser() -> impl Parser<Token, ast::UntypedDefinition, Error = ParseError
.map_with_span(|(((fail, name), span_end), body), span| {
ast::UntypedDefinition::Test(ast::Function {
arguments: vec![],
body: body.unwrap_or_else(|| UntypedExpr::todo(span, None)),
body: body.unwrap_or_else(|| UntypedExpr::todo(None, span)),
doc: None,
location: span_end,
end_position: span.end - 1,

View File

@ -0,0 +1,65 @@
use chumsky::prelude::*;
use crate::{
expr::UntypedExpr,
parser::{
error::ParseError,
expr::{block::parser as block, string},
token::Token,
},
};
pub fn parser(
sequence: Recursive<'_, Token, UntypedExpr, ParseError>,
) -> impl Parser<Token, UntypedExpr, Error = ParseError> + '_ {
let message = || choice((string::hybrid(), block(sequence.clone())));
choice((
just(Token::Todo)
.ignore_then(message().or_not())
.map_with_span(UntypedExpr::todo),
just(Token::ErrorTerm)
.ignore_then(message().or_not())
.map_with_span(UntypedExpr::error),
))
}
#[cfg(test)]
mod tests {
use crate::assert_expr;
#[test]
fn error_basic() {
assert_expr!(
r#"
error @"foo"
"#
);
}
#[test]
fn error_sugar() {
assert_expr!(
r#"
error "foo"
"#
);
}
#[test]
fn todo_basic() {
assert_expr!(
r#"
todo @"foo"
"#
);
}
#[test]
fn todo_sugar() {
assert_expr!(
r#"
todo "foo"
"#
);
}
}

View File

@ -7,6 +7,7 @@ pub mod assignment;
mod block;
pub(crate) mod bytearray;
mod chained;
mod error_todo;
mod if_else;
mod int;
mod list;
@ -22,6 +23,7 @@ pub use anonymous_function::parser as anonymous_function;
pub use block::parser as block;
pub use bytearray::parser as bytearray;
pub use chained::parser as chained;
pub use error_todo::parser as error_todo;
pub use if_else::parser as if_else;
pub use int::parser as int;
pub use list::parser as list;
@ -40,159 +42,169 @@ pub fn parser(
sequence: Recursive<'_, Token, UntypedExpr, ParseError>,
) -> impl Parser<Token, UntypedExpr, Error = ParseError> + '_ {
recursive(|expression| {
let chained_debugged = chained(sequence, expression)
.then(just(Token::Question).or_not())
.map_with_span(|(value, token), location| match token {
Some(_) => UntypedExpr::TraceIfFalse {
value: Box::new(value),
location,
},
None => value,
});
// Negate
let op = choice((
just(Token::Bang).to(ast::UnOp::Not),
just(Token::Minus)
// NOTE: Prevent conflict with usage for '-' as a standalone binary op.
// This will make '-' parse when used as standalone binop in a function call.
// For example:
//
// foo(a, -, b)
//
// but it'll fail in a let-binding:
//
// let foo = -
//
// which seems acceptable.
.then_ignore(just(Token::Comma).not().rewind())
.to(ast::UnOp::Negate),
));
let unary = op
.map_with_span(|op, span| (op, span))
.repeated()
.then(chained_debugged)
.foldr(|(un_op, span), value| UntypedExpr::UnOp {
op: un_op,
location: span.union(value.location()),
value: Box::new(value),
})
.boxed();
// Product
let op = choice((
just(Token::Star).to(ast::BinOp::MultInt),
just(Token::Slash).to(ast::BinOp::DivInt),
just(Token::Percent).to(ast::BinOp::ModInt),
));
let product = unary
.clone()
.then(op.then(unary).repeated())
.foldl(|a, (op, b)| UntypedExpr::BinOp {
location: a.location().union(b.location()),
name: op,
left: Box::new(a),
right: Box::new(b),
})
.boxed();
// Sum
let op = choice((
just(Token::Plus).to(ast::BinOp::AddInt),
just(Token::Minus).to(ast::BinOp::SubInt),
));
let sum = product
.clone()
.then(op.then(product).repeated())
.foldl(|a, (op, b)| UntypedExpr::BinOp {
location: a.location().union(b.location()),
name: op,
left: Box::new(a),
right: Box::new(b),
})
.boxed();
// Comparison
let op = choice((
just(Token::EqualEqual).to(ast::BinOp::Eq),
just(Token::NotEqual).to(ast::BinOp::NotEq),
just(Token::Less).to(ast::BinOp::LtInt),
just(Token::Greater).to(ast::BinOp::GtInt),
just(Token::LessEqual).to(ast::BinOp::LtEqInt),
just(Token::GreaterEqual).to(ast::BinOp::GtEqInt),
));
let comparison = sum
.clone()
.then(op.then(sum).repeated())
.foldl(|a, (op, b)| UntypedExpr::BinOp {
location: a.location().union(b.location()),
name: op,
left: Box::new(a),
right: Box::new(b),
})
.boxed();
// Conjunction
let op = just(Token::AmperAmper).to(ast::BinOp::And);
let conjunction = comparison
.clone()
.then(op.then(comparison).repeated())
.foldl(|a, (op, b)| UntypedExpr::BinOp {
location: a.location().union(b.location()),
name: op,
left: Box::new(a),
right: Box::new(b),
})
.boxed();
// Disjunction
let op = just(Token::VbarVbar).to(ast::BinOp::Or);
let disjunction = conjunction
.clone()
.then(op.then(conjunction).repeated())
.foldl(|a, (op, b)| UntypedExpr::BinOp {
location: a.location().union(b.location()),
name: op,
left: Box::new(a),
right: Box::new(b),
})
.boxed();
// Pipeline
disjunction
.clone()
.then(
choice((just(Token::Pipe), just(Token::NewLinePipe)))
.then(disjunction)
.repeated(),
)
.foldl(|l, (pipe, r)| {
if let UntypedExpr::PipeLine {
mut expressions,
one_liner,
} = l
{
expressions.push(r);
return UntypedExpr::PipeLine {
expressions,
one_liner,
};
}
let mut expressions = Vec1::new(l);
expressions.push(r);
UntypedExpr::PipeLine {
expressions,
one_liner: pipe != Token::NewLinePipe,
}
})
choice((
error_todo(sequence.clone()),
pure_expression(sequence, expression),
))
})
}
pub fn pure_expression<'a>(
sequence: Recursive<'a, Token, UntypedExpr, ParseError>,
expression: Recursive<'a, Token, UntypedExpr, ParseError>,
) -> impl Parser<Token, UntypedExpr, Error = ParseError> + 'a {
let chained_debugged = chained(sequence, expression)
.then(just(Token::Question).or_not())
.map_with_span(|(value, token), location| match token {
Some(_) => UntypedExpr::TraceIfFalse {
value: Box::new(value),
location,
},
None => value,
});
// Negate
let op = choice((
just(Token::Bang).to(ast::UnOp::Not),
just(Token::Minus)
// NOTE: Prevent conflict with usage for '-' as a standalone binary op.
// This will make '-' parse when used as standalone binop in a function call.
// For example:
//
// foo(a, -, b)
//
// but it'll fail in a let-binding:
//
// let foo = -
//
// which seems acceptable.
.then_ignore(just(Token::Comma).not().rewind())
.to(ast::UnOp::Negate),
));
let unary = op
.map_with_span(|op, span| (op, span))
.repeated()
.then(chained_debugged)
.foldr(|(un_op, span), value| UntypedExpr::UnOp {
op: un_op,
location: span.union(value.location()),
value: Box::new(value),
})
.boxed();
// Product
let op = choice((
just(Token::Star).to(ast::BinOp::MultInt),
just(Token::Slash).to(ast::BinOp::DivInt),
just(Token::Percent).to(ast::BinOp::ModInt),
));
let product = unary
.clone()
.then(op.then(unary).repeated())
.foldl(|a, (op, b)| UntypedExpr::BinOp {
location: a.location().union(b.location()),
name: op,
left: Box::new(a),
right: Box::new(b),
})
.boxed();
// Sum
let op = choice((
just(Token::Plus).to(ast::BinOp::AddInt),
just(Token::Minus).to(ast::BinOp::SubInt),
));
let sum = product
.clone()
.then(op.then(product).repeated())
.foldl(|a, (op, b)| UntypedExpr::BinOp {
location: a.location().union(b.location()),
name: op,
left: Box::new(a),
right: Box::new(b),
})
.boxed();
// Comparison
let op = choice((
just(Token::EqualEqual).to(ast::BinOp::Eq),
just(Token::NotEqual).to(ast::BinOp::NotEq),
just(Token::Less).to(ast::BinOp::LtInt),
just(Token::Greater).to(ast::BinOp::GtInt),
just(Token::LessEqual).to(ast::BinOp::LtEqInt),
just(Token::GreaterEqual).to(ast::BinOp::GtEqInt),
));
let comparison = sum
.clone()
.then(op.then(sum).repeated())
.foldl(|a, (op, b)| UntypedExpr::BinOp {
location: a.location().union(b.location()),
name: op,
left: Box::new(a),
right: Box::new(b),
})
.boxed();
// Conjunction
let op = just(Token::AmperAmper).to(ast::BinOp::And);
let conjunction = comparison
.clone()
.then(op.then(comparison).repeated())
.foldl(|a, (op, b)| UntypedExpr::BinOp {
location: a.location().union(b.location()),
name: op,
left: Box::new(a),
right: Box::new(b),
})
.boxed();
// Disjunction
let op = just(Token::VbarVbar).to(ast::BinOp::Or);
let disjunction = conjunction
.clone()
.then(op.then(conjunction).repeated())
.foldl(|a, (op, b)| UntypedExpr::BinOp {
location: a.location().union(b.location()),
name: op,
left: Box::new(a),
right: Box::new(b),
})
.boxed();
// Pipeline
disjunction
.clone()
.then(
choice((just(Token::Pipe), just(Token::NewLinePipe)))
.then(disjunction)
.repeated(),
)
.foldl(|l, (pipe, r)| {
if let UntypedExpr::PipeLine {
mut expressions,
one_liner,
} = l
{
expressions.push(r);
return UntypedExpr::PipeLine {
expressions,
one_liner,
};
}
let mut expressions = Vec1::new(l);
expressions.push(r);
UntypedExpr::PipeLine {
expressions,
one_liner: pipe != Token::NewLinePipe,
}
})
}
#[cfg(test)]
mod tests {
use crate::assert_expr;

View File

@ -3,33 +3,27 @@ use chumsky::prelude::*;
use crate::{
ast::TraceKind,
expr::UntypedExpr,
parser::{error::ParseError, token::Token},
parser::{
error::ParseError,
expr::{block::parser as block, string},
token::Token,
},
};
pub fn parser() -> impl Parser<Token, UntypedExpr, Error = ParseError> {
recursive(|expression| {
recursive(|sequence| {
choice((
just(Token::Trace)
.ignore_then(super::parser(expression.clone()))
.then(expression.clone())
.ignore_then(choice((string::hybrid(), block(sequence.clone()))))
.then(sequence.clone())
.map_with_span(|(text, then_), span| UntypedExpr::Trace {
kind: TraceKind::Trace,
location: span,
then: Box::new(then_),
text: Box::new(super::string::flexible(text)),
text: Box::new(text),
}),
just(Token::ErrorTerm)
.ignore_then(super::parser(expression.clone()).or_not())
.map_with_span(|reason, span| {
UntypedExpr::error(span, reason.map(super::string::flexible))
}),
just(Token::Todo)
.ignore_then(super::parser(expression.clone()).or_not())
.map_with_span(|reason, span| {
UntypedExpr::todo(span, reason.map(super::string::flexible))
}),
super::parser(expression.clone())
.then(expression.repeated())
super::parser(sequence.clone())
.then(sequence.repeated())
.foldl(|current, next| current.append_in_sequence(next)),
))
})

View File

@ -0,0 +1,15 @@
---
source: crates/aiken-lang/src/parser/expr/error_todo.rs
description: "Code:\n\nerror @\"foo\"\n"
---
Trace {
kind: Error,
location: 0..12,
then: ErrorTerm {
location: 0..12,
},
text: String {
location: 6..12,
value: "foo",
},
}

View File

@ -0,0 +1,15 @@
---
source: crates/aiken-lang/src/parser/expr/error_todo.rs
description: "Code:\n\nerror \"foo\"\n"
---
Trace {
kind: Error,
location: 0..11,
then: ErrorTerm {
location: 0..11,
},
text: String {
location: 6..11,
value: "foo",
},
}

View File

@ -0,0 +1,15 @@
---
source: crates/aiken-lang/src/parser/expr/error_todo.rs
description: "Code:\n\ntodo @\"foo\"\n"
---
Trace {
kind: Todo,
location: 0..11,
then: ErrorTerm {
location: 0..11,
},
text: String {
location: 5..11,
value: "foo",
},
}

View File

@ -0,0 +1,15 @@
---
source: crates/aiken-lang/src/parser/expr/error_todo.rs
description: "Code:\n\ntodo \"foo\"\n"
---
Trace {
kind: Todo,
location: 0..10,
then: ErrorTerm {
location: 0..10,
},
text: String {
location: 5..10,
value: "foo",
},
}

View File

@ -1,9 +1,11 @@
use chumsky::prelude::*;
use crate::{
ast,
expr::UntypedExpr,
parser::{error::ParseError, literal::string::parser as string, token::Token},
parser::{
error::ParseError, literal::bytearray::utf8_string, literal::string::parser as string,
token::Token,
},
};
pub fn parser() -> impl Parser<Token, UntypedExpr, Error = ParseError> {
@ -13,23 +15,15 @@ pub fn parser() -> impl Parser<Token, UntypedExpr, Error = ParseError> {
})
}
/// Interpret bytearray string literals written as utf-8 strings, as strings.
///
/// This is mostly convenient so that todo & error works with either @"..." or plain "...".
/// In this particular context, there's actually no ambiguity about the right-hand-side, so
/// we can provide this syntactic sugar.
pub fn flexible(expr: UntypedExpr) -> UntypedExpr {
match expr {
UntypedExpr::ByteArray {
preferred_format: ast::ByteArrayFormatPreference::Utf8String,
bytes,
location,
} => UntypedExpr::String {
location,
value: String::from_utf8(bytes).unwrap(),
},
_ => expr,
}
pub fn hybrid() -> impl Parser<Token, UntypedExpr, Error = ParseError> {
choice((
string(),
utf8_string().map(|(_, bytes)| String::from_utf8(bytes).unwrap()),
))
.map_with_span(|value, span| UntypedExpr::String {
location: span,
value,
})
}
#[cfg(test)]

View File

@ -4,13 +4,13 @@ use vec1::vec1;
use crate::{
ast,
expr::UntypedExpr,
parser::{error::ParseError, expr::string::flexible, pattern, token::Token},
parser::{error::ParseError, pattern, token::Token},
};
use super::guard;
pub fn parser(
r: Recursive<'_, Token, UntypedExpr, ParseError>,
expression: Recursive<'_, Token, UntypedExpr, ParseError>,
) -> impl Parser<Token, ast::UntypedClause, Error = ParseError> + '_ {
pattern()
.then(just(Token::Vbar).ignore_then(pattern()).repeated().or_not())
@ -27,23 +27,7 @@ pub fn parser(
}),
)))
// TODO: add hint "Did you mean to wrap a multi line clause in curly braces?"
.then(choice((
r.clone(),
just(Token::Todo)
.ignore_then(
r.clone()
.then_ignore(one_of(Token::RArrow).not().rewind())
.or_not(),
)
.map_with_span(|reason, span| UntypedExpr::todo(span, reason.map(flexible))),
just(Token::ErrorTerm)
.ignore_then(
r.clone()
.then_ignore(just(Token::RArrow).not().rewind())
.or_not(),
)
.map_with_span(|reason, span| UntypedExpr::error(span, reason.map(flexible))),
)))
.then(expression)
.map_with_span(
|(((pattern, alternative_patterns_opt), guard), then), span| {
let mut patterns = vec1![pattern];
@ -57,3 +41,43 @@ pub fn parser(
},
)
}
#[cfg(test)]
mod tests {
use crate::assert_expr;
#[test]
fn todo_clause() {
assert_expr!(
r#"
when val is {
Bar1{..} -> True
Bar2{..} -> todo @"unimplemented"
}
"#
);
}
#[test]
fn error_single_clause_no_message() {
assert_expr!(
r#"
when val is {
Bar1{..} -> error
}
"#
);
}
#[test]
fn todo_double_clause_no_message() {
assert_expr!(
r#"
when val is {
Bar1{..} -> todo
Bar2{..} -> todo
}
"#
);
}
}

View File

@ -0,0 +1,40 @@
---
source: crates/aiken-lang/src/parser/expr/when/clause.rs
description: "Code:\n\nwhen val is {\n Bar1{..} -> error\n}\n"
---
When {
location: 0..35,
subject: Var {
location: 5..8,
name: "val",
},
clauses: [
UntypedClause {
location: 16..33,
patterns: [
Constructor {
is_record: true,
location: 16..24,
name: "Bar1",
arguments: [],
module: None,
constructor: (),
with_spread: true,
tipo: (),
},
],
guard: None,
then: Trace {
kind: Error,
location: 28..33,
then: ErrorTerm {
location: 28..33,
},
text: String {
location: 28..33,
value: "aiken::error",
},
},
},
],
}

View File

@ -0,0 +1,60 @@
---
source: crates/aiken-lang/src/parser/expr/when/clause.rs
description: "Code:\n\nwhen val is {\n Bar1{..} -> True\n Bar2{..} -> todo @\"unimplemented\"\n}\n"
---
When {
location: 0..70,
subject: Var {
location: 5..8,
name: "val",
},
clauses: [
UntypedClause {
location: 16..32,
patterns: [
Constructor {
is_record: true,
location: 16..24,
name: "Bar1",
arguments: [],
module: None,
constructor: (),
with_spread: true,
tipo: (),
},
],
guard: None,
then: Var {
location: 28..32,
name: "True",
},
},
UntypedClause {
location: 35..68,
patterns: [
Constructor {
is_record: true,
location: 35..43,
name: "Bar2",
arguments: [],
module: None,
constructor: (),
with_spread: true,
tipo: (),
},
],
guard: None,
then: Trace {
kind: Todo,
location: 47..68,
then: ErrorTerm {
location: 47..68,
},
text: String {
location: 52..68,
value: "unimplemented",
},
},
},
],
}

View File

@ -0,0 +1,67 @@
---
source: crates/aiken-lang/src/parser/expr/when/clause.rs
description: "Code:\n\nwhen val is {\n Bar1{..} -> todo\n Bar2{..} -> todo\n}\n"
---
When {
location: 0..53,
subject: Var {
location: 5..8,
name: "val",
},
clauses: [
UntypedClause {
location: 16..32,
patterns: [
Constructor {
is_record: true,
location: 16..24,
name: "Bar1",
arguments: [],
module: None,
constructor: (),
with_spread: true,
tipo: (),
},
],
guard: None,
then: Trace {
kind: Todo,
location: 28..32,
then: ErrorTerm {
location: 28..32,
},
text: String {
location: 28..32,
value: "aiken::todo",
},
},
},
UntypedClause {
location: 35..51,
patterns: [
Constructor {
is_record: true,
location: 35..43,
name: "Bar2",
arguments: [],
module: None,
constructor: (),
with_spread: true,
tipo: (),
},
],
guard: None,
then: Trace {
kind: Todo,
location: 47..51,
then: ErrorTerm {
location: 47..51,
},
text: String {
location: 47..51,
value: "aiken::todo",
},
},
},
],
}