feat: impl if/is

This commit introduces a new feature into
the parser, typechecker, and formatter.
The work for code gen will be in the next commit.

I was able to leverage some existing infrastructure
by making using of `AssignmentPattern`. A new field
`is` was introduced into `IfBranch`. This field holds
a generic `Option<Is>` meaning a new generic has to be
introduced into `IfBranch`. When used in `UntypedExpr`,
`IfBranch` must use `AssignmentPattern`. When used in
`TypedExpr`, `IfBranch` must use `TypedPattern`.

The parser was updated such that we can support this
kind of psuedo grammar:

`if <expr:condition> [is [<pattern>: ]<annotation>]`

This can be read as, when parsing an `if` expression,
always expect an expression after the keyword `if`. And then
optionally there may be this `is` stuff, and within that you
may optionally expect a pattern followed by a colon. We will
always expect an annotation.

This first expression is still saved as the field
`condition` in `IfBranch`. If `pattern` is not there
AND `expr:condition` is `UntypedExpr::Var` we can set
the pattern to be `Pattern::Var` with the same name. From
there shadowing should allow this syntax sugar to feel
kinda magical within the `IfBranch` block that follow.

The typechecker doesn't need to be aware of the sugar
described above. The typechecker looks at `branch.is`
and if it's `Some(is)` then it'll use `infer_assignment`
for some help. Because of the way that `is` can inject
variables into the scope of the branch's block and since
it's basically just like how `expect` works minus the error
we get to re-use that helper method.

It's important to note that in the typechecker, if `is`
is `Some(_)` then we do not enforce that `condition` is
of type `Bool`. This is because the bool itself will be
whether or not the `is` itself holds true given a PlutusData
payload.

When `is` is None, we do exactly what was being done
previously so that plain `if` expressions remain unaffected
with no semantic changes.

The formatter had to be made aware of the new changes with
some simple changes that need no further explanation.
This commit is contained in:
rvcas
2024-06-11 19:05:55 -04:00
committed by Lucas
parent b2c42febaf
commit 1b8805825b
15 changed files with 599 additions and 74 deletions

View File

@@ -33,6 +33,13 @@ pub fn let_(
}
fn assignment_patterns() -> impl Parser<Token, Vec<ast::AssignmentPattern>, Error = ParseError> {
assignment_pattern()
.separated_by(just(Token::Comma))
.allow_trailing()
.at_least(1)
}
pub fn assignment_pattern() -> impl Parser<Token, ast::AssignmentPattern, Error = ParseError> {
pattern()
.then(just(Token::Colon).ignore_then(annotation()).or_not())
.map_with_span(|(pattern, annotation), span| ast::AssignmentPattern {
@@ -40,9 +47,6 @@ fn assignment_patterns() -> impl Parser<Token, Vec<ast::AssignmentPattern>, Erro
annotation,
location: span,
})
.separated_by(just(Token::Comma))
.allow_trailing()
.at_least(1)
}
pub fn expect(

View File

@@ -3,7 +3,7 @@ use chumsky::prelude::*;
use crate::{
ast,
expr::UntypedExpr,
parser::{error::ParseError, token::Token},
parser::{annotation, error::ParseError, pattern, token::Token},
};
use super::block;
@@ -40,11 +40,44 @@ fn if_branch<'a>(
expression: Recursive<'a, Token, UntypedExpr, ParseError>,
) -> impl Parser<Token, ast::UntypedIfBranch, Error = ParseError> + 'a {
expression
.then(
just(Token::Is)
.ignore_then(
pattern()
.then_ignore(just(Token::Colon))
.or_not()
.then(annotation())
.map_with_span(|(pattern, annotation), span| (pattern, annotation, span)),
)
.or_not(),
)
.then(block(sequence))
.map_with_span(|(condition, body), span| ast::IfBranch {
condition,
body,
location: span,
.map_with_span(|((condition, is), body), span| {
let is = is.map(|(pattern, annotation, is_span)| {
let pattern = pattern.unwrap_or_else(|| match &condition {
UntypedExpr::Var { name, location } => ast::Pattern::Var {
name: name.clone(),
location: *location,
},
_ => ast::Pattern::Discard {
location: is_span,
name: "_".to_string(),
},
});
ast::AssignmentPattern {
pattern,
annotation: Some(annotation),
location: is_span,
}
});
ast::IfBranch {
condition,
body,
is,
location: span,
}
})
}
@@ -81,4 +114,23 @@ mod tests {
"#
);
}
#[test]
fn if_else_with_soft_cast() {
assert_expr!(
r#"
if ec1 is Some(x): Option<Int> {
ec2
} else if ec1 is Foo { foo }: Foo {
ec1
} else if ec1 is Option<Int> {
let Some(x) = ec1
x
} else {
Infinity
}
"#
);
}
}

View File

@@ -22,6 +22,7 @@ If {
location: 23..26,
name: "ec2",
},
is: None,
location: 3..28,
},
IfBranch {
@@ -56,6 +57,7 @@ If {
location: 60..63,
name: "ec1",
},
is: None,
location: 37..65,
},
],

View File

@@ -28,6 +28,7 @@ If {
},
},
},
is: None,
location: 3..19,
},
IfBranch {
@@ -53,6 +54,7 @@ If {
numeric_underscore: false,
},
},
is: None,
location: 28..41,
},
],

View File

@@ -0,0 +1,183 @@
---
source: crates/aiken-lang/src/parser/expr/if_else.rs
description: "Code:\n\nif ec1 is Some(x): Option<Int> {\n ec2\n} else if ec1 is Foo { foo }: Foo {\n ec1\n} else if ec1 is Option<Int> {\n let Some(x) = ec1\n\n x\n} else {\n Infinity\n}\n"
---
If {
location: 0..158,
branches: [
IfBranch {
condition: Var {
location: 3..6,
name: "ec1",
},
body: Var {
location: 35..38,
name: "ec2",
},
is: Some(
AssignmentPattern {
pattern: Constructor {
is_record: false,
location: 10..17,
name: "Some",
arguments: [
CallArg {
label: None,
location: 15..16,
value: Var {
location: 15..16,
name: "x",
},
},
],
module: None,
constructor: (),
spread_location: None,
tipo: (),
},
annotation: Some(
Constructor {
location: 19..30,
module: None,
name: "Option",
arguments: [
Constructor {
location: 26..29,
module: None,
name: "Int",
arguments: [],
},
],
},
),
location: 10..30,
},
),
location: 3..40,
},
IfBranch {
condition: Var {
location: 49..52,
name: "ec1",
},
body: Var {
location: 77..80,
name: "ec1",
},
is: Some(
AssignmentPattern {
pattern: Constructor {
is_record: true,
location: 56..67,
name: "Foo",
arguments: [
CallArg {
label: Some(
"foo",
),
location: 62..65,
value: Var {
location: 62..65,
name: "foo",
},
},
],
module: None,
constructor: (),
spread_location: None,
tipo: (),
},
annotation: Some(
Constructor {
location: 69..72,
module: None,
name: "Foo",
arguments: [],
},
),
location: 56..72,
},
),
location: 49..82,
},
IfBranch {
condition: Var {
location: 91..94,
name: "ec1",
},
body: Sequence {
location: 114..136,
expressions: [
Assignment {
location: 114..131,
value: Var {
location: 128..131,
name: "ec1",
},
patterns: [
AssignmentPattern {
pattern: Constructor {
is_record: false,
location: 118..125,
name: "Some",
arguments: [
CallArg {
label: None,
location: 123..124,
value: Var {
location: 123..124,
name: "x",
},
},
],
module: None,
constructor: (),
spread_location: None,
tipo: (),
},
annotation: None,
location: 118..125,
},
],
kind: Let {
backpassing: false,
},
},
Var {
location: 135..136,
name: "x",
},
],
},
is: Some(
AssignmentPattern {
pattern: Var {
location: 91..94,
name: "ec1",
},
annotation: Some(
Constructor {
location: 98..109,
module: None,
name: "Option",
arguments: [
Constructor {
location: 105..108,
module: None,
name: "Int",
arguments: [],
},
],
},
),
location: 98..109,
},
),
location: 91..138,
},
],
final_else: Var {
location: 148..156,
name: "Infinity",
},
}