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 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/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/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 new file mode 100644 index 00000000..0a59ac69 --- /dev/null +++ b/crates/aiken-lang/src/parser/expr/snapshots/trace_dangling_colons.snap @@ -0,0 +1,5 @@ +--- +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" +--- +I found an unexpected token '}'. 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/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")); }) } } diff --git a/crates/aiken-lang/src/tests/check.rs b/crates/aiken-lang/src/tests/check.rs index cc70b408..9410b801 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,44 @@ 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 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#" 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); }