Authorize complete patterns as function args.

This is mainly a syntactic trick/sugar, but it's been pretty annoying
  to me for a while that we can't simply pattern-match/destructure
  single-variant constructors directly from the args list. A classic
  example is when writing property tests:

  ```ak
  test foo(params via both(bytearray(), int())) {
    let (bytes, ix) = params
    ...
  }
  ```

  Now can be replaced simply with:

  ```
  test foo((bytes, ix) via both(bytearray(), int())) {
    ...
  }
  ```

  If feels natural, especially coming from the JavaScript, Haskell or
  Rust worlds and is mostly convenient. Behind the scene, the compiler
  does nothing more than re-writing the AST as the first form, with
  pre-generated arg names. Then, we fully rely on the existing
  type-checking capabilities and thus, works in a seamless way as if we
  were just pattern matching inline.
This commit is contained in:
KtorZ
2024-06-07 15:23:33 +02:00
parent b6da42baf2
commit 858dfccc82
23 changed files with 944 additions and 68 deletions

View File

@@ -1,8 +1,7 @@
use crate::{
ast,
ast::ArgBy::ByName,
expr::UntypedExpr,
parser::{annotation, error::ParseError, expr, token::Token, utils},
parser::{annotation, error::ParseError, expr, pattern, token::Token, utils},
};
use chumsky::prelude::*;
@@ -51,38 +50,45 @@ pub fn param(is_validator_param: bool) -> impl Parser<Token, ast::UntypedArg, Er
choice((
select! {Token::Name {name} => name}
.then(select! {Token::DiscardName {name} => name})
.map_with_span(|(label, name), span| ast::ArgName::Discarded {
label,
name,
location: span,
.map_with_span(|(label, name), span| {
ast::ArgBy::ByName(ast::ArgName::Discarded {
label,
name,
location: span,
})
}),
select! {Token::DiscardName {name} => name}.map_with_span(|name, span| {
ast::ArgName::Discarded {
ast::ArgBy::ByName(ast::ArgName::Discarded {
label: name.clone(),
name,
location: span,
}
})
}),
select! {Token::Name {name} => name}
.then(select! {Token::Name {name} => name})
.map_with_span(|(label, name), span| ast::ArgName::Named {
label,
.map_with_span(|(label, name), span| {
ast::ArgBy::ByName(ast::ArgName::Named {
label,
name,
location: span,
})
}),
select! {Token::Name {name} => name}.map_with_span(|name, span| {
ast::ArgBy::ByName(ast::ArgName::Named {
label: name.clone(),
name,
location: span,
}),
select! {Token::Name {name} => name}.map_with_span(|name, span| ast::ArgName::Named {
label: name.clone(),
name,
location: span,
})
}),
pattern().map(ast::ArgBy::ByPattern),
))
.then(just(Token::Colon).ignore_then(annotation()).or_not())
.map_with_span(move |(arg_name, annotation), span| ast::UntypedArg {
.map_with_span(move |(by, annotation), span| ast::UntypedArg {
location: span,
annotation,
doc: None,
is_validator_param,
by: ByName(arg_name),
by,
})
}
@@ -118,4 +124,36 @@ mod tests {
"#
);
}
#[test]
fn function_by_pattern_no_annotation() {
assert_definition!(
r#"
fn foo(Foo { my_field }) {
my_field * 2
}
"#
);
}
#[test]
fn function_by_pattern_with_annotation() {
assert_definition!(
r#"
fn foo(Foo { my_field }: Foo) {
my_field * 2
}
"#
);
}
#[test]
fn function_by_pattern_with_alias() {
assert_definition!(
r#"
fn foo(Foo { my_field, .. } as x) {
my_field * x.my_other_field
}
"#
);
}
}

View File

@@ -0,0 +1,62 @@
---
source: crates/aiken-lang/src/parser/definition/function.rs
description: "Code:\n\nfn foo(Foo { my_field }) {\n my_field * 2\n}\n"
---
Fn(
Function {
arguments: [
UntypedArg {
by: ByPattern(
Constructor {
is_record: true,
location: 7..23,
name: "Foo",
arguments: [
CallArg {
label: Some(
"my_field",
),
location: 13..21,
value: Var {
location: 13..21,
name: "my_field",
},
},
],
module: None,
constructor: (),
spread_location: None,
tipo: (),
},
),
location: 7..23,
annotation: None,
doc: None,
is_validator_param: false,
},
],
body: BinOp {
location: 31..43,
name: MultInt,
left: Var {
location: 31..39,
name: "my_field",
},
right: UInt {
location: 42..43,
value: "2",
base: Decimal {
numeric_underscore: false,
},
},
},
doc: None,
location: 0..24,
name: "foo",
public: false,
return_annotation: None,
return_type: (),
end_position: 44,
on_test_failure: FailImmediately,
},
)

View File

@@ -0,0 +1,69 @@
---
source: crates/aiken-lang/src/parser/definition/function.rs
description: "Code:\n\nfn foo(Foo { my_field, .. } as x) {\n my_field * x.my_other_field\n}\n"
---
Fn(
Function {
arguments: [
UntypedArg {
by: ByPattern(
Assign {
name: "x",
location: 7..32,
pattern: Constructor {
is_record: true,
location: 7..27,
name: "Foo",
arguments: [
CallArg {
label: Some(
"my_field",
),
location: 13..21,
value: Var {
location: 13..21,
name: "my_field",
},
},
],
module: None,
constructor: (),
spread_location: Some(
23..25,
),
tipo: (),
},
},
),
location: 7..32,
annotation: None,
doc: None,
is_validator_param: false,
},
],
body: BinOp {
location: 40..67,
name: MultInt,
left: Var {
location: 40..48,
name: "my_field",
},
right: FieldAccess {
location: 51..67,
label: "my_other_field",
container: Var {
location: 51..52,
name: "x",
},
},
},
doc: None,
location: 0..33,
name: "foo",
public: false,
return_annotation: None,
return_type: (),
end_position: 68,
on_test_failure: FailImmediately,
},
)

View File

@@ -0,0 +1,69 @@
---
source: crates/aiken-lang/src/parser/definition/function.rs
description: "Code:\n\nfn foo(Foo { my_field }: Foo) {\n my_field * 2\n}\n"
---
Fn(
Function {
arguments: [
UntypedArg {
by: ByPattern(
Constructor {
is_record: true,
location: 7..23,
name: "Foo",
arguments: [
CallArg {
label: Some(
"my_field",
),
location: 13..21,
value: Var {
location: 13..21,
name: "my_field",
},
},
],
module: None,
constructor: (),
spread_location: None,
tipo: (),
},
),
location: 7..28,
annotation: Some(
Constructor {
location: 25..28,
module: None,
name: "Foo",
arguments: [],
},
),
doc: None,
is_validator_param: false,
},
],
body: BinOp {
location: 36..48,
name: MultInt,
left: Var {
location: 36..44,
name: "my_field",
},
right: UInt {
location: 47..48,
value: "2",
base: Decimal {
numeric_underscore: false,
},
},
},
doc: None,
location: 0..29,
name: "foo",
public: false,
return_annotation: None,
return_type: (),
end_position: 49,
on_test_failure: FailImmediately,
},
)

View File

@@ -7,6 +7,7 @@ use crate::{
chain::{call::parser as call, field_access, tuple_index::parser as tuple_index, Chain},
error::ParseError,
expr::{self, bytearray, int as uint, list, string, tuple, var},
pattern,
token::Token,
},
};
@@ -54,25 +55,28 @@ pub fn parser() -> impl Parser<Token, ast::UntypedDefinition, Error = ParseError
pub fn via() -> impl Parser<Token, ast::UntypedArgVia, Error = ParseError> {
choice((
select! {Token::DiscardName {name} => name}.map_with_span(|name, span| {
ast::ArgName::Discarded {
ast::ArgBy::ByName(ast::ArgName::Discarded {
label: name.clone(),
name,
location: span,
}
})
}),
select! {Token::Name {name} => name}.map_with_span(|name, location| ast::ArgName::Named {
label: name.clone(),
name,
location,
select! {Token::Name {name} => name}.map_with_span(|name, location| {
ast::ArgBy::ByName(ast::ArgName::Named {
label: name.clone(),
name,
location,
})
}),
pattern().map(ast::ArgBy::ByPattern),
))
.then(just(Token::Colon).ignore_then(annotation()).or_not())
.map_with_span(|(arg_name, annotation), location| (arg_name, annotation, location))
.then_ignore(just(Token::Via))
.then(fuzzer())
.map(|((arg_name, annotation, location), via)| ast::ArgVia {
.map(|((by, annotation, location), via)| ast::ArgVia {
arg: ast::UntypedArg {
by: ast::ArgBy::ByName(arg_name),
by,
annotation,
location,
doc: None,

View File

@@ -1,7 +1,7 @@
use crate::{
ast,
expr::{FnStyle, UntypedExpr},
parser::{annotation, error::ParseError, token::Token},
parser::{annotation, error::ParseError, pattern, token::Token},
};
use chumsky::prelude::*;
@@ -32,25 +32,28 @@ pub fn params() -> impl Parser<Token, ast::UntypedArg, Error = ParseError> {
// TODO: return a better error when a label is provided `UnexpectedLabel`
choice((
select! {Token::DiscardName {name} => name}.map_with_span(|name, span| {
ast::ArgName::Discarded {
ast::ArgBy::ByName(ast::ArgName::Discarded {
label: name.clone(),
name,
location: span,
}
})
}),
select! {Token::Name {name} => name}.map_with_span(|name, span| ast::ArgName::Named {
label: name.clone(),
name,
location: span,
select! {Token::Name {name} => name}.map_with_span(|name, span| {
ast::ArgBy::ByName(ast::ArgName::Named {
label: name.clone(),
name,
location: span,
})
}),
pattern().map(ast::ArgBy::ByPattern),
))
.then(just(Token::Colon).ignore_then(annotation()).or_not())
.map_with_span(|(arg_name, annotation), span| ast::UntypedArg {
.map_with_span(|(by, annotation), span| ast::UntypedArg {
is_validator_param: false,
location: span,
annotation,
doc: None,
by: ast::ArgBy::ByName(arg_name),
by,
})
}
@@ -62,4 +65,19 @@ mod tests {
fn anonymous_function_basic() {
assert_expr!(r#"fn (a: Int) -> Int { a + 1 }"#);
}
#[test]
fn anonymous_function_by_pattern_no_annotation() {
assert_expr!(r#"fn (Foo { my_field }) { my_field * 2 }"#);
}
#[test]
fn anonymous_function_by_pattern_with_annotation() {
assert_expr!(r#"fn (Foo { my_field } : Foo) { my_field * 2 }"#);
}
#[test]
fn anonymous_function_by_pattern_with_alias() {
assert_expr!(r#"fn (Foo { my_field, .. } as x) { my_field * my_other_field }"#);
}
}

View File

@@ -0,0 +1,55 @@
---
source: crates/aiken-lang/src/parser/expr/anonymous_function.rs
description: "Code:\n\nfn (Foo { my_field }) { my_field * 2 }"
---
Fn {
location: 0..38,
fn_style: Plain,
arguments: [
UntypedArg {
by: ByPattern(
Constructor {
is_record: true,
location: 4..20,
name: "Foo",
arguments: [
CallArg {
label: Some(
"my_field",
),
location: 10..18,
value: Var {
location: 10..18,
name: "my_field",
},
},
],
module: None,
constructor: (),
spread_location: None,
tipo: (),
},
),
location: 4..20,
annotation: None,
doc: None,
is_validator_param: false,
},
],
body: BinOp {
location: 24..36,
name: MultInt,
left: Var {
location: 24..32,
name: "my_field",
},
right: UInt {
location: 35..36,
value: "2",
base: Decimal {
numeric_underscore: false,
},
},
},
return_annotation: None,
}

View File

@@ -0,0 +1,58 @@
---
source: crates/aiken-lang/src/parser/expr/anonymous_function.rs
description: "Code:\n\nfn (Foo { my_field, .. } as x) { my_field * my_other_field }"
---
Fn {
location: 0..60,
fn_style: Plain,
arguments: [
UntypedArg {
by: ByPattern(
Assign {
name: "x",
location: 4..29,
pattern: Constructor {
is_record: true,
location: 4..24,
name: "Foo",
arguments: [
CallArg {
label: Some(
"my_field",
),
location: 10..18,
value: Var {
location: 10..18,
name: "my_field",
},
},
],
module: None,
constructor: (),
spread_location: Some(
20..22,
),
tipo: (),
},
},
),
location: 4..29,
annotation: None,
doc: None,
is_validator_param: false,
},
],
body: BinOp {
location: 33..58,
name: MultInt,
left: Var {
location: 33..41,
name: "my_field",
},
right: Var {
location: 44..58,
name: "my_other_field",
},
},
return_annotation: None,
}

View File

@@ -0,0 +1,62 @@
---
source: crates/aiken-lang/src/parser/expr/anonymous_function.rs
description: "Code:\n\nfn (Foo { my_field } : Foo) { my_field * 2 }"
---
Fn {
location: 0..44,
fn_style: Plain,
arguments: [
UntypedArg {
by: ByPattern(
Constructor {
is_record: true,
location: 4..20,
name: "Foo",
arguments: [
CallArg {
label: Some(
"my_field",
),
location: 10..18,
value: Var {
location: 10..18,
name: "my_field",
},
},
],
module: None,
constructor: (),
spread_location: None,
tipo: (),
},
),
location: 4..26,
annotation: Some(
Constructor {
location: 23..26,
module: None,
name: "Foo",
arguments: [],
},
),
doc: None,
is_validator_param: false,
},
],
body: BinOp {
location: 30..42,
name: MultInt,
left: Var {
location: 30..38,
name: "my_field",
},
right: UInt {
location: 41..42,
value: "2",
base: Decimal {
numeric_underscore: false,
},
},
},
return_annotation: None,
}