diff --git a/crates/aiken-lang/src/expr.rs b/crates/aiken-lang/src/expr.rs index 32c14c29..05962a02 100644 --- a/crates/aiken-lang/src/expr.rs +++ b/crates/aiken-lang/src/expr.rs @@ -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 { + pub fn todo(reason: Option, location: Span) -> Self { UntypedExpr::Trace { location, kind: TraceKind::Todo, @@ -552,7 +552,7 @@ impl UntypedExpr { } } - pub fn error(location: Span, reason: Option) -> Self { + pub fn error(reason: Option, location: Span) -> Self { UntypedExpr::Trace { location, kind: TraceKind::Error, diff --git a/crates/aiken-lang/src/parser/definition/function.rs b/crates/aiken-lang/src/parser/definition/function.rs index e0a615b3..6a3573a4 100644 --- a/crates/aiken-lang/src/parser/definition/function.rs +++ b/crates/aiken-lang/src/parser/definition/function.rs @@ -27,7 +27,7 @@ pub fn parser() -> impl Parser impl Parser, +) -> impl Parser + '_ { + 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" + "# + ); + } +} diff --git a/crates/aiken-lang/src/parser/expr/mod.rs b/crates/aiken-lang/src/parser/expr/mod.rs index 96d361f2..4f651c0d 100644 --- a/crates/aiken-lang/src/parser/expr/mod.rs +++ b/crates/aiken-lang/src/parser/expr/mod.rs @@ -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 + '_ { 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 + '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; diff --git a/crates/aiken-lang/src/parser/expr/sequence.rs b/crates/aiken-lang/src/parser/expr/sequence.rs index 68e5c075..199d9902 100644 --- a/crates/aiken-lang/src/parser/expr/sequence.rs +++ b/crates/aiken-lang/src/parser/expr/sequence.rs @@ -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 { - 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)), )) }) diff --git a/crates/aiken-lang/src/parser/expr/snapshots/error_basic.snap b/crates/aiken-lang/src/parser/expr/snapshots/error_basic.snap new file mode 100644 index 00000000..c7569fae --- /dev/null +++ b/crates/aiken-lang/src/parser/expr/snapshots/error_basic.snap @@ -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", + }, +} diff --git a/crates/aiken-lang/src/parser/expr/snapshots/error_sugar.snap b/crates/aiken-lang/src/parser/expr/snapshots/error_sugar.snap new file mode 100644 index 00000000..a1ee2c84 --- /dev/null +++ b/crates/aiken-lang/src/parser/expr/snapshots/error_sugar.snap @@ -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", + }, +} diff --git a/crates/aiken-lang/src/parser/expr/snapshots/todo_basic.snap b/crates/aiken-lang/src/parser/expr/snapshots/todo_basic.snap new file mode 100644 index 00000000..92394c5d --- /dev/null +++ b/crates/aiken-lang/src/parser/expr/snapshots/todo_basic.snap @@ -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", + }, +} diff --git a/crates/aiken-lang/src/parser/expr/snapshots/todo_sugar.snap b/crates/aiken-lang/src/parser/expr/snapshots/todo_sugar.snap new file mode 100644 index 00000000..ce2e4877 --- /dev/null +++ b/crates/aiken-lang/src/parser/expr/snapshots/todo_sugar.snap @@ -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", + }, +} diff --git a/crates/aiken-lang/src/parser/expr/string.rs b/crates/aiken-lang/src/parser/expr/string.rs index f1361d64..ed8f4f75 100644 --- a/crates/aiken-lang/src/parser/expr/string.rs +++ b/crates/aiken-lang/src/parser/expr/string.rs @@ -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 { @@ -13,23 +15,15 @@ pub fn parser() -> impl Parser { }) } -/// 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 { + choice(( + string(), + utf8_string().map(|(_, bytes)| String::from_utf8(bytes).unwrap()), + )) + .map_with_span(|value, span| UntypedExpr::String { + location: span, + value, + }) } #[cfg(test)] diff --git a/crates/aiken-lang/src/parser/expr/when/clause.rs b/crates/aiken-lang/src/parser/expr/when/clause.rs index 474a470f..fc2df9df 100644 --- a/crates/aiken-lang/src/parser/expr/when/clause.rs +++ b/crates/aiken-lang/src/parser/expr/when/clause.rs @@ -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 + '_ { 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 + } + "# + ); + } +} diff --git a/crates/aiken-lang/src/parser/expr/when/snapshots/error_single_clause_no_message.snap b/crates/aiken-lang/src/parser/expr/when/snapshots/error_single_clause_no_message.snap new file mode 100644 index 00000000..92b6171f --- /dev/null +++ b/crates/aiken-lang/src/parser/expr/when/snapshots/error_single_clause_no_message.snap @@ -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", + }, + }, + }, + ], +} diff --git a/crates/aiken-lang/src/parser/expr/when/snapshots/todo_clause.snap b/crates/aiken-lang/src/parser/expr/when/snapshots/todo_clause.snap new file mode 100644 index 00000000..26c65eea --- /dev/null +++ b/crates/aiken-lang/src/parser/expr/when/snapshots/todo_clause.snap @@ -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", + }, + }, + }, + ], +} diff --git a/crates/aiken-lang/src/parser/expr/when/snapshots/todo_double_clause_no_message.snap b/crates/aiken-lang/src/parser/expr/when/snapshots/todo_double_clause_no_message.snap new file mode 100644 index 00000000..b708e48a --- /dev/null +++ b/crates/aiken-lang/src/parser/expr/when/snapshots/todo_double_clause_no_message.snap @@ -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", + }, + }, + }, + ], +}