From 858dfccc823335fc1d865b3c2d461b42dd45d30b Mon Sep 17 00:00:00 2001 From: KtorZ Date: Fri, 7 Jun 2024 15:23:33 +0200 Subject: [PATCH] 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. --- CHANGELOG.md | 4 + crates/aiken-lang/src/ast.rs | 28 ++++++- crates/aiken-lang/src/format.rs | 36 +++++---- .../src/parser/definition/function.rs | 72 +++++++++++++---- .../function_by_pattern_no_annotation.snap | 62 ++++++++++++++ .../function_by_pattern_with_alias.snap | 69 ++++++++++++++++ .../function_by_pattern_with_annotation.snap | 69 ++++++++++++++++ .../aiken-lang/src/parser/definition/test.rs | 20 +++-- .../src/parser/expr/anonymous_function.rs | 36 ++++++--- ...ous_function_by_pattern_no_annotation.snap | 55 +++++++++++++ ...nymous_function_by_pattern_with_alias.snap | 58 ++++++++++++++ ...s_function_by_pattern_with_annotation.snap | 62 ++++++++++++++ crates/aiken-lang/src/tests/check.rs | 34 ++++++++ crates/aiken-lang/src/tests/format.rs | 65 +++++++++++++++ .../snapshots/format_anon_fn_pattern.snap | 23 ++++++ .../tests/snapshots/format_fn_pattern.snap | 23 ++++++ .../snapshots/format_validator_pattern.snap | 27 +++++++ crates/aiken-lang/src/tipo/expr.rs | 74 +++++++++++++++-- crates/aiken-project/src/test_framework.rs | 21 ++--- examples/acceptance_tests/104/aiken.lock | 28 +++++++ examples/acceptance_tests/104/aiken.toml | 21 +++++ examples/acceptance_tests/104/plutus.json | 80 +++++++++++++++++++ .../acceptance_tests/104/validators/tests.ak | 45 +++++++++++ 23 files changed, 944 insertions(+), 68 deletions(-) create mode 100644 crates/aiken-lang/src/parser/definition/snapshots/function_by_pattern_no_annotation.snap create mode 100644 crates/aiken-lang/src/parser/definition/snapshots/function_by_pattern_with_alias.snap create mode 100644 crates/aiken-lang/src/parser/definition/snapshots/function_by_pattern_with_annotation.snap create mode 100644 crates/aiken-lang/src/parser/expr/snapshots/anonymous_function_by_pattern_no_annotation.snap create mode 100644 crates/aiken-lang/src/parser/expr/snapshots/anonymous_function_by_pattern_with_alias.snap create mode 100644 crates/aiken-lang/src/parser/expr/snapshots/anonymous_function_by_pattern_with_annotation.snap create mode 100644 crates/aiken-lang/src/tests/snapshots/format_anon_fn_pattern.snap create mode 100644 crates/aiken-lang/src/tests/snapshots/format_fn_pattern.snap create mode 100644 crates/aiken-lang/src/tests/snapshots/format_validator_pattern.snap create mode 100644 examples/acceptance_tests/104/aiken.lock create mode 100644 examples/acceptance_tests/104/aiken.toml create mode 100644 examples/acceptance_tests/104/plutus.json create mode 100644 examples/acceptance_tests/104/validators/tests.ak diff --git a/CHANGELOG.md b/CHANGELOG.md index 98aa26dd..e77243fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## v1.0.30-alpha - UNRELEASED +### Added + +- **aiken-lang**: also authorize (complete) patterns in function arguments list instead of only variable names. @KtorZ + ### Changed - **aiken-lang**: duplicate import lines are now automatically merged instead of raising a warning. However, imports can no longer appear anywhere in the file and must come as the first definitions. @KtorZ diff --git a/crates/aiken-lang/src/ast.rs b/crates/aiken-lang/src/ast.rs index d83211c1..a3220a1e 100644 --- a/crates/aiken-lang/src/ast.rs +++ b/crates/aiken-lang/src/ast.rs @@ -15,7 +15,7 @@ use std::{ rc::Rc, }; use uplc::machine::runtime::Compressable; -use vec1::Vec1; +use vec1::{vec1, Vec1}; pub const BACKPASS_VARIABLE: &str = "_backpass"; pub const CAPTURE_VARIABLE: &str = "_capture"; @@ -797,6 +797,32 @@ pub enum ArgBy { ByPattern(UntypedPattern), } +impl ArgBy { + pub fn into_extra_assignment( + self, + name: &ArgName, + annotation: Option<&Annotation>, + location: Span, + ) -> Option { + match self { + ArgBy::ByName(..) => None, + ArgBy::ByPattern(pattern) => Some(UntypedExpr::Assignment { + location, + value: Box::new(UntypedExpr::Var { + location, + name: name.get_name(), + }), + patterns: vec1![AssignmentPattern { + pattern, + location, + annotation: annotation.cloned(), + }], + kind: AssignmentKind::Let { backpassing: false }, + }), + } + } +} + #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub struct UntypedArg { pub by: ArgBy, diff --git a/crates/aiken-lang/src/format.rs b/crates/aiken-lang/src/format.rs index 7350a949..c5a36dfc 100644 --- a/crates/aiken-lang/src/format.rs +++ b/crates/aiken-lang/src/format.rs @@ -464,15 +464,17 @@ impl<'comments> Formatter<'comments> { let doc_comments = self.doc_comments(arg.location.start); - let doc = match arg.by { - ArgBy::ByName(ref arg_name) => match &arg.annotation { - None => arg_name.to_doc(), - Some(a) => arg_name.to_doc().append(": ").append(self.annotation(a)), - } - .group(), - ArgBy::ByPattern(..) => todo!(), + let mut doc = match arg.by { + ArgBy::ByName(ref arg_name) => arg_name.to_doc(), + ArgBy::ByPattern(ref pattern) => self.pattern(pattern), }; + doc = match &arg.annotation { + None => doc, + Some(a) => doc.append(": ").append(self.annotation(a)), + } + .group(); + let doc = doc_comments.append(doc.group()).group(); commented(doc, comments) @@ -483,12 +485,14 @@ impl<'comments> Formatter<'comments> { let doc_comments = self.doc_comments(arg_via.arg.location.start); - let doc = match arg_via.arg.by { - ArgBy::ByName(ref arg_name) => match &arg_via.arg.annotation { - None => arg_name.to_doc(), - Some(a) => arg_name.to_doc().append(": ").append(self.annotation(a)), - }, - ArgBy::ByPattern(..) => todo!(), + let mut doc = match arg_via.arg.by { + ArgBy::ByName(ref arg_name) => arg_name.to_doc(), + ArgBy::ByPattern(ref pattern) => self.pattern(pattern), + }; + + doc = match &arg_via.arg.annotation { + None => doc, + Some(a) => doc.append(": ").append(self.annotation(a)), } .append(" via ") .append(self.expr(&arg_via.via, false)) @@ -1981,7 +1985,11 @@ impl<'a> Documentable<'a> for &'a ArgName { } fn pub_(public: bool) -> Document<'static> { - if public { "pub ".to_doc() } else { nil() } + if public { + "pub ".to_doc() + } else { + nil() + } } impl<'a> Documentable<'a> for &'a UnqualifiedImport { diff --git a/crates/aiken-lang/src/parser/definition/function.rs b/crates/aiken-lang/src/parser/definition/function.rs index 099f14e1..fe5f15ae 100644 --- a/crates/aiken-lang/src/parser/definition/function.rs +++ b/crates/aiken-lang/src/parser/definition/function.rs @@ -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 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 + } + "# + ); + } } diff --git a/crates/aiken-lang/src/parser/definition/snapshots/function_by_pattern_no_annotation.snap b/crates/aiken-lang/src/parser/definition/snapshots/function_by_pattern_no_annotation.snap new file mode 100644 index 00000000..359a9343 --- /dev/null +++ b/crates/aiken-lang/src/parser/definition/snapshots/function_by_pattern_no_annotation.snap @@ -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, + }, +) diff --git a/crates/aiken-lang/src/parser/definition/snapshots/function_by_pattern_with_alias.snap b/crates/aiken-lang/src/parser/definition/snapshots/function_by_pattern_with_alias.snap new file mode 100644 index 00000000..69128a87 --- /dev/null +++ b/crates/aiken-lang/src/parser/definition/snapshots/function_by_pattern_with_alias.snap @@ -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, + }, +) diff --git a/crates/aiken-lang/src/parser/definition/snapshots/function_by_pattern_with_annotation.snap b/crates/aiken-lang/src/parser/definition/snapshots/function_by_pattern_with_annotation.snap new file mode 100644 index 00000000..6b3da311 --- /dev/null +++ b/crates/aiken-lang/src/parser/definition/snapshots/function_by_pattern_with_annotation.snap @@ -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, + }, +) diff --git a/crates/aiken-lang/src/parser/definition/test.rs b/crates/aiken-lang/src/parser/definition/test.rs index 7e2cbec1..0931c30d 100644 --- a/crates/aiken-lang/src/parser/definition/test.rs +++ b/crates/aiken-lang/src/parser/definition/test.rs @@ -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 impl Parser { 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, diff --git a/crates/aiken-lang/src/parser/expr/anonymous_function.rs b/crates/aiken-lang/src/parser/expr/anonymous_function.rs index 2d092ce4..39f35259 100644 --- a/crates/aiken-lang/src/parser/expr/anonymous_function.rs +++ b/crates/aiken-lang/src/parser/expr/anonymous_function.rs @@ -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 { // 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 }"#); + } } diff --git a/crates/aiken-lang/src/parser/expr/snapshots/anonymous_function_by_pattern_no_annotation.snap b/crates/aiken-lang/src/parser/expr/snapshots/anonymous_function_by_pattern_no_annotation.snap new file mode 100644 index 00000000..5bb821d1 --- /dev/null +++ b/crates/aiken-lang/src/parser/expr/snapshots/anonymous_function_by_pattern_no_annotation.snap @@ -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, +} diff --git a/crates/aiken-lang/src/parser/expr/snapshots/anonymous_function_by_pattern_with_alias.snap b/crates/aiken-lang/src/parser/expr/snapshots/anonymous_function_by_pattern_with_alias.snap new file mode 100644 index 00000000..4ac06aca --- /dev/null +++ b/crates/aiken-lang/src/parser/expr/snapshots/anonymous_function_by_pattern_with_alias.snap @@ -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, +} diff --git a/crates/aiken-lang/src/parser/expr/snapshots/anonymous_function_by_pattern_with_annotation.snap b/crates/aiken-lang/src/parser/expr/snapshots/anonymous_function_by_pattern_with_annotation.snap new file mode 100644 index 00000000..5a2251e8 --- /dev/null +++ b/crates/aiken-lang/src/parser/expr/snapshots/anonymous_function_by_pattern_with_annotation.snap @@ -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, +} diff --git a/crates/aiken-lang/src/tests/check.rs b/crates/aiken-lang/src/tests/check.rs index e4d60b6c..b9d19b0b 100644 --- a/crates/aiken-lang/src/tests/check.rs +++ b/crates/aiken-lang/src/tests/check.rs @@ -2539,3 +2539,37 @@ fn mutually_recursive_1() { assert!(check(parse(source_code)).is_ok()); } + +#[test] +fn fn_single_variant_pattern() { + let source_code = r#" + pub type Foo { + a: Int + } + + pub fn foo(Foo { a }) { + a + 1 + } + "#; + + assert!(dbg!(check(parse(source_code))).is_ok()); +} + +#[test] +fn fn_multi_variant_pattern() { + let source_code = r#" + type Foo { + A { a: Int } + B { b: Int } + } + + pub fn foo(A { a }) { + a + 1 + } + "#; + + assert!(matches!( + dbg!(check_validator(parse(source_code))), + Err((_, Error::NotExhaustivePatternMatch { .. })) + )) +} diff --git a/crates/aiken-lang/src/tests/format.rs b/crates/aiken-lang/src/tests/format.rs index c8a5f0c2..a9d951db 100644 --- a/crates/aiken-lang/src/tests/format.rs +++ b/crates/aiken-lang/src/tests/format.rs @@ -883,3 +883,68 @@ fn format_pairs() { }"# ); } + +#[test] +fn format_fn_pattern() { + assert_format!( + r#" + pub fn foo(Foo { a, b, .. }) { + todo + } + + pub fn bar([Bar] : List) { + todo + } + + pub fn baz((Baz, Baz) as x) { + todo + } + + pub fn fiz(Pair(fst, snd) as x: Pair) { + todo + } + + test buz((a, b) via some_fuzzer()) { + todo + } + "# + ); +} + +#[test] +fn format_anon_fn_pattern() { + assert_format!( + r#" + pub fn main() { + let foo = fn (Foo { a, b, .. }) { todo } + let bar = fn ([Bar] : List) { todo } + let baz = fn ((Baz, Baz) as x) { todo } + let fiz = fn (Pair(fst, snd) as x: Pair) { todo } + todo + } + "# + ); +} + +#[test] +fn format_validator_pattern() { + assert_format!( + r#" + validator(Foo { a, b, .. }) { + fn foo() { todo } + } + + validator([Bar] : List) { + fn bar() { todo } + } + + validator((Baz, Baz) as x) { + fn baz() { todo } + } + + validator((fst, snd) as x: Pair) { + fn fiz() { todo } + } + "# + ); +} diff --git a/crates/aiken-lang/src/tests/snapshots/format_anon_fn_pattern.snap b/crates/aiken-lang/src/tests/snapshots/format_anon_fn_pattern.snap new file mode 100644 index 00000000..00e712a7 --- /dev/null +++ b/crates/aiken-lang/src/tests/snapshots/format_anon_fn_pattern.snap @@ -0,0 +1,23 @@ +--- +source: crates/aiken-lang/src/tests/format.rs +description: "Code:\n\npub fn main() {\n let foo = fn (Foo { a, b, .. }) { todo }\n let bar = fn ([Bar] : List) { todo }\n let baz = fn ((Baz, Baz) as x) { todo }\n let fiz = fn (Pair(fst, snd) as x: Pair) { todo }\n todo\n}\n" +--- +pub fn main() { + let foo = + fn(Foo { a, b, .. }) { + todo + } + let bar = + fn([Bar]: List) { + todo + } + let baz = + fn((Baz, Baz) as x) { + todo + } + let fiz = + fn(Pair(fst, snd) as x: Pair) { + todo + } + todo +} diff --git a/crates/aiken-lang/src/tests/snapshots/format_fn_pattern.snap b/crates/aiken-lang/src/tests/snapshots/format_fn_pattern.snap new file mode 100644 index 00000000..7746e0fb --- /dev/null +++ b/crates/aiken-lang/src/tests/snapshots/format_fn_pattern.snap @@ -0,0 +1,23 @@ +--- +source: crates/aiken-lang/src/tests/format.rs +description: "Code:\n\npub fn foo(Foo { a, b, .. }) {\n todo\n}\n\npub fn bar([Bar] : List) {\n todo\n}\n\npub fn baz((Baz, Baz) as x) {\n todo\n}\n\npub fn fiz(Pair(fst, snd) as x: Pair) {\n todo\n}\n\ntest buz((a, b) via some_fuzzer()) {\n todo\n}\n" +--- +pub fn foo(Foo { a, b, .. }) { + todo +} + +pub fn bar([Bar]: List) { + todo +} + +pub fn baz((Baz, Baz) as x) { + todo +} + +pub fn fiz(Pair(fst, snd) as x: Pair) { + todo +} + +test buz((a, b) via some_fuzzer()) { + todo +} diff --git a/crates/aiken-lang/src/tests/snapshots/format_validator_pattern.snap b/crates/aiken-lang/src/tests/snapshots/format_validator_pattern.snap new file mode 100644 index 00000000..74023392 --- /dev/null +++ b/crates/aiken-lang/src/tests/snapshots/format_validator_pattern.snap @@ -0,0 +1,27 @@ +--- +source: crates/aiken-lang/src/tests/format.rs +description: "Code:\n\nvalidator(Foo { a, b, .. }) {\n fn foo() { todo }\n}\n\nvalidator([Bar] : List) {\n fn bar() { todo }\n}\n\nvalidator((Baz, Baz) as x) {\n fn baz() { todo }\n}\n\nvalidator((fst, snd) as x: Pair) {\n fn fiz() { todo }\n}\n" +--- +validator(Foo { a, b, .. }) { + fn foo() { + todo + } +} + +validator([Bar]: List) { + fn bar() { + todo + } +} + +validator((Baz, Baz) as x) { + fn baz() { + todo + } +} + +validator((fst, snd) as x: Pair) { + fn fiz() { + todo + } +} diff --git a/crates/aiken-lang/src/tipo/expr.rs b/crates/aiken-lang/src/tipo/expr.rs index 3a29539a..036a166f 100644 --- a/crates/aiken-lang/src/tipo/expr.rs +++ b/crates/aiken-lang/src/tipo/expr.rs @@ -59,6 +59,39 @@ pub(crate) fn infer_function( return_type: _, } = fun; + let mut extra_let_assignments = Vec::new(); + for (i, arg) in arguments.iter().enumerate() { + let let_assignment = arg.by.clone().into_extra_assignment( + &arg.arg_name(i), + arg.annotation.as_ref(), + arg.location, + ); + match let_assignment { + None => {} + Some(expr) => extra_let_assignments.push(expr), + } + } + + let sequence; + + let body = if extra_let_assignments.is_empty() { + body + } else if let UntypedExpr::Sequence { expressions, .. } = body { + extra_let_assignments.extend(expressions.clone()); + sequence = UntypedExpr::Sequence { + expressions: extra_let_assignments, + location: *location, + }; + &sequence + } else { + extra_let_assignments.extend([body.clone()]); + sequence = UntypedExpr::Sequence { + expressions: extra_let_assignments, + location: body.location(), + }; + &sequence + }; + let preregistered_fn = environment .get_variable(name) .expect("Could not find preregistered type for function"); @@ -331,9 +364,13 @@ impl<'a, 'b> ExprTyper<'a, 'b> { let mut arguments = Vec::new(); + let mut extra_let_assignments = Vec::new(); for (i, arg) in args.into_iter().enumerate() { - let arg = self.infer_param(arg, expected_args.get(i).cloned(), i)?; - + let (arg, extra_let_assignment) = + self.infer_param(arg, expected_args.get(i).cloned(), i)?; + if let Some(expr) = extra_let_assignment { + extra_let_assignments.push(expr); + } arguments.push(arg); } @@ -342,6 +379,28 @@ impl<'a, 'b> ExprTyper<'a, 'b> { None => None, }; + let body_location = body.location(); + + let body = if extra_let_assignments.is_empty() { + body + } else if let UntypedExpr::Sequence { + location, + expressions, + } = body + { + extra_let_assignments.extend(expressions); + UntypedExpr::Sequence { + expressions: extra_let_assignments, + location, + } + } else { + extra_let_assignments.extend([body]); + UntypedExpr::Sequence { + expressions: extra_let_assignments, + location: body_location, + } + }; + self.infer_fn_with_known_types(arguments, body, return_type) } @@ -1071,13 +1130,12 @@ impl<'a, 'b> ExprTyper<'a, 'b> { }) } - // TODO: Handle arg pattern fn infer_param( &mut self, untyped_arg: UntypedArg, expected: Option>, ix: usize, - ) -> Result { + ) -> Result<(TypedArg, Option), Error> { let arg_name = untyped_arg.arg_name(ix); let UntypedArg { @@ -1102,14 +1160,18 @@ impl<'a, 'b> ExprTyper<'a, 'b> { self.unify(expected, tipo.clone(), location, false)?; } - Ok(TypedArg { + let extra_assignment = by.into_extra_assignment(&arg_name, annotation.as_ref(), location); + + let typed_arg = TypedArg { arg_name, location, annotation, tipo, is_validator_param, doc, - }) + }; + + Ok((typed_arg, extra_assignment)) } fn infer_assignment( diff --git a/crates/aiken-project/src/test_framework.rs b/crates/aiken-project/src/test_framework.rs index d42a39c0..9c7b3e80 100644 --- a/crates/aiken-project/src/test_framework.rs +++ b/crates/aiken-project/src/test_framework.rs @@ -507,10 +507,8 @@ impl Prng { fn as_prng(cst: &PlutusData) -> Prng { if let PlutusData::Constr(Constr { tag, fields, .. }) = cst { if *tag == 121 + Prng::SEEDED { - if let [ - PlutusData::BoundedBytes(bytes), - PlutusData::BoundedBytes(choices), - ] = &fields[..] + if let [PlutusData::BoundedBytes(bytes), PlutusData::BoundedBytes(choices)] = + &fields[..] { return Prng::Seeded { choices: choices.to_vec(), @@ -1089,11 +1087,9 @@ impl TryFrom for Assertion { final_else, .. } => { - if let [ - IfBranch { - condition, body, .. - }, - ] = &branches[..] + if let [IfBranch { + condition, body, .. + }] = &branches[..] { let then_is_true = match body { TypedExpr::Var { @@ -1512,14 +1508,13 @@ mod test { } "#}); - assert!( - prop.run::<()>( + assert!(prop + .run::<()>( 42, PropertyTest::DEFAULT_MAX_SUCCESS, &PlutusVersion::default() ) - .is_success() - ); + .is_success()); } #[test] diff --git a/examples/acceptance_tests/104/aiken.lock b/examples/acceptance_tests/104/aiken.lock new file mode 100644 index 00000000..7319f59c --- /dev/null +++ b/examples/acceptance_tests/104/aiken.lock @@ -0,0 +1,28 @@ +# This file was generated by Aiken +# You typically do not need to edit this file + +[[requirements]] +name = "aiken-lang/stdlib" +version = "main" +source = "github" + +[[requirements]] +name = "aiken-lang/fuzz" +version = "main" +source = "github" + +[[packages]] +name = "aiken-lang/stdlib" +version = "main" +requirements = [] +source = "github" + +[[packages]] +name = "aiken-lang/fuzz" +version = "main" +requirements = [] +source = "github" + +[etags] +"aiken-lang/fuzz@main" = [{ secs_since_epoch = 1717767691, nanos_since_epoch = 206091000 }, "98cf81aa68f9ccf68bc5aba9be06d06cb1db6e8eff60b668ed5e8ddf3588206b"] +"aiken-lang/stdlib@main" = [{ secs_since_epoch = 1717767690, nanos_since_epoch = 920449000 }, "a746f5b5cd3c2ca5dc19c43bcfc64230c546fafea2ba5f8e340c227b85886078"] diff --git a/examples/acceptance_tests/104/aiken.toml b/examples/acceptance_tests/104/aiken.toml new file mode 100644 index 00000000..9144ac68 --- /dev/null +++ b/examples/acceptance_tests/104/aiken.toml @@ -0,0 +1,21 @@ +name = "aiken-lang/104" +version = "0.0.0" +compiler = "v1.0.29-alpha" +plutus = "v2" +license = "Apache-2.0" +description = "Aiken contracts for project 'aiken-lang/104'" + +[repository] +user = "aiken-lang" +project = "104" +platform = "github" + +[[dependencies]] +name = "aiken-lang/stdlib" +version = "main" +source = "github" + +[[dependencies]] +name = "aiken-lang/fuzz" +version = "main" +source = "github" diff --git a/examples/acceptance_tests/104/plutus.json b/examples/acceptance_tests/104/plutus.json new file mode 100644 index 00000000..e2c093d5 --- /dev/null +++ b/examples/acceptance_tests/104/plutus.json @@ -0,0 +1,80 @@ +{ + "preamble": { + "title": "aiken-lang/104", + "description": "Aiken contracts for project 'aiken-lang/104'", + "version": "0.0.0", + "plutusVersion": "v2", + "compiler": { + "name": "Aiken", + "version": "v1.0.29-alpha+257bd23" + }, + "license": "Apache-2.0" + }, + "validators": [ + { + "title": "tests.foo_3", + "redeemer": { + "title": "_data", + "schema": { + "$ref": "#/definitions/Data" + } + }, + "parameters": [ + { + "title": "th_arg", + "schema": { + "$ref": "#/definitions/tests~1Foo" + } + } + ], + "compiledCode": "582401000032323222253330043370e6eb4c018c014dd5001a400429309b2b2b9a5573cae841", + "hash": "047dafbc61fb4a550a28398bde3680c48ff2000cf1022efc883124cd" + } + ], + "definitions": { + "Bool": { + "title": "Bool", + "anyOf": [ + { + "title": "False", + "dataType": "constructor", + "index": 0, + "fields": [] + }, + { + "title": "True", + "dataType": "constructor", + "index": 1, + "fields": [] + } + ] + }, + "Data": { + "title": "Data", + "description": "Any Plutus data." + }, + "Int": { + "dataType": "integer" + }, + "tests/Foo": { + "title": "Foo", + "anyOf": [ + { + "title": "Foo", + "dataType": "constructor", + "index": 0, + "fields": [ + { + "title": "a0", + "$ref": "#/definitions/Int" + }, + { + "title": "a1", + "$ref": "#/definitions/Bool" + } + ] + } + ] + } + } +} \ No newline at end of file diff --git a/examples/acceptance_tests/104/validators/tests.ak b/examples/acceptance_tests/104/validators/tests.ak new file mode 100644 index 00000000..f8f3582e --- /dev/null +++ b/examples/acceptance_tests/104/validators/tests.ak @@ -0,0 +1,45 @@ +use aiken/fuzz + +type Foo { + a0: Int, + a1: Bool, +} + +fn foo_1(Foo { a0, .. }) -> Int { + a0 + 1 +} + +fn foo_2(Foo { a0, a1 } as foo) -> Int { + if a1 { + a0 + 1 + } else { + foo.a0 - 1 + } +} + +validator(Foo { a0, .. }: Foo) { + fn foo_3(_data, _redeemer) { + a0 == 1 + } +} + +test example_1() { + foo_1(Foo { a0: 1, a1: False }) == 2 +} + +test example_2() { + foo_2(Foo { a0: 1, a1: False }) == 0 +} + +test example_3() { + foo_3(Foo { a0: 1, a1: False }, "", "") +} + +test example_4() { + let foo_4 = fn(Foo { a1, .. }) { a1 } + foo_4(Foo { a0: 1, a1: True }) +} + +test example_5((a, b) via fuzz.both(fuzz.int(), fuzz.int())) { + a + b == b + a +}