Allow assignment as last expression

This is debatable, but I would argue that it's been sufficiently
  annoying for people and such a low-hanging fruit that we ought to do
  something about it.

  The strategy here is simple: when we find a sequence of expression
  that ends with an assignment (let or expect), we can simply desugar it
  into two expressions: the assignment followed by either `Void` or a
  boolean.

  The latter is used when the assignment pattern is itself a boolean;
  the next boolean becomes the expected value. The former, `Void`, is
  used for everything else. So said differently, any assignment
  implicitly _returns Void_, except for boolean which return the actual
  patterned bool.

  <table>
  <thead><tr><th>expression</th><th>desugar into</th></tr></thead>
  <tbody>
  <tr>
  <td>

  ```aiken
  fn expect_bool(data: Data) -> Void {
    expect _: Bool = data
  }
  ```
  </td>
  <td>

  ```aiken
  fn expect_bool(data: Data) -> Void {
    expect _: Bool = data
    Void
  }
  ```
  </td>
  </tr>
  <tr>
  <td>

  ```aiken
  fn weird_maths() -> Bool {
    expect 1 == 2
  }
  ```
  </td>
  <td>

  ```aiken
  fn weird_maths() -> Bool {
    expect True = 1 == 2
    True
  }
  ```
  </td>
  </tr>
  </tbody>
  </table>
This commit is contained in:
KtorZ 2024-08-20 14:19:27 +02:00
parent d1ee90a3a0
commit fbe6f02fd1
No known key found for this signature in database
GPG Key ID: 33173CB6F77F4277
5 changed files with 240 additions and 103 deletions

View File

@ -1423,6 +1423,26 @@ impl UntypedPattern {
is_record: false,
}
}
/// Returns Some(<bool>) if the pattern is a [`Boolean`] literal,
/// holding the target value. None if it isn't a bool pattern.
pub fn get_bool(&self) -> Option<bool> {
match self {
Self::Constructor {
module: None,
name,
constructor: (),
..
} if name == "True" => Some(true),
Self::Constructor {
module: None,
name,
constructor: (),
..
} if name == "False" => Some(false),
_ => None,
}
}
}
impl TypedPattern {

View File

@ -1,4 +1,5 @@
use crate::{
use crate::tipo::ValueConstructorVariant;
pub(crate) use crate::{
ast::{
self, Annotation, ArgBy, ArgName, AssignmentPattern, BinOp, Bls12_381Point,
ByteArrayFormatPreference, CallArg, Curve, DataType, DataTypeKey, DefinitionLocation,
@ -7,7 +8,7 @@ use crate::{
TypedDataType, TypedIfBranch, TypedRecordUpdateArg, UnOp, UntypedArg,
UntypedAssignmentKind, UntypedClause, UntypedIfBranch, UntypedRecordUpdateArg,
},
builtins::void,
builtins::{bool, void},
parser::token::Base,
tipo::{
check_replaceable_opaque_type, convert_opaque_type, lookup_data_type_by_tipo,
@ -472,6 +473,44 @@ impl TypedExpr {
.or(Some(Located::Expression(self))),
}
}
pub fn void(location: Span) -> Self {
TypedExpr::Var {
name: "Void".to_string(),
constructor: ValueConstructor {
public: true,
variant: ValueConstructorVariant::Record {
name: "Void".to_string(),
arity: 0,
field_map: None,
location: Span::empty(),
module: String::new(),
constructors_count: 1,
},
tipo: void(),
},
location,
}
}
pub fn bool(value: bool, location: Span) -> Self {
TypedExpr::Var {
name: "Bool".to_string(),
constructor: ValueConstructor {
public: true,
variant: ValueConstructorVariant::Record {
name: if value { "True" } else { "False" }.to_string(),
arity: 0,
field_map: None,
location: Span::empty(),
module: String::new(),
constructors_count: 2,
},
tipo: bool(),
},
location,
}
}
}
// Represent how a function was written so that we can format it back.

View File

@ -1009,85 +1009,6 @@ fn anonymous_function_dupicate_args() {
))
}
#[test]
fn assignement_last_expr_when() {
let source_code = r#"
pub fn foo() {
let bar = None
when bar is {
Some(_) -> {
let wow = 1
}
None -> {
2
}
}
}
"#;
assert!(matches!(
check(parse(source_code)),
Err((_, Error::LastExpressionIsAssignment { .. }))
))
}
#[test]
fn assignement_last_expr_if_first_branch() {
let source_code = r#"
pub fn foo() {
if True {
let thing = 1
} else {
1
}
}
"#;
assert!(matches!(
check(parse(source_code)),
Err((_, Error::LastExpressionIsAssignment { .. }))
))
}
#[test]
fn assignement_last_expr_if_branches() {
let source_code = r#"
pub fn foo() {
if True {
2
} else if False {
let thing = 1
} else {
1
}
}
"#;
assert!(matches!(
check(parse(source_code)),
Err((_, Error::LastExpressionIsAssignment { .. }))
))
}
#[test]
fn assignement_last_expr_if_final_else() {
let source_code = r#"
pub fn foo() {
if True {
1
} else {
let thing = 1
}
}
"#;
assert!(matches!(
check(parse(source_code)),
Err((_, Error::LastExpressionIsAssignment { .. }))
))
}
#[test]
fn if_scoping() {
let source_code = r#"
@ -2956,3 +2877,79 @@ fn pattern_bytearray_not_unify_subject() {
Err((_, Error::CouldNotUnify { .. }))
))
}
#[test]
fn recover_no_assignment_sequence() {
let source_code = r#"
pub fn main() {
let result = 42
expect result + 1 == 43
}
"#;
assert!(check(parse(source_code)).is_ok());
}
#[test]
fn recover_no_assignment_fn_body() {
let source_code = r#"
pub fn is_bool(foo: Data) -> Void {
expect _: Bool = foo
}
"#;
assert!(check(parse(source_code)).is_ok());
}
#[test]
fn recover_no_assignment_when_clause() {
let source_code = r#"
pub fn main(foo) {
when foo is {
[] -> let bar = foo
[x, ..] -> expect _: Int = x
}
}
"#;
let (warnings, _) = check(parse(source_code)).unwrap();
assert!(matches!(
&warnings[..],
[Warning::UnusedVariable { name, .. }] if name == "bar",
))
}
#[test]
fn recover_no_assignment_fn_if_then_else() {
let source_code = r#"
pub fn foo(weird_maths) -> Bool {
if weird_maths {
expect 1 == 2
} else {
expect 1 + 1 == 2
}
}
"#;
assert!(check(parse(source_code)).is_ok());
}
#[test]
fn recover_no_assignment_logical_chain_op() {
let source_code = r#"
pub fn foo() -> Bool {
and {
expect 1 + 1 == 2,
True,
2 > 0,
or {
expect True,
False,
}
}
}
"#;
assert!(check(parse(source_code)).is_ok());
}

View File

@ -2,7 +2,7 @@ use super::Type;
use crate::{
ast::{Annotation, BinOp, CallArg, LogicalOpChainKind, Span, UntypedFunction, UntypedPattern},
error::ExtraData,
expr::{self, UntypedExpr},
expr::{self, AssignmentPattern, UntypedExpr},
format::Formatter,
levenshtein,
pretty::Documentable,
@ -15,6 +15,7 @@ use owo_colors::{
Stream::{Stderr, Stdout},
};
use std::{collections::HashMap, fmt::Display, rc::Rc};
use vec1::Vec1;
#[derive(Debug, Clone, thiserror::Error)]
#[error(
@ -470,6 +471,7 @@ If you really meant to return that last expression, try to replace it with the f
#[label("let-binding as last expression")]
location: Span,
expr: expr::UntypedExpr,
patterns: Vec1<AssignmentPattern>,
},
#[error(

View File

@ -1397,9 +1397,16 @@ impl<'a, 'b> ExprTyper<'a, 'b> {
let (then, typed_patterns) = self.in_new_scope(|scope| {
let typed_patterns = scope.infer_clause_pattern(patterns, subject, &location)?;
assert_no_assignment(&then)?;
let then = scope.infer(then)?;
let then = if let Some(filler) =
recover_from_no_assignment(assert_no_assignment(&then), then.location())?
{
TypedExpr::Sequence {
location,
expressions: vec![scope.infer(then)?, filler],
}
} else {
scope.infer(then)?
};
Ok::<_, Error>((then, typed_patterns))
})?;
@ -1521,8 +1528,16 @@ impl<'a, 'b> ExprTyper<'a, 'b> {
typed_branches.push(typed_branch);
}
assert_no_assignment(&final_else)?;
let typed_final_else = self.infer(final_else)?;
let typed_final_else = if let Some(filler) =
recover_from_no_assignment(assert_no_assignment(&final_else), final_else.location())?
{
TypedExpr::Sequence {
location: final_else.location(),
expressions: vec![self.infer(final_else)?, filler],
}
} else {
self.infer(final_else)?
};
self.unify(
first_body_type.clone(),
@ -1569,8 +1584,18 @@ impl<'a, 'b> ExprTyper<'a, 'b> {
location: branch.condition.location().union(location),
})
}
assert_no_assignment(&branch.body)?;
let body = typer.infer(branch.body.clone())?;
let body = if let Some(filler) = recover_from_no_assignment(
assert_no_assignment(&branch.body),
branch.body.location(),
)? {
TypedExpr::Sequence {
location: branch.body.location(),
expressions: vec![typer.infer(branch.body.clone())?, filler],
}
} else {
typer.infer(branch.body.clone())?
};
Ok((*value, body, Some((pattern, tipo))))
})?,
@ -1584,8 +1609,17 @@ impl<'a, 'b> ExprTyper<'a, 'b> {
false,
)?;
assert_no_assignment(&branch.body)?;
let body = self.infer(branch.body.clone())?;
let body = if let Some(filler) = recover_from_no_assignment(
assert_no_assignment(&branch.body),
branch.body.location(),
)? {
TypedExpr::Sequence {
location: branch.body.location(),
expressions: vec![self.infer(branch.body.clone())?, filler],
}
} else {
self.infer(branch.body.clone())?
};
(condition, body, None)
}
@ -1631,7 +1665,9 @@ impl<'a, 'b> ExprTyper<'a, 'b> {
body: UntypedExpr,
return_type: Option<Rc<Type>>,
) -> Result<(Vec<TypedArg>, TypedExpr, Rc<Type>), Error> {
assert_no_assignment(&body)?;
let location = body.location();
let no_assignment = assert_no_assignment(&body);
let (body_rigid_names, body_infer) = self.in_new_scope(|body_typer| {
let mut argument_names = HashMap::with_capacity(args.len());
@ -1668,7 +1704,17 @@ impl<'a, 'b> ExprTyper<'a, 'b> {
Ok((body_typer.hydrator.rigid_names(), body_typer.infer(body)))
})?;
let body = body_infer.map_err(|e| e.with_unify_error_rigid_names(&body_rigid_names))?;
let inferred_body =
body_infer.map_err(|e| e.with_unify_error_rigid_names(&body_rigid_names));
let body = if let Some(filler) = recover_from_no_assignment(no_assignment, location)? {
TypedExpr::Sequence {
location,
expressions: vec![inferred_body?, filler],
}
} else {
inferred_body?
};
// Check that any return type is accurate.
let return_type = match return_type {
@ -1757,9 +1803,17 @@ 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)?;
let typed_expression = if let Some(filler) = recover_from_no_assignment(
assert_no_assignment(&expression),
expression.location(),
)? {
TypedExpr::Sequence {
location: expression.location(),
expressions: vec![self.infer(expression)?, filler],
}
} else {
self.infer(expression)?
};
self.unify(
bool(),
@ -2046,21 +2100,29 @@ impl<'a, 'b> ExprTyper<'a, 'b> {
let typed_expression = scope.infer(expression)?;
expressions.push(match i.cmp(&(count - 1)) {
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 => {
no_assignment?;
typed_expression
if let Some(filler) =
recover_from_no_assignment(no_assignment, typed_expression.location())?
{
expressions.push(typed_expression);
expressions.push(filler);
} else {
expressions.push(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(typed_expression)?,
Ordering::Less => {
expressions.push(assert_assignment(typed_expression)?);
}
// Can't actually happen
Ordering::Greater => typed_expression,
})
Ordering::Greater => unreachable!(),
}
}
Ok(expressions)
@ -2479,11 +2541,28 @@ impl<'a, 'b> ExprTyper<'a, 'b> {
}
}
fn recover_from_no_assignment(
result: Result<(), Error>,
span: Span,
) -> Result<Option<TypedExpr>, Error> {
if let Err(Error::LastExpressionIsAssignment { patterns, .. }) = result {
match patterns.first().pattern.get_bool() {
Some(expected) if patterns.len() == 1 => Ok(Some(TypedExpr::bool(expected, span))),
_ => Ok(Some(TypedExpr::void(span))),
}
} else {
result.map(|()| None)
}
}
fn assert_no_assignment(expr: &UntypedExpr) -> Result<(), Error> {
match expr {
UntypedExpr::Assignment { value, .. } => Err(Error::LastExpressionIsAssignment {
UntypedExpr::Assignment {
value, patterns, ..
} => Err(Error::LastExpressionIsAssignment {
location: expr.location(),
expr: *value.clone(),
patterns: patterns.clone(),
}),
UntypedExpr::Trace { then, .. } => assert_no_assignment(then),
UntypedExpr::Fn { .. }