diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f91db35..0999f2f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,10 @@ ### Fixed - **aiken-lang**: Fix flat encoding and decoding of large integer values. @KtorZ +- **aiken-lang**: Traces should not have following expressions formatted into a block. @rvcas +- **aiken-lang**: Sequences should not be erased if the sole expression is an assignment. @rvcas +- **aiken-lang**: Should not be able to assign an assignment to an assignment. @rvcas +- **aiken-lang**: Should not be able to have an assignment in a logical op chain. @rvcas - **aiken**: Ensures that test expected to fail that return `False` are considered to pass & improve error reporting when they fail. @KtorZ ### Removed diff --git a/crates/aiken-lang/src/parser/definition/function.rs b/crates/aiken-lang/src/parser/definition/function.rs index 639f187a..92d8af38 100644 --- a/crates/aiken-lang/src/parser/definition/function.rs +++ b/crates/aiken-lang/src/parser/definition/function.rs @@ -109,4 +109,15 @@ mod tests { "# ); } + + #[test] + fn function_assignment_only() { + assert_definition!( + r#" + fn run() { + let x = 1 + 1 + } + "# + ); + } } diff --git a/crates/aiken-lang/src/parser/definition/snapshots/function_assignment_only.snap b/crates/aiken-lang/src/parser/definition/snapshots/function_assignment_only.snap new file mode 100644 index 00000000..23f7613e --- /dev/null +++ b/crates/aiken-lang/src/parser/definition/snapshots/function_assignment_only.snap @@ -0,0 +1,44 @@ +--- +source: crates/aiken-lang/src/parser/definition/function.rs +description: "Code:\n\nfn run() {\n let x = 1 + 1\n}\n" +--- +Fn( + Function { + arguments: [], + body: Assignment { + location: 13..26, + value: BinOp { + location: 21..26, + name: AddInt, + left: UInt { + location: 21..22, + value: "1", + base: Decimal { + numeric_underscore: false, + }, + }, + right: UInt { + location: 25..26, + value: "1", + base: Decimal { + numeric_underscore: false, + }, + }, + }, + pattern: Var { + location: 17..18, + name: "x", + }, + kind: Let, + annotation: None, + }, + doc: None, + location: 0..8, + name: "run", + public: false, + return_annotation: None, + return_type: (), + end_position: 27, + can_error: true, + }, +) diff --git a/crates/aiken-lang/src/parser/error.rs b/crates/aiken-lang/src/parser/error.rs index 01be9ca2..0212e042 100644 --- a/crates/aiken-lang/src/parser/error.rs +++ b/crates/aiken-lang/src/parser/error.rs @@ -28,6 +28,16 @@ impl ParseError { self } + pub fn invalid_assignment_right_hand_side(span: Span) -> Self { + Self { + kind: ErrorKind::UnfinishedAssignmentRightHandSide, + span, + while_parsing: None, + expected: HashSet::new(), + label: Some("invalid assignment right-hand side"), + } + } + pub fn invalid_tuple_index(span: Span, index: String, suffix: Option) -> Self { let hint = suffix.map(|suffix| format!("Did you mean '{index}{suffix}'?")); Self { @@ -163,6 +173,16 @@ pub enum ErrorKind { hint: Option, }, + #[error("I spotted an unfinished assignment.")] + #[diagnostic( + help( + "{} and {} bindings must be followed by a valid, complete, expression.", + "let".if_supports_color(Stdout, |s| s.yellow()), + "expect".if_supports_color(Stdout, |s| s.yellow()), + ), + )] + UnfinishedAssignmentRightHandSide, + #[error("I tripped over a {}", fmt_curve_type(.curve))] PointNotOnCurve { curve: CurveType }, diff --git a/crates/aiken-lang/src/parser/expr/assignment.rs b/crates/aiken-lang/src/parser/expr/assignment.rs index 7525c569..30f8fbf5 100644 --- a/crates/aiken-lang/src/parser/expr/assignment.rs +++ b/crates/aiken-lang/src/parser/expr/assignment.rs @@ -14,15 +14,19 @@ pub fn let_( .then(just(Token::Colon).ignore_then(annotation()).or_not()) .then_ignore(just(Token::Equal)) .then(r.clone()) - .map_with_span( - move |((pattern, annotation), value), span| UntypedExpr::Assignment { + .validate(move |((pattern, annotation), value), span, emit| { + if matches!(value, UntypedExpr::Assignment { .. }) { + emit(ParseError::invalid_assignment_right_hand_side(span)) + } + + UntypedExpr::Assignment { location: span, value: Box::new(value), pattern, kind: ast::AssignmentKind::Let, annotation, - }, - ) + } + }) } pub fn expect( @@ -36,7 +40,7 @@ pub fn expect( .or_not(), ) .then(r.clone()) - .map_with_span(move |(opt_pattern, value), span| { + .validate(move |(opt_pattern, value), span, emit| { let (pattern, annotation) = opt_pattern.unwrap_or_else(|| { ( ast::UntypedPattern::Constructor { @@ -53,6 +57,10 @@ pub fn expect( ) }); + if matches!(value, UntypedExpr::Assignment { .. }) { + emit(ParseError::invalid_assignment_right_hand_side(span)) + } + UntypedExpr::Assignment { location: span, value: Box::new(value), @@ -86,4 +94,42 @@ mod tests { fn expect_trace_if_false() { assert_expr!("expect foo?"); } + + #[test] + fn expect_unfinished_let() { + assert_expr!( + " + let a = + // foo + let b = 42 + " + ); + } + + #[test] + fn expect_let_in_let() { + assert_expr!("let a = { let b = 42 }"); + } + + #[test] + fn expect_let_in_let_return() { + assert_expr!( + " + let a = { + let b = 42 + b + } + " + ); + } + + #[test] + fn expect_let_in_let_parens() { + assert_expr!("let a = ( let b = 42 )"); + } + + #[test] + fn expect_expect_let() { + assert_expr!("expect { let a = 42 } = foo"); + } } diff --git a/crates/aiken-lang/src/parser/expr/block.rs b/crates/aiken-lang/src/parser/expr/block.rs index 1d4634a9..ac1db7c8 100644 --- a/crates/aiken-lang/src/parser/expr/block.rs +++ b/crates/aiken-lang/src/parser/expr/block.rs @@ -17,6 +17,16 @@ pub fn parser( just(Token::RightParen), ), )) + .map_with_span(|e, span| { + if matches!(e, UntypedExpr::Assignment { .. }) { + UntypedExpr::Sequence { + location: span, + expressions: vec![e], + } + } else { + e + } + }) } #[cfg(test)] diff --git a/crates/aiken-lang/src/parser/expr/snapshots/expect_expect_let.snap b/crates/aiken-lang/src/parser/expr/snapshots/expect_expect_let.snap new file mode 100644 index 00000000..2f2b1431 --- /dev/null +++ b/crates/aiken-lang/src/parser/expr/snapshots/expect_expect_let.snap @@ -0,0 +1,40 @@ +--- +source: crates/aiken-lang/src/parser/expr/assignment.rs +description: "Code:\n\nexpect { let a = 42 } = foo" +--- +Assignment { + location: 0..21, + value: Sequence { + location: 7..21, + expressions: [ + Assignment { + location: 9..19, + value: UInt { + location: 17..19, + value: "42", + base: Decimal { + numeric_underscore: false, + }, + }, + pattern: Var { + location: 13..14, + name: "a", + }, + kind: Let, + annotation: None, + }, + ], + }, + pattern: Constructor { + is_record: false, + location: 0..21, + name: "True", + arguments: [], + module: None, + constructor: (), + with_spread: false, + tipo: (), + }, + kind: Expect, + annotation: None, +} diff --git a/crates/aiken-lang/src/parser/expr/snapshots/expect_let_in_let.snap b/crates/aiken-lang/src/parser/expr/snapshots/expect_let_in_let.snap new file mode 100644 index 00000000..feaa148b --- /dev/null +++ b/crates/aiken-lang/src/parser/expr/snapshots/expect_let_in_let.snap @@ -0,0 +1,34 @@ +--- +source: crates/aiken-lang/src/parser/expr/assignment.rs +description: "Code:\n\nlet a = { let b = 42 }" +--- +Assignment { + location: 0..22, + value: Sequence { + location: 8..22, + expressions: [ + Assignment { + location: 10..20, + value: UInt { + location: 18..20, + value: "42", + base: Decimal { + numeric_underscore: false, + }, + }, + pattern: Var { + location: 14..15, + name: "b", + }, + kind: Let, + annotation: None, + }, + ], + }, + pattern: Var { + location: 4..5, + name: "a", + }, + kind: Let, + annotation: None, +} diff --git a/crates/aiken-lang/src/parser/expr/snapshots/expect_let_in_let_parens.snap b/crates/aiken-lang/src/parser/expr/snapshots/expect_let_in_let_parens.snap new file mode 100644 index 00000000..7ca8ace8 --- /dev/null +++ b/crates/aiken-lang/src/parser/expr/snapshots/expect_let_in_let_parens.snap @@ -0,0 +1,34 @@ +--- +source: crates/aiken-lang/src/parser/expr/assignment.rs +description: "Code:\n\nlet a = ( let b = 42 )" +--- +Assignment { + location: 0..22, + value: Sequence { + location: 8..22, + expressions: [ + Assignment { + location: 10..20, + value: UInt { + location: 18..20, + value: "42", + base: Decimal { + numeric_underscore: false, + }, + }, + pattern: Var { + location: 14..15, + name: "b", + }, + kind: Let, + annotation: None, + }, + ], + }, + pattern: Var { + location: 4..5, + name: "a", + }, + kind: Let, + annotation: None, +} diff --git a/crates/aiken-lang/src/parser/expr/snapshots/expect_let_in_let_return.snap b/crates/aiken-lang/src/parser/expr/snapshots/expect_let_in_let_return.snap new file mode 100644 index 00000000..0d11cf5f --- /dev/null +++ b/crates/aiken-lang/src/parser/expr/snapshots/expect_let_in_let_return.snap @@ -0,0 +1,38 @@ +--- +source: crates/aiken-lang/src/parser/expr/assignment.rs +description: "Code:\n\nlet a = {\n let b = 42\n b\n}\n" +--- +Assignment { + location: 0..28, + value: Sequence { + location: 12..26, + expressions: [ + Assignment { + location: 12..22, + value: UInt { + location: 20..22, + value: "42", + base: Decimal { + numeric_underscore: false, + }, + }, + pattern: Var { + location: 16..17, + name: "b", + }, + kind: Let, + annotation: None, + }, + Var { + location: 25..26, + name: "b", + }, + ], + }, + pattern: Var { + location: 4..5, + name: "a", + }, + kind: Let, + annotation: None, +} diff --git a/crates/aiken-lang/src/parser/expr/snapshots/expect_unfinished_let.snap b/crates/aiken-lang/src/parser/expr/snapshots/expect_unfinished_let.snap new file mode 100644 index 00000000..2a9b3c48 --- /dev/null +++ b/crates/aiken-lang/src/parser/expr/snapshots/expect_unfinished_let.snap @@ -0,0 +1,15 @@ +--- +source: crates/aiken-lang/src/parser/expr/assignment.rs +description: "Invalid code (parse error):\n\nlet a =\n// foo\nlet b = 42\n" +--- +[ + ParseError { + kind: UnfinishedAssignmentRightHandSide, + span: 0..25, + while_parsing: None, + expected: {}, + label: Some( + "invalid assignment right-hand side", + ), + }, +] diff --git a/crates/aiken-lang/src/parser/utils.rs b/crates/aiken-lang/src/parser/utils.rs index 75d63e92..08853bd4 100644 --- a/crates/aiken-lang/src/parser/utils.rs +++ b/crates/aiken-lang/src/parser/utils.rs @@ -28,15 +28,28 @@ macro_rules! assert_expr { let stream = chumsky::Stream::from_iter($crate::ast::Span::create(tokens.len(), 1), tokens.into_iter()); - let result = $crate::parser::expr::sequence().parse(stream).unwrap(); + let result = $crate::parser::expr::sequence().parse(stream); - insta::with_settings!({ - description => concat!("Code:\n\n", indoc::indoc! { $code }), - prepend_module_to_snapshot => false, - omit_expression => true - }, { - insta::assert_debug_snapshot!(result); - }); + match result { + Ok(expr) => { + insta::with_settings!({ + description => concat!("Code:\n\n", indoc::indoc! { $code }), + prepend_module_to_snapshot => false, + omit_expression => true + }, { + insta::assert_debug_snapshot!(expr); + }) + }, + Err(err) => { + insta::with_settings!({ + description => concat!("Invalid code (parse error):\n\n", indoc::indoc! { $code }), + prepend_module_to_snapshot => false, + omit_expression => true + }, { + insta::assert_debug_snapshot!(err); + }) + } + } }; } diff --git a/crates/aiken-lang/src/tipo/error.rs b/crates/aiken-lang/src/tipo/error.rs index 8afd2a73..283efc5e 100644 --- a/crates/aiken-lang/src/tipo/error.rs +++ b/crates/aiken-lang/src/tipo/error.rs @@ -407,10 +407,10 @@ Perhaps, try the following: as, expect, check, const, else, fn, if, is, let, opaque, pub, test, todo, trace, type, use, when"#))] KeywordInModuleName { name: String, keyword: String }, - #[error("I discovered a function which is ending with an assignment.\n")] + #[error("I discovered a block which is ending with an assignment.\n")] #[diagnostic(url("https://aiken-lang.org/language-tour/functions#named-functions"))] #[diagnostic(code("illegal::return"))] - #[diagnostic(help(r#"In Aiken, functions must return an explicit result in the form of an expression. While assignments are technically speaking expressions, they aren't allowed to be the last expression of a function because they convey a different meaning and this could be error-prone. + #[diagnostic(help(r#"In Aiken, code blocks (such as function bodies) must return an explicit result in the form of an expression. While assignments are technically speaking expressions, they aren't allowed to be the last expression of a function because they convey a different meaning and this could be error-prone. If you really meant to return that last expression, try to replace it with the following: diff --git a/crates/aiken-lang/src/tipo/expr.rs b/crates/aiken-lang/src/tipo/expr.rs index 693b8257..a3dde5d1 100644 --- a/crates/aiken-lang/src/tipo/expr.rs +++ b/crates/aiken-lang/src/tipo/expr.rs @@ -1650,6 +1650,8 @@ impl<'a, 'b> ExprTyper<'a, 'b> { let mut typed_expressions = vec![]; for expression in expressions { + assert_no_assignment(&expression)?; + let typed_expression = self.infer(expression)?; self.unify( diff --git a/examples/acceptance_tests/091/plutus.json b/examples/acceptance_tests/091/plutus.json deleted file mode 100644 index a6a24507..00000000 --- a/examples/acceptance_tests/091/plutus.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "preamble": { - "title": "aiken-lang/acceptance_test_090", - "version": "0.0.0", - "plutusVersion": "v2", - "compiler": { - "name": "Aiken", - "version": "v1.0.21-alpha+9f263c4" - } - }, - "validators": [ - { - "title": "foo.spend", - "datum": { - "title": "datum", - "schema": { - "$ref": "#/definitions/Int" - } - }, - "redeemer": { - "title": "_redeemer", - "schema": { - "$ref": "#/definitions/Data" - } - }, - "compiledCode": "583f010000322223253330053370e00290487777c9cfdde5c8f27bf4c1637fc55b5eeef7d8c4d9e0d4454967ff7d6e7ee6e242eb60c6318a4c26cac6eb400d5cd1", - "hash": "d18aa035514acb988a34d33fc246420c5b0eca4f3f947ce95e294447" - } - ], - "definitions": { - "Data": { - "title": "Data", - "description": "Any Plutus data." - }, - "Int": { - "dataType": "integer" - } - } -} \ No newline at end of file diff --git a/examples/acceptance_tests/092/aiken.lock b/examples/acceptance_tests/092/aiken.lock new file mode 100644 index 00000000..6e350cda --- /dev/null +++ b/examples/acceptance_tests/092/aiken.lock @@ -0,0 +1,7 @@ +# This file was generated by Aiken +# You typically do not need to edit this file + +requirements = [] +packages = [] + +[etags] diff --git a/examples/acceptance_tests/092/aiken.toml b/examples/acceptance_tests/092/aiken.toml new file mode 100644 index 00000000..e1606480 --- /dev/null +++ b/examples/acceptance_tests/092/aiken.toml @@ -0,0 +1,2 @@ +name = "aiken-lang/acceptance_test_092" +version = "0.0.0" diff --git a/examples/acceptance_tests/092/lib/foo.ak b/examples/acceptance_tests/092/lib/foo.ak new file mode 100644 index 00000000..81ef9e41 --- /dev/null +++ b/examples/acceptance_tests/092/lib/foo.ak @@ -0,0 +1,41 @@ +test foo_1() { + let a = { + let b = 42 + b + } + + a == 42 +} + +test foo_2() { + expect Some(a) = { + let b = 42 + Some(b) + } + + a == 42 +} + +test foo_3() { + let c = Some(42) + + let a = { + expect Some(b) = c + b + } + + a == 42 +} + +test foo_4() { + let a = { + let b = 2 + let c = { + let d = 14 + d * b + } + c + 14 + } + + a == 42 +}