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); }