From 222d244bcf64ae8b1492d84b2474a663f4294329 Mon Sep 17 00:00:00 2001 From: KtorZ Date: Sat, 1 Mar 2025 16:58:41 +0100 Subject: [PATCH 1/4] Do not allow empty continuation for backpassed assignments Fixes #1111. Signed-off-by: KtorZ --- crates/aiken-lang/src/tests/check.rs | 23 +++++++++++++++++++++++ crates/aiken-lang/src/tipo/expr.rs | 28 +++++++++++++++++++--------- 2 files changed, 42 insertions(+), 9 deletions(-) diff --git a/crates/aiken-lang/src/tests/check.rs b/crates/aiken-lang/src/tests/check.rs index cc70b408..8655bb8d 100644 --- a/crates/aiken-lang/src/tests/check.rs +++ b/crates/aiken-lang/src/tests/check.rs @@ -33,6 +33,7 @@ fn check_module( for (package, module) in extra { let mut warnings = vec![]; + let typed_module = module .infer( &id_gen, @@ -3309,6 +3310,28 @@ fn softcasting_unused_let_binding() { assert!(warnings.is_empty(), "should not contain any warnings"); } +#[test] +fn dangling_let_in_block() { + let source_code = r#" + fn for_each(xs: List, with: fn(Int) -> a) -> a { + todo + } + + test foo() { + { + let _ <- for_each([1, 2, 3]) + } + } + "#; + + let result = check_validator(parse(source_code)); + + assert!( + matches!(result, Err((_, Error::LastExpressionIsAssignment { .. }))), + "{result:?}" + ) +} + #[test] fn dangling_trace_let_standalone() { let source_code = r#" diff --git a/crates/aiken-lang/src/tipo/expr.rs b/crates/aiken-lang/src/tipo/expr.rs index 5064da62..4c9d959e 100644 --- a/crates/aiken-lang/src/tipo/expr.rs +++ b/crates/aiken-lang/src/tipo/expr.rs @@ -1970,11 +1970,12 @@ impl<'a, 'b> ExprTyper<'a, 'b> { PipeTyper::infer(self, expressions) } + #[allow(clippy::result_large_err)] fn backpass( &mut self, breakpoint: UntypedExpr, mut continuation: Vec, - ) -> UntypedExpr { + ) -> Result { let UntypedExpr::Assignment { location, value, @@ -1985,6 +1986,15 @@ impl<'a, 'b> ExprTyper<'a, 'b> { unreachable!("backpass misuse: breakpoint isn't an Assignment ?!"); }; + if continuation.is_empty() { + return Err(Error::LastExpressionIsAssignment { + location, + expr: *value, + patterns: patterns.clone(), + kind, + }); + } + let value_location = value.location(); let call_location = Span { @@ -2101,11 +2111,11 @@ impl<'a, 'b> ExprTyper<'a, 'b> { value: UntypedExpr::lambda(names, continuation, lambda_span), }); - UntypedExpr::Call { + Ok(UntypedExpr::Call { location: call_location, fun, arguments: new_arguments, - } + }) } // This typically occurs on function captures. We do not try to assert anything on the @@ -2136,15 +2146,15 @@ impl<'a, 'b> ExprTyper<'a, 'b> { }; if arguments.is_empty() { - call + Ok(call) } else { - UntypedExpr::Fn { + Ok(UntypedExpr::Fn { location: call_location, fn_style, arguments, body: call.into(), return_annotation, - } + }) } } @@ -2152,7 +2162,7 @@ impl<'a, 'b> ExprTyper<'a, 'b> { // with our continuation. If the expression isn't callable? No problem, the // type-checker will catch that eventually in exactly the same way as if the code was // written like that to begin with. - _ => UntypedExpr::Call { + _ => Ok(UntypedExpr::Call { location: call_location, fun: value, arguments: vec![CallArg { @@ -2160,7 +2170,7 @@ impl<'a, 'b> ExprTyper<'a, 'b> { label: None, value: UntypedExpr::lambda(names, continuation, lambda_span), }], - }, + }), } } @@ -2203,7 +2213,7 @@ impl<'a, 'b> ExprTyper<'a, 'b> { } if let Some(breakpoint) = breakpoint { - prefix.push(self.backpass(breakpoint, suffix)); + prefix.push(self.backpass(breakpoint, suffix)?); return self.infer_seq(location, prefix); } From 9b8137c056553b55b1b50490f9a9494c44d9c9d3 Mon Sep 17 00:00:00 2001 From: KtorZ Date: Sat, 1 Mar 2025 16:59:22 +0100 Subject: [PATCH 2/4] Better trace parsing + default behaviour. Somehow, we allow traces with no continuation after and currently default to a Todo. That todo is completely invisible from a user standpoint and return a generic `a`. So it is pretty easy for someone to think their program is okay, compiles, and have no issues, while simply crashing at runtime because of an invisible todo. Hence, I've changed that to default to `Void` instead, which is more sensible as a default for an empty trace. Also, I've made the parser fail with one puts a colon for label, but doesn't add any value to display. Fixes #1113. Signed-off-by: KtorZ --- .../src/parser/expr/fail_todo_trace.rs | 21 +++++- .../expr/snapshots/trace_dangling_colons.snap | 72 +++++++++++++++++++ .../expr/snapshots/trace_expr_todo.snap | 12 +--- .../parser/expr/snapshots/trace_labelled.snap | 12 +--- .../parser/expr/snapshots/trace_variadic.snap | 12 +--- crates/aiken-lang/src/tests/check.rs | 16 +++++ 6 files changed, 112 insertions(+), 33 deletions(-) create mode 100644 crates/aiken-lang/src/parser/expr/snapshots/trace_dangling_colons.snap diff --git a/crates/aiken-lang/src/parser/expr/fail_todo_trace.rs b/crates/aiken-lang/src/parser/expr/fail_todo_trace.rs index aed2d6f3..2a1180a6 100644 --- a/crates/aiken-lang/src/parser/expr/fail_todo_trace.rs +++ b/crates/aiken-lang/src/parser/expr/fail_todo_trace.rs @@ -1,5 +1,5 @@ use crate::{ - ast::TraceKind, + ast::{well_known, TraceKind}, expr::UntypedExpr, parser::{ error::{ParseError, Pattern}, @@ -32,7 +32,8 @@ pub fn parser<'a>( choice((just(Token::Colon), just(Token::Comma))) .then( choice((string::hybrid(), expression.clone())) - .separated_by(just(Token::Comma)), + .separated_by(just(Token::Comma)) + .at_least(1), ) .validate(|(token, arguments), span, emit| { if token != Token::Colon { @@ -53,7 +54,10 @@ pub fn parser<'a>( |((label, arguments), continuation), span| UntypedExpr::Trace { kind: TraceKind::Trace, location: span, - then: Box::new(continuation.unwrap_or_else(|| UntypedExpr::todo(None, span))), + then: Box::new(continuation.unwrap_or_else(|| UntypedExpr::Var { + location: span, + name: well_known::VOID.to_string(), + })), label: Box::new(label), arguments, }, @@ -193,4 +197,15 @@ mod tests { "# ); } + + #[test] + fn trace_dangling_colons() { + assert_expr!( + r#" + let debug = fn() { + trace "foo": + } + "# + ); + } } diff --git a/crates/aiken-lang/src/parser/expr/snapshots/trace_dangling_colons.snap b/crates/aiken-lang/src/parser/expr/snapshots/trace_dangling_colons.snap new file mode 100644 index 00000000..87a82c3e --- /dev/null +++ b/crates/aiken-lang/src/parser/expr/snapshots/trace_dangling_colons.snap @@ -0,0 +1,72 @@ +--- +source: crates/aiken-lang/src/parser/expr/fail_todo_trace.rs +description: "Invalid code (parse error):\n\nlet debug = fn() {\n trace \"foo\":\n}\n" +--- +[ + ParseError { + kind: Unexpected( + Token( + RightBrace, + ), + ), + span: 34..35, + while_parsing: None, + expected: { + Token( + If, + ), + Token( + Expect, + ), + Token( + Minus, + ), + Token( + When, + ), + Token( + And, + ), + Token( + Or, + ), + Token( + LeftParen, + ), + Token( + Todo, + ), + Token( + LeftSquare, + ), + Token( + NewLineMinus, + ), + Token( + Bang, + ), + Token( + Fail, + ), + Token( + NewLineLeftParen, + ), + Token( + LeftBrace, + ), + Token( + Trace, + ), + Token( + Let, + ), + Token( + Hash, + ), + Token( + Fn, + ), + }, + label: None, + }, +] diff --git a/crates/aiken-lang/src/parser/expr/snapshots/trace_expr_todo.snap b/crates/aiken-lang/src/parser/expr/snapshots/trace_expr_todo.snap index ae65d4df..32b3ea26 100644 --- a/crates/aiken-lang/src/parser/expr/snapshots/trace_expr_todo.snap +++ b/crates/aiken-lang/src/parser/expr/snapshots/trace_expr_todo.snap @@ -5,17 +5,9 @@ description: "Code:\n\ntrace some_var\n" Trace { kind: Trace, location: 0..14, - then: Trace { - kind: Todo, + then: Var { location: 0..14, - then: ErrorTerm { - location: 0..14, - }, - label: String { - location: 0..14, - value: "aiken::todo", - }, - arguments: [], + name: "Void", }, label: Var { location: 6..14, diff --git a/crates/aiken-lang/src/parser/expr/snapshots/trace_labelled.snap b/crates/aiken-lang/src/parser/expr/snapshots/trace_labelled.snap index 3af8fa07..2deadaf9 100644 --- a/crates/aiken-lang/src/parser/expr/snapshots/trace_labelled.snap +++ b/crates/aiken-lang/src/parser/expr/snapshots/trace_labelled.snap @@ -5,17 +5,9 @@ description: "Code:\n\ntrace foo: \"bar\"\n" Trace { kind: Trace, location: 0..16, - then: Trace { - kind: Todo, + then: Var { location: 0..16, - then: ErrorTerm { - location: 0..16, - }, - label: String { - location: 0..16, - value: "aiken::todo", - }, - arguments: [], + name: "Void", }, label: Var { location: 6..9, diff --git a/crates/aiken-lang/src/parser/expr/snapshots/trace_variadic.snap b/crates/aiken-lang/src/parser/expr/snapshots/trace_variadic.snap index 71ee7150..721f7493 100644 --- a/crates/aiken-lang/src/parser/expr/snapshots/trace_variadic.snap +++ b/crates/aiken-lang/src/parser/expr/snapshots/trace_variadic.snap @@ -5,17 +5,9 @@ description: "Code:\n\ntrace \"foo\": @\"bar\", baz\n" Trace { kind: Trace, location: 0..24, - then: Trace { - kind: Todo, + then: Var { location: 0..24, - then: ErrorTerm { - location: 0..24, - }, - label: String { - location: 0..24, - value: "aiken::todo", - }, - arguments: [], + name: "Void", }, label: String { location: 6..11, diff --git a/crates/aiken-lang/src/tests/check.rs b/crates/aiken-lang/src/tests/check.rs index 8655bb8d..9410b801 100644 --- a/crates/aiken-lang/src/tests/check.rs +++ b/crates/aiken-lang/src/tests/check.rs @@ -3332,6 +3332,22 @@ fn dangling_let_in_block() { ) } +#[test] +fn default_trace_return() { + let source_code = r#" + fn debug() { + trace @"patate": Void + } + + test foo() { + debug() + True + } + "#; + + assert!(matches!(check_validator(parse(source_code)), Ok(..))) +} + #[test] fn dangling_trace_let_standalone() { let source_code = r#" From eadbc60a723e4c2d54e0786672d354136f7c7277 Mon Sep 17 00:00:00 2001 From: KtorZ Date: Sun, 2 Mar 2025 12:59:54 +0100 Subject: [PATCH 3/4] Fill-in changelog. Signed-off-by: KtorZ --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9af4af67..f29ed911 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## v1.1.14 - UNRELEASED + +### Changed + +- **aiken-lang**: Prevent (type error) backpassing blocks with empty continuation. See [#1111](https://github.com/aiken-lang/aiken/issues/1111). @KtorZ +- **aiken-lang**: Change default placeholder for `trace` to `Void` instead of `todo`. @KtorZ +- **aiken-lang**: Disallow (parse error) dangling colon `:` in traces. See [#1113](https://github.com/aiken-lang/aiken/issues/1113). @KtorZ + ## v1.1.13 - 2025-02-26 ### Added From 862fc490c14058cf0381d0e8451a7f456397df7b Mon Sep 17 00:00:00 2001 From: KtorZ Date: Sun, 2 Mar 2025 13:45:05 +0100 Subject: [PATCH 4/4] Use error's description for snapshot assertions + sort reported expected tokens Turns out the snapshot test would randomly fail because the expected tokens are a HashSet, which is unordered by construction. So, using the auto-derived `Debug` instance to compare it with snapshot would only succeed if the tokens happened to be in the same order in the HashSet, which only happens by accident. Signed-off-by: KtorZ --- crates/aiken-lang/src/parser/error.rs | 4 +- .../expr/snapshots/expect_unfinished_let.snap | 12 +--- .../expr/snapshots/trace_dangling_colons.snap | 69 +------------------ .../snapshots/when_guard_deprecation.snap | 12 +--- .../pattern_bytearray_g1_element.snap | 12 +--- .../pattern_bytearray_g2_element.snap | 12 +--- .../pattern/snapshots/pattern_string.snap | 12 +--- crates/aiken-lang/src/parser/token.rs | 6 +- crates/aiken-lang/src/parser/utils.rs | 2 +- 9 files changed, 14 insertions(+), 127 deletions(-) diff --git a/crates/aiken-lang/src/parser/error.rs b/crates/aiken-lang/src/parser/error.rs index 3fed3993..6434ef23 100644 --- a/crates/aiken-lang/src/parser/error.rs +++ b/crates/aiken-lang/src/parser/error.rs @@ -3,6 +3,7 @@ use crate::{ parser::token::Token, }; use indoc::formatdoc; +use itertools::Itertools; use miette::Diagnostic; use owo_colors::{OwoColorize, Stream::Stdout}; use std::collections::HashSet; @@ -18,6 +19,7 @@ use std::collections::HashSet; "I am looking for one of the following patterns:\n{}", expected .iter() + .sorted() .map(|x| format!( "→ {}", x.to_aiken() @@ -320,7 +322,7 @@ fn fmt_unknown_curve(curve: &String, point: &Option) -> String { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash, Diagnostic, thiserror::Error)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Diagnostic, thiserror::Error)] pub enum Pattern { #[error("I found an unexpected char '{0:?}'.")] #[diagnostic(help("Try removing it!"))] 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 index 2a9b3c48..7efd9f10 100644 --- a/crates/aiken-lang/src/parser/expr/snapshots/expect_unfinished_let.snap +++ b/crates/aiken-lang/src/parser/expr/snapshots/expect_unfinished_let.snap @@ -2,14 +2,4 @@ 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", - ), - }, -] +I spotted an unfinished assignment. diff --git a/crates/aiken-lang/src/parser/expr/snapshots/trace_dangling_colons.snap b/crates/aiken-lang/src/parser/expr/snapshots/trace_dangling_colons.snap index 87a82c3e..0a59ac69 100644 --- a/crates/aiken-lang/src/parser/expr/snapshots/trace_dangling_colons.snap +++ b/crates/aiken-lang/src/parser/expr/snapshots/trace_dangling_colons.snap @@ -2,71 +2,4 @@ source: crates/aiken-lang/src/parser/expr/fail_todo_trace.rs description: "Invalid code (parse error):\n\nlet debug = fn() {\n trace \"foo\":\n}\n" --- -[ - ParseError { - kind: Unexpected( - Token( - RightBrace, - ), - ), - span: 34..35, - while_parsing: None, - expected: { - Token( - If, - ), - Token( - Expect, - ), - Token( - Minus, - ), - Token( - When, - ), - Token( - And, - ), - Token( - Or, - ), - Token( - LeftParen, - ), - Token( - Todo, - ), - Token( - LeftSquare, - ), - Token( - NewLineMinus, - ), - Token( - Bang, - ), - Token( - Fail, - ), - Token( - NewLineLeftParen, - ), - Token( - LeftBrace, - ), - Token( - Trace, - ), - Token( - Let, - ), - Token( - Hash, - ), - Token( - Fn, - ), - }, - label: None, - }, -] +I found an unexpected token '}'. diff --git a/crates/aiken-lang/src/parser/expr/when/snapshots/when_guard_deprecation.snap b/crates/aiken-lang/src/parser/expr/when/snapshots/when_guard_deprecation.snap index 086d4032..56f52c57 100644 --- a/crates/aiken-lang/src/parser/expr/when/snapshots/when_guard_deprecation.snap +++ b/crates/aiken-lang/src/parser/expr/when/snapshots/when_guard_deprecation.snap @@ -2,14 +2,4 @@ source: crates/aiken-lang/src/parser/expr/when/mod.rs description: "Invalid code (parse error):\n\nwhen a is {\n 2 if x > 1 -> 3\n _ -> 1\n}\n" --- -[ - ParseError { - kind: DeprecatedWhenClause, - span: 14..29, - while_parsing: None, - expected: {}, - label: Some( - "deprecated", - ), - }, -] +I found a now-deprecated clause guard in a when/is expression. diff --git a/crates/aiken-lang/src/parser/pattern/snapshots/pattern_bytearray_g1_element.snap b/crates/aiken-lang/src/parser/pattern/snapshots/pattern_bytearray_g1_element.snap index d7b02650..bd0124f3 100644 --- a/crates/aiken-lang/src/parser/pattern/snapshots/pattern_bytearray_g1_element.snap +++ b/crates/aiken-lang/src/parser/pattern/snapshots/pattern_bytearray_g1_element.snap @@ -2,14 +2,4 @@ source: crates/aiken-lang/src/parser/pattern/bytearray.rs description: "Invalid code (parse error):\n\nwhen foo is {\n #\"950dfd33da2682260c76038dfb8bad6e84ae9d599a3c151815945ac1e6ef6b1027cd917f3907479d20d636ce437a41f5\" -> False\n _ -> True\n}\n" --- -[ - ParseError { - kind: PatternMatchOnCurvePoint, - span: 18..132, - while_parsing: None, - expected: {}, - label: Some( - "cannot pattern-match on curve point", - ), - }, -] +I choked on a curve point in a bytearray pattern. diff --git a/crates/aiken-lang/src/parser/pattern/snapshots/pattern_bytearray_g2_element.snap b/crates/aiken-lang/src/parser/pattern/snapshots/pattern_bytearray_g2_element.snap index bd7ec372..f5f30e2a 100644 --- a/crates/aiken-lang/src/parser/pattern/snapshots/pattern_bytearray_g2_element.snap +++ b/crates/aiken-lang/src/parser/pattern/snapshots/pattern_bytearray_g2_element.snap @@ -2,14 +2,4 @@ source: crates/aiken-lang/src/parser/pattern/bytearray.rs description: "Invalid code (parse error):\n\nwhen foo is {\n #\"b0629fa1158c2d23a10413fe91d381a84d25e31d041cd0377d25828498fd02011b35893938ced97535395e4815201e67108bcd4665e0db25d602d76fa791fab706c54abf5e1a9e44b4ac1e6badf3d2ac0328f5e30be341677c8bac5dda7682f1\" -> False\n _ -> True\n}\n" --- -[ - ParseError { - kind: PatternMatchOnCurvePoint, - span: 18..228, - while_parsing: None, - expected: {}, - label: Some( - "cannot pattern-match on curve point", - ), - }, -] +I choked on a curve point in a bytearray pattern. diff --git a/crates/aiken-lang/src/parser/pattern/snapshots/pattern_string.snap b/crates/aiken-lang/src/parser/pattern/snapshots/pattern_string.snap index fcd55431..29a9c728 100644 --- a/crates/aiken-lang/src/parser/pattern/snapshots/pattern_string.snap +++ b/crates/aiken-lang/src/parser/pattern/snapshots/pattern_string.snap @@ -2,14 +2,4 @@ source: crates/aiken-lang/src/parser/pattern/string.rs description: "Invalid code (parse error):\n\nwhen foo is {\n @\"foo\" -> True\n}\n" --- -[ - ParseError { - kind: PatternMatchOnString, - span: 16..22, - while_parsing: None, - expected: {}, - label: Some( - "cannot pattern-match on string", - ), - }, -] +I refuse to cooperate and match a utf-8 string. diff --git a/crates/aiken-lang/src/parser/token.rs b/crates/aiken-lang/src/parser/token.rs index 4c928e32..9ec82f2e 100644 --- a/crates/aiken-lang/src/parser/token.rs +++ b/crates/aiken-lang/src/parser/token.rs @@ -1,12 +1,14 @@ use std::fmt; -#[derive(Clone, Debug, PartialEq, Hash, Eq, Copy, serde::Serialize, serde::Deserialize)] +#[derive( + Clone, Debug, PartialEq, PartialOrd, Ord, Hash, Eq, Copy, serde::Serialize, serde::Deserialize, +)] pub enum Base { Decimal { numeric_underscore: bool }, Hexadecimal, } -#[derive(Clone, Debug, PartialEq, Hash, Eq)] +#[derive(Clone, Debug, PartialEq, PartialOrd, Ord, Hash, Eq)] pub enum Token { Error(char), Name { name: String }, diff --git a/crates/aiken-lang/src/parser/utils.rs b/crates/aiken-lang/src/parser/utils.rs index 7849e651..d0d4cfba 100644 --- a/crates/aiken-lang/src/parser/utils.rs +++ b/crates/aiken-lang/src/parser/utils.rs @@ -45,7 +45,7 @@ macro_rules! assert_expr { prepend_module_to_snapshot => false, omit_expression => true }, { - insta::assert_debug_snapshot!(err); + insta::assert_snapshot!(err.into_iter().map(|e| e.to_string()).collect::>().join("\n")); }) } }