diff --git a/CHANGELOG.md b/CHANGELOG.md index 46b76038..3bb20b58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ - **aiken-lang**: disallow `MLResult` in a type definition. @rvcas - **aiken-lang**: reversed deserialization of bls types out of data types. @rvcas - **aiken-lang**: validator args unexpectedly unbound causing code gen crashes. @rvcas +- **aiken-lang**: allow implicitly discarded values when right-hand-side unified with `Void`. @KtorZ ### Changed diff --git a/crates/aiken-lang/src/tests/check.rs b/crates/aiken-lang/src/tests/check.rs index dfa724ad..7812785e 100644 --- a/crates/aiken-lang/src/tests/check.rs +++ b/crates/aiken-lang/src/tests/check.rs @@ -93,6 +93,19 @@ fn validator_illegal_return_type() { )) } +#[test] +fn implicitly_discard_void() { + let source_code = r#" + pub fn label(str: String) -> Void { + trace str Void + } + "#; + + let (warnings, _) = check_validator(parse(source_code)).expect("should type-check"); + + assert!(warnings.is_empty(), "no warnings: {warnings:#?}"); +} + #[test] fn validator_illegal_arity() { let source_code = r#" diff --git a/crates/aiken-lang/src/tipo/expr.rs b/crates/aiken-lang/src/tipo/expr.rs index a528a625..ae16cecb 100644 --- a/crates/aiken-lang/src/tipo/expr.rs +++ b/crates/aiken-lang/src/tipo/expr.rs @@ -9,16 +9,19 @@ use super::{ use crate::{ ast::{ Annotation, Arg, ArgName, AssignmentKind, BinOp, Bls12_381Point, ByteArrayFormatPreference, - CallArg, ClauseGuard, Constant, Curve, IfBranch, LogicalOpChainKind, RecordUpdateSpread, - Span, TraceKind, TraceLevel, Tracing, TypedArg, TypedCallArg, TypedClause, - TypedClauseGuard, TypedIfBranch, TypedPattern, TypedRecordUpdateArg, UnOp, UntypedArg, - UntypedClause, UntypedClauseGuard, UntypedIfBranch, UntypedPattern, UntypedRecordUpdateArg, + CallArg, ClauseGuard, Constant, Curve, IfBranch, LogicalOpChainKind, Pattern, + RecordUpdateSpread, Span, TraceKind, TraceLevel, Tracing, TypedArg, TypedCallArg, + TypedClause, TypedClauseGuard, TypedIfBranch, TypedPattern, TypedRecordUpdateArg, UnOp, + UntypedArg, UntypedClause, UntypedClauseGuard, UntypedIfBranch, UntypedPattern, + UntypedRecordUpdateArg, + }, + builtins::{ + bool, byte_array, function, g1_element, g2_element, int, list, string, tuple, void, }, - builtins::{bool, byte_array, function, g1_element, g2_element, int, list, string, tuple}, expr::{FnStyle, TypedExpr, UntypedExpr}, format, line_numbers::LineNumbers, - tipo::fields::FieldMap, + tipo::{fields::FieldMap, PatternConstructor}, }; use std::{cmp::Ordering, collections::HashMap, rc::Rc}; use vec1::Vec1; @@ -1699,20 +1702,25 @@ impl<'a, 'b> ExprTyper<'a, 'b> { let mut expressions = Vec::with_capacity(count); for (i, expression) in untyped.into_iter().enumerate() { - match i.cmp(&(count - 1)) { + let no_assignment = assert_no_assignment(&expression); + + let typed_expression = scope.infer(expression)?; + + expressions.push(match i.cmp(&(count - 1)) { // When the expression is the last in a sequence, we enforce it is NOT // an assignment (kind of treat assignments like statements). - Ordering::Equal => assert_no_assignment(&expression)?, + Ordering::Equal => { + no_assignment?; + typed_expression + } // This isn't the final expression in the sequence, so it *must* // be a let-binding; we do not allow anything else. - Ordering::Less => assert_assignment(&expression)?, + Ordering::Less => assert_assignment(typed_expression)?, // Can't actually happen - Ordering::Greater => (), - } - - expressions.push(scope.infer(expression)?); + Ordering::Greater => typed_expression, + }) } Ok(expressions) @@ -2076,12 +2084,35 @@ fn assert_no_assignment(expr: &UntypedExpr) -> Result<(), Error> { | UntypedExpr::CurvePoint { .. } => Ok(()), } } -fn assert_assignment(expr: &UntypedExpr) -> Result<(), Error> { - if !matches!(*expr, UntypedExpr::Assignment { .. }) { + +fn assert_assignment(expr: TypedExpr) -> Result { + if !matches!(expr, TypedExpr::Assignment { .. }) { + if expr.tipo().is_void() { + return Ok(TypedExpr::Assignment { + location: expr.location(), + tipo: void(), + value: expr.clone().into(), + pattern: Pattern::Constructor { + is_record: false, + location: expr.location(), + name: "Void".to_string(), + constructor: PatternConstructor::Record { + name: "Void".to_string(), + field_map: None, + }, + arguments: vec![], + module: None, + with_spread: false, + tipo: void(), + }, + kind: AssignmentKind::Let, + }); + } + return Err(Error::ImplicitlyDiscardedExpression { location: expr.location(), }); } - Ok(()) + Ok(expr) } diff --git a/examples/acceptance_tests/098/aiken.lock b/examples/acceptance_tests/098/aiken.lock new file mode 100644 index 00000000..6e350cda --- /dev/null +++ b/examples/acceptance_tests/098/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/098/aiken.toml b/examples/acceptance_tests/098/aiken.toml new file mode 100644 index 00000000..6d28355c --- /dev/null +++ b/examples/acceptance_tests/098/aiken.toml @@ -0,0 +1,3 @@ +name = "aiken-lang/acceptance_test_098" +version = "0.0.0" +description = "" diff --git a/examples/acceptance_tests/098/lib/foo.ak b/examples/acceptance_tests/098/lib/foo.ak new file mode 100644 index 00000000..42033c2a --- /dev/null +++ b/examples/acceptance_tests/098/lib/foo.ak @@ -0,0 +1,8 @@ +fn label(str: String) -> Void { + trace str Void +} + +test foo() { + label(@"Foo") + True +}