From 5d7585cc0584a8c292f582926496acea03e70755 Mon Sep 17 00:00:00 2001 From: KtorZ Date: Sat, 21 Jan 2023 17:36:38 +0100 Subject: [PATCH] Implement parser for when clause guard With pretty parse errors on failures. The type-checker was already implemented for those, so it now only requires some work in the code generation. Fixes #297. --- crates/aiken-lang/src/ast.rs | 34 ++-- crates/aiken-lang/src/format.rs | 3 + crates/aiken-lang/src/parser.rs | 142 ++++++++++++-- crates/aiken-lang/src/parser/error.rs | 34 +++- crates/aiken-lang/src/tests/parser.rs | 264 ++++++++++++++++++++++++++ crates/aiken-lang/src/tipo/expr.rs | 44 +---- 6 files changed, 456 insertions(+), 65 deletions(-) diff --git a/crates/aiken-lang/src/ast.rs b/crates/aiken-lang/src/ast.rs index 2b6f0c9a..f72bc609 100644 --- a/crates/aiken-lang/src/ast.rs +++ b/crates/aiken-lang/src/ast.rs @@ -807,6 +807,11 @@ pub type TypedClauseGuard = ClauseGuard, String>; #[derive(Debug, Clone, PartialEq)] pub enum ClauseGuard { + Not { + location: Span, + value: Box, + }, + Equals { location: Span, left: Box, @@ -861,12 +866,6 @@ pub enum ClauseGuard { name: String, }, - // TupleIndex { - // location: Span, - // index: u64, - // tipo: Type, - // tuple: Box, - // }, Constant(Constant), } @@ -874,10 +873,10 @@ impl ClauseGuard { pub fn location(&self) -> Span { match self { ClauseGuard::Constant(constant) => constant.location(), - ClauseGuard::Or { location, .. } + ClauseGuard::Not { location, .. } + | ClauseGuard::Or { location, .. } | ClauseGuard::And { location, .. } | ClauseGuard::Var { location, .. } - // | ClauseGuard::TupleIndex { location, .. } | ClauseGuard::Equals { location, .. } | ClauseGuard::NotEquals { location, .. } | ClauseGuard::GtInt { location, .. } @@ -890,17 +889,15 @@ impl ClauseGuard { pub fn precedence(&self) -> u8 { // Ensure that this matches the other precedence function for guards match self { - ClauseGuard::Or { .. } => 1, - ClauseGuard::And { .. } => 2, - - ClauseGuard::Equals { .. } | ClauseGuard::NotEquals { .. } => 3, - + ClauseGuard::Not { .. } => 1, + ClauseGuard::Or { .. } => 2, + ClauseGuard::And { .. } => 3, + ClauseGuard::Equals { .. } | ClauseGuard::NotEquals { .. } => 4, ClauseGuard::GtInt { .. } | ClauseGuard::GtEqInt { .. } | ClauseGuard::LtInt { .. } - | ClauseGuard::LtEqInt { .. } => 4, - - ClauseGuard::Constant(_) | ClauseGuard::Var { .. } => 5, + | ClauseGuard::LtEqInt { .. } => 5, + ClauseGuard::Constant(_) | ClauseGuard::Var { .. } => 6, } } } @@ -909,10 +906,9 @@ impl TypedClauseGuard { pub fn tipo(&self) -> Arc { match self { ClauseGuard::Var { tipo, .. } => tipo.clone(), - // ClauseGuard::TupleIndex { type_, .. } => type_.clone(), ClauseGuard::Constant(constant) => constant.tipo(), - - ClauseGuard::Or { .. } + ClauseGuard::Not { .. } + | ClauseGuard::Or { .. } | ClauseGuard::And { .. } | ClauseGuard::Equals { .. } | ClauseGuard::NotEquals { .. } diff --git a/crates/aiken-lang/src/format.rs b/crates/aiken-lang/src/format.rs index f8f7dd78..2fc87ab4 100644 --- a/crates/aiken-lang/src/format.rs +++ b/crates/aiken-lang/src/format.rs @@ -1539,6 +1539,9 @@ impl<'comments> Formatter<'comments> { fn clause_guard<'a>(&mut self, clause_guard: &'a UntypedClauseGuard) -> Document<'a> { match clause_guard { + ClauseGuard::Not { value, .. } => { + docvec!["!", self.clause_guard(value)] + } ClauseGuard::And { left, right, .. } => { self.clause_guard_bin_op(" && ", clause_guard.precedence(), left, right) } diff --git a/crates/aiken-lang/src/parser.rs b/crates/aiken-lang/src/parser.rs index 312e2295..201e61a7 100644 --- a/crates/aiken-lang/src/parser.rs +++ b/crates/aiken-lang/src/parser.rs @@ -989,9 +989,6 @@ pub fn expr_parser( }, ); - // TODO: do guards later - // let when_clause_guard_parser = just(Token::If); - let when_clause_parser = pattern_parser() .separated_by(just(Token::Comma)) .at_least(1) @@ -1005,20 +1002,29 @@ pub fn expr_parser( .repeated() .or_not(), ) - // TODO: do guards later - // .then(when_clause_guard_parser) + .then(choice(( + just(Token::If) + .ignore_then(when_clause_guard_parser()) + .or_not() + .then_ignore(just(Token::RArrow)), + just(Token::If) + .ignore_then(take_until(just(Token::RArrow))) + .validate(|_value, span, emit| { + emit(ParseError::invalid_when_clause_guard(span)); + None + }), + ))) // TODO: add hint "Did you mean to wrap a multi line clause in curly braces?" - .then_ignore(just(Token::RArrow)) .then(r.clone()) - .map_with_span(|((patterns, alternative_patterns_opt), then), span| { - ast::UntypedClause { + .map_with_span( + |(((patterns, alternative_patterns_opt), guard), then), span| ast::UntypedClause { location: span, pattern: patterns, alternative_patterns: alternative_patterns_opt.unwrap_or_default(), - guard: None, + guard, then, - } - }); + }, + ); let when_parser = just(Token::When) // TODO: If subject is empty we should return ParseErrorType::ExpectedExpr, @@ -1373,6 +1379,120 @@ pub fn expr_parser( }) } +pub fn when_clause_guard_parser() -> impl Parser, Error = ParseError> +{ + recursive(|r| { + let var_parser = select! { + Token::Name { name } => name, + Token::UpName { name } => name, + } + .map_with_span(|name, span| ast::ClauseGuard::Var { + name, + tipo: (), + location: span, + }); + + let constant_parser = constant_value_parser().map(ast::ClauseGuard::Constant); + + let block_parser = r + .clone() + .delimited_by(just(Token::LeftParen), just(Token::RightParen)); + + let leaf_parser = choice((var_parser, constant_parser, block_parser)).boxed(); + + let unary_op = just(Token::Bang); + + let unary = unary_op + .map_with_span(|op, span| (op, span)) + .repeated() + .then(leaf_parser) + .foldr(|(_, span), value| ast::ClauseGuard::Not { + location: span.union(value.location()), + value: Box::new(value), + }) + .boxed(); + + let comparison_op = choice(( + just(Token::EqualEqual).to(BinOp::Eq), + just(Token::NotEqual).to(BinOp::NotEq), + just(Token::Less).to(BinOp::LtInt), + just(Token::Greater).to(BinOp::GtInt), + just(Token::LessEqual).to(BinOp::LtEqInt), + just(Token::GreaterEqual).to(BinOp::GtEqInt), + )); + + let comparison = unary + .clone() + .then(comparison_op.then(unary).repeated()) + .foldl(|left, (op, right)| { + let location = left.location().union(right.location()); + let left = Box::new(left); + let right = Box::new(right); + match op { + BinOp::Eq => ast::ClauseGuard::Equals { + location, + left, + right, + }, + BinOp::NotEq => ast::ClauseGuard::NotEquals { + location, + left, + right, + }, + BinOp::LtInt => ast::ClauseGuard::LtInt { + location, + left, + right, + }, + BinOp::GtInt => ast::ClauseGuard::GtInt { + location, + left, + right, + }, + BinOp::LtEqInt => ast::ClauseGuard::LtEqInt { + location, + left, + right, + }, + BinOp::GtEqInt => ast::ClauseGuard::GtEqInt { + location, + left, + right, + }, + _ => unreachable!(), + } + }) + .boxed(); + + let logical_op = choice(( + just(Token::AmperAmper).to(BinOp::And), + just(Token::VbarVbar).to(BinOp::Or), + )); + + comparison + .clone() + .then(logical_op.then(comparison).repeated()) + .foldl(|left, (op, right)| { + let location = left.location().union(right.location()); + let left = Box::new(left); + let right = Box::new(right); + match op { + BinOp::And => ast::ClauseGuard::And { + location, + left, + right, + }, + BinOp::Or => ast::ClauseGuard::Or { + location, + left, + right, + }, + _ => unreachable!(), + } + }) + }) +} + pub fn type_parser() -> impl Parser { recursive(|r| { choice(( diff --git a/crates/aiken-lang/src/parser/error.rs b/crates/aiken-lang/src/parser/error.rs index 08acd784..4b2d1055 100644 --- a/crates/aiken-lang/src/parser/error.rs +++ b/crates/aiken-lang/src/parser/error.rs @@ -36,6 +36,16 @@ impl ParseError { } } + pub fn invalid_when_clause_guard(span: Span) -> Self { + Self { + kind: ErrorKind::InvalidWhenClause, + span, + while_parsing: None, + expected: HashSet::new(), + label: Some("invalid clause guard"), + } + } + pub fn malformed_base16_string_literal(span: Span) -> Self { Self { kind: ErrorKind::MalformedBase16StringLiteral, @@ -101,7 +111,7 @@ pub enum ErrorKind { #[help] hint: Option, }, - #[error("I tripped over a malformed base16-encoded string literal")] + #[error("I tripped over a malformed base16-encoded string literal.")] #[diagnostic(help("{}", formatdoc! { r#"You can declare literal bytearrays from base16-encoded (a.k.a. hexadecimal) string literals. @@ -113,6 +123,28 @@ pub enum ErrorKind { "# , "pub const".bright_blue(), "=".yellow(), "\"f4c9f9c4252d86702c2f4c2e49e6648c7cffe3c8f2b6b7d779788f50\"".bright_purple()}))] MalformedBase16StringLiteral, + #[error("I failed to understand a when clause guard.")] + #[diagnostic(url("https://aiken-lang.org/language-tour/control-flow#checking-equality-and-ordering-in-patterns"))] + #[diagnostic(help("{}", formatdoc! { + r#"Clause guards are not as capable as standard expressions. While you can combine multiple clauses using '{operator_or}' and '{operator_and}', you can't do any arithmetic in there. They are mainly meant to compare pattern variables to some known constants using simple binary operators. + + For example, the following clauses are well-formed: + + {good} (x, _) if x == 10 -> ... + {good} (_, y) if y > 0 && y < 10 -> ... + {good} (x, y) if x && (y > 0 || y < 10) -> ... + + However, those aren't: + + {bad} (x, _) if x % 3 == 0 -> ... + {bad} (x, y) if x + y > 42 -> ... + "# + , operator_or = "||".yellow() + , operator_and = "&&".yellow() + , good = "✔️".green() + , bad = "✖️".red() + }))] + InvalidWhenClause, } #[derive(Debug, PartialEq, Eq, Hash, Diagnostic, thiserror::Error)] diff --git a/crates/aiken-lang/src/tests/parser.rs b/crates/aiken-lang/src/tests/parser.rs index 75d5ce75..de8b06c5 100644 --- a/crates/aiken-lang/src/tests/parser.rs +++ b/crates/aiken-lang/src/tests/parser.rs @@ -2112,3 +2112,267 @@ fn subtraction_vs_negate() { })], ); } + +#[test] +fn clause_guards() { + let code = indoc! {r#" + fn foo() { + when a is { + _ if 42 -> Void + _ if bar -> Void + _ if True -> Void + _ if a || b && c -> Void + _ if a || (b && c) -> Void + _ if a <= 42 || b > 14 || "str" -> Void + _ if a == 14 && !b -> Void + _ if !!True -> Void + } + } + "#}; + assert_definitions( + code, + vec![ast::Definition::Fn(Function { + arguments: vec![], + body: expr::UntypedExpr::When { + location: Span::new((), 13..250), + subjects: vec![expr::UntypedExpr::Var { + location: Span::new((), 18..19), + name: "a".to_string(), + }], + clauses: vec![ + ast::Clause { + location: Span::new((), 29..44), + pattern: vec![ast::Pattern::Discard { + name: "_".to_string(), + location: Span::new((), 29..30), + }], + alternative_patterns: vec![], + guard: Some(ast::ClauseGuard::Constant(ast::Constant::Int { + location: Span::new((), 34..36), + value: "42".to_string(), + })), + then: expr::UntypedExpr::Var { + location: Span::new((), 40..44), + name: "Void".to_string(), + }, + }, + ast::Clause { + location: Span::new((), 49..65), + pattern: vec![ast::Pattern::Discard { + name: "_".to_string(), + location: Span::new((), 49..50), + }], + alternative_patterns: vec![], + guard: Some(ast::ClauseGuard::Var { + location: Span::new((), 54..57), + name: "bar".to_string(), + tipo: (), + }), + then: expr::UntypedExpr::Var { + location: Span::new((), 61..65), + name: "Void".to_string(), + }, + }, + ast::Clause { + location: Span::new((), 70..87), + pattern: vec![ast::Pattern::Discard { + name: "_".to_string(), + location: Span::new((), 70..71), + }], + alternative_patterns: vec![], + guard: Some(ast::ClauseGuard::Var { + location: Span::new((), 75..79), + tipo: (), + name: "True".to_string(), + }), + then: expr::UntypedExpr::Var { + location: Span::new((), 83..87), + name: "Void".to_string(), + }, + }, + ast::Clause { + location: Span::new((), 92..116), + pattern: vec![ast::Pattern::Discard { + name: "_".to_string(), + location: Span::new((), 92..93), + }], + alternative_patterns: vec![], + guard: Some(ast::ClauseGuard::And { + location: Span::new((), 97..108), + left: Box::new(ast::ClauseGuard::Or { + location: Span::new((), 97..103), + left: Box::new(ast::ClauseGuard::Var { + location: Span::new((), 97..98), + name: "a".to_string(), + tipo: (), + }), + right: Box::new(ast::ClauseGuard::Var { + location: Span::new((), 102..103), + name: "b".to_string(), + tipo: (), + }), + }), + right: Box::new(ast::ClauseGuard::Var { + location: Span::new((), 107..108), + name: "c".to_string(), + tipo: (), + }), + }), + then: expr::UntypedExpr::Var { + location: Span::new((), 112..116), + name: "Void".to_string(), + }, + }, + ast::Clause { + location: Span::new((), 121..147), + pattern: vec![ast::Pattern::Discard { + name: "_".to_string(), + location: Span::new((), 121..122), + }], + alternative_patterns: vec![], + guard: Some(ast::ClauseGuard::Or { + location: Span::new((), 126..138), + left: Box::new(ast::ClauseGuard::Var { + location: Span::new((), 126..127), + name: "a".to_string(), + tipo: (), + }), + right: Box::new(ast::ClauseGuard::And { + location: Span::new((), 132..138), + left: Box::new(ast::ClauseGuard::Var { + location: Span::new((), 132..133), + name: "b".to_string(), + tipo: (), + }), + right: Box::new(ast::ClauseGuard::Var { + location: Span::new((), 137..138), + name: "c".to_string(), + tipo: (), + }), + }), + }), + then: expr::UntypedExpr::Var { + location: Span::new((), 143..147), + name: "Void".to_string(), + }, + }, + ast::Clause { + location: Span::new((), 152..191), + pattern: vec![ast::Pattern::Discard { + name: "_".to_string(), + location: Span::new((), 152..153), + }], + alternative_patterns: vec![], + guard: Some(ast::ClauseGuard::Or { + location: Span::new((), 157..183), + left: Box::new(ast::ClauseGuard::Or { + location: Span::new((), 157..174), + left: Box::new(ast::ClauseGuard::LtEqInt { + location: Span::new((), 157..164), + left: Box::new(ast::ClauseGuard::Var { + location: Span::new((), 157..158), + name: "a".to_string(), + tipo: (), + }), + right: Box::new(ast::ClauseGuard::Constant( + ast::Constant::Int { + location: Span::new((), 162..164), + value: "42".to_string(), + }, + )), + }), + right: Box::new(ast::ClauseGuard::GtInt { + location: Span::new((), 168..174), + left: Box::new(ast::ClauseGuard::Var { + location: Span::new((), 168..169), + name: "b".to_string(), + tipo: (), + }), + right: Box::new(ast::ClauseGuard::Constant( + ast::Constant::Int { + location: Span::new((), 172..174), + value: "14".to_string(), + }, + )), + }), + }), + right: Box::new(ast::ClauseGuard::Constant(ast::Constant::String { + location: Span::new((), 178..183), + value: "str".to_string(), + })), + }), + then: expr::UntypedExpr::Var { + location: Span::new((), 187..191), + name: "Void".to_string(), + }, + }, + ast::Clause { + location: Span::new((), 196..222), + pattern: vec![ast::Pattern::Discard { + name: "_".to_string(), + location: Span::new((), 196..197), + }], + alternative_patterns: vec![], + guard: Some(ast::ClauseGuard::And { + location: Span::new((), 201..214), + left: Box::new(ast::ClauseGuard::Equals { + location: Span::new((), 201..208), + left: Box::new(ast::ClauseGuard::Var { + location: Span::new((), 201..202), + name: "a".to_string(), + tipo: (), + }), + right: Box::new(ast::ClauseGuard::Constant(ast::Constant::Int { + location: Span::new((), 206..208), + value: "14".to_string(), + })), + }), + right: Box::new(ast::ClauseGuard::Not { + location: Span::new((), 212..214), + value: Box::new(ast::ClauseGuard::Var { + location: Span::new((), 213..214), + name: "b".to_string(), + tipo: (), + }), + }), + }), + then: expr::UntypedExpr::Var { + location: Span::new((), 218..222), + name: "Void".to_string(), + }, + }, + ast::Clause { + location: Span::new((), 227..246), + pattern: vec![ast::Pattern::Discard { + name: "_".to_string(), + location: Span::new((), 227..228), + }], + alternative_patterns: vec![], + guard: Some(ast::ClauseGuard::Not { + location: Span::new((), 232..238), + value: Box::new(ast::ClauseGuard::Not { + location: Span::new((), 233..238), + value: Box::new(ast::ClauseGuard::Var { + location: Span::new((), 234..238), + tipo: (), + name: "True".to_string(), + }), + }), + }), + then: expr::UntypedExpr::Var { + location: Span::new((), 242..246), + name: "Void".to_string(), + }, + }, + ], + }, + doc: None, + location: Span::new((), 0..8), + name: "foo".to_string(), + public: false, + return_annotation: None, + return_type: (), + end_position: 251, + })], + ); +} diff --git a/crates/aiken-lang/src/tipo/expr.rs b/crates/aiken-lang/src/tipo/expr.rs index 788178cf..45f7132a 100644 --- a/crates/aiken-lang/src/tipo/expr.rs +++ b/crates/aiken-lang/src/tipo/expr.rs @@ -993,41 +993,17 @@ impl<'a, 'b> ExprTyper<'a, 'b> { }) } - // ClauseGuard::TupleIndex { - // location, - // tuple, - // index, - // .. - // } => { - // let tuple = self.infer_clause_guard(*tuple)?; - // match tuple.type_().as_ref() { - // Type::Tuple { elems } => { - // let type_ = elems - // .get(index as usize) - // .ok_or(Error::OutOfBoundsTupleIndex { - // location, - // index, - // size: elems.len(), - // })? - // .clone(); - // Ok(ClauseGuard::TupleIndex { - // location, - // index, - // type_, - // tuple: Box::new(tuple), - // }) - // } + ClauseGuard::Not { + location, value, .. + } => { + let value = self.infer_clause_guard(*value)?; + self.unify(bool(), value.tipo(), value.location())?; + Ok(ClauseGuard::Not { + location, + value: Box::new(value), + }) + } - // typ if typ.is_unbound() => Err(Error::NotATupleUnbound { - // location: tuple.location(), - // }), - - // _ => Err(Error::NotATuple { - // location: tuple.location(), - // given: tuple.type_(), - // }), - // } - // } ClauseGuard::And { location, left,