Merge branch 'desugar-last-assignment'

This commit is contained in:
KtorZ 2024-08-23 16:04:49 +02:00
commit 842001dc0d
No known key found for this signature in database
GPG Key ID: 33173CB6F77F4277
13 changed files with 280 additions and 71 deletions

View File

@ -44,6 +44,10 @@
- **aiken-lang**: remove warning on discarded expect, allowing to keep 'side-effects' when necessary. See [#967](https://github.com/aiken-lang/aiken/pull/967). @KtorZ - **aiken-lang**: remove warning on discarded expect, allowing to keep 'side-effects' when necessary. See [#967](https://github.com/aiken-lang/aiken/pull/967). @KtorZ
- **aiken-lang**: allow expect as last (or only) expression in function body, when clauses and if branches. Such expressions unify with `Void`. See [#1000](https://github.com/aiken-lang/aiken/pull/1000). @KtorZ
- **aiken-lang**: allow tests to return `Void`. Tests that return `Void` are treated the same as tests that return `True`. See [#1000](https://github.com/aiken-lang/aiken/pull/1000). @KtorZ
- **aiken-lang**: rework traces to be (1) variadic, (2) generic in its arguments and (3) structured. @KtorZ - **aiken-lang**: rework traces to be (1) variadic, (2) generic in its arguments and (3) structured. @KtorZ
In more details: In more details:

View File

@ -1,4 +1,5 @@
use crate::{ use crate::tipo::ValueConstructorVariant;
pub(crate) use crate::{
ast::{ ast::{
self, Annotation, ArgBy, ArgName, AssignmentPattern, BinOp, Bls12_381Point, self, Annotation, ArgBy, ArgName, AssignmentPattern, BinOp, Bls12_381Point,
ByteArrayFormatPreference, CallArg, Curve, DataType, DataTypeKey, DefinitionLocation, ByteArrayFormatPreference, CallArg, Curve, DataType, DataTypeKey, DefinitionLocation,
@ -472,6 +473,25 @@ impl TypedExpr {
.or(Some(Located::Expression(self))), .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,
}
}
} }
// Represent how a function was written so that we can format it back. // Represent how a function was written so that we can format it back.

View File

@ -52,14 +52,7 @@ Test(
location: 0..26, location: 0..26,
name: "foo", name: "foo",
public: false, public: false,
return_annotation: Some( return_annotation: None,
Constructor {
location: 0..39,
module: None,
name: "Bool",
arguments: [],
},
),
return_type: (), return_type: (),
end_position: 38, end_position: 38,
on_test_failure: FailImmediately, on_test_failure: FailImmediately,

View File

@ -37,14 +37,7 @@ Test(
location: 0..28, location: 0..28,
name: "foo", name: "foo",
public: false, public: false,
return_annotation: Some( return_annotation: None,
Constructor {
location: 0..41,
module: None,
name: "Bool",
arguments: [],
},
),
return_type: (), return_type: (),
end_position: 40, end_position: 40,
on_test_failure: FailImmediately, on_test_failure: FailImmediately,

View File

@ -44,14 +44,7 @@ Test(
location: 0..26, location: 0..26,
name: "foo", name: "foo",
public: false, public: false,
return_annotation: Some( return_annotation: None,
Constructor {
location: 0..39,
module: None,
name: "Bool",
arguments: [],
},
),
return_type: (), return_type: (),
end_position: 38, end_position: 38,
on_test_failure: FailImmediately, on_test_failure: FailImmediately,

View File

@ -13,14 +13,7 @@ Test(
location: 0..10, location: 0..10,
name: "foo", name: "foo",
public: false, public: false,
return_annotation: Some( return_annotation: None,
Constructor {
location: 0..23,
module: None,
name: "Bool",
arguments: [],
},
),
return_type: (), return_type: (),
end_position: 22, end_position: 22,
on_test_failure: FailImmediately, on_test_failure: FailImmediately,

View File

@ -44,14 +44,7 @@ Test(
location: 0..26, location: 0..26,
name: "invalid_inputs", name: "invalid_inputs",
public: false, public: false,
return_annotation: Some( return_annotation: None,
Constructor {
location: 0..61,
module: None,
name: "Bool",
arguments: [],
},
),
return_type: (), return_type: (),
end_position: 60, end_position: 60,
on_test_failure: SucceedEventually, on_test_failure: SucceedEventually,

View File

@ -45,7 +45,7 @@ pub fn parser() -> impl Parser<Token, ast::UntypedDefinition, Error = ParseError
end_position: span.end - 1, end_position: span.end - 1,
name, name,
public: false, public: false,
return_annotation: Some(ast::Annotation::boolean(span)), return_annotation: None,
return_type: (), return_type: (),
on_test_failure: fail.unwrap_or(OnTestFailure::FailImmediately), on_test_failure: fail.unwrap_or(OnTestFailure::FailImmediately),
}) })

View File

@ -1088,6 +1088,28 @@ fn assignement_last_expr_if_final_else() {
)) ))
} }
#[test]
fn assignment_last_expr_logical_chain() {
let source_code = r#"
pub fn foo() -> Bool {
and {
expect 1 + 1 == 2,
True,
2 > 0,
or {
expect True,
False,
}
}
}
"#;
assert!(matches!(
check(parse(source_code)),
Err((_, Error::LastExpressionIsAssignment { .. }))
))
}
#[test] #[test]
fn if_scoping() { fn if_scoping() {
let source_code = r#" let source_code = r#"
@ -1747,8 +1769,7 @@ fn pipe_wrong_arity_fully_saturated_return_fn() {
fn fuzzer_ok_basic() { fn fuzzer_ok_basic() {
let source_code = r#" let source_code = r#"
fn int() -> Fuzzer<Int> { todo } fn int() -> Fuzzer<Int> { todo }
test prop(n via int()) { True }
test prop(n via int()) { todo }
"#; "#;
assert!(check(parse(source_code)).is_ok()); assert!(check(parse(source_code)).is_ok());
@ -1758,8 +1779,7 @@ fn fuzzer_ok_basic() {
fn fuzzer_ok_explicit() { fn fuzzer_ok_explicit() {
let source_code = r#" let source_code = r#"
fn int(prng: PRNG) -> Option<(PRNG, Int)> { todo } fn int(prng: PRNG) -> Option<(PRNG, Int)> { todo }
test prop(n via int) { Void }
test prop(n via int) { todo }
"#; "#;
assert!(check(parse(source_code)).is_ok()); assert!(check(parse(source_code)).is_ok());
@ -1771,7 +1791,7 @@ fn fuzzer_ok_list() {
fn int() -> Fuzzer<Int> { todo } fn int() -> Fuzzer<Int> { todo }
fn list(a: Fuzzer<a>) -> Fuzzer<List<a>> { todo } fn list(a: Fuzzer<a>) -> Fuzzer<List<a>> { todo }
test prop(xs via list(int())) { todo } test prop(xs via list(int())) { True }
"#; "#;
assert!(check(parse(source_code)).is_ok()); assert!(check(parse(source_code)).is_ok());
@ -2956,3 +2976,92 @@ fn pattern_bytearray_not_unify_subject() {
Err((_, Error::CouldNotUnify { .. })) 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 {
[] -> Void
[x, ..] -> expect _: Int = x
}
}
"#;
assert!(check(parse(source_code)).is_ok());
}
#[test]
fn recover_no_assignment_fn_if_then_else() {
let source_code = r#"
pub fn foo(weird_maths) -> Void {
if weird_maths {
expect 1 == 2
} else {
expect 1 + 1 == 2
}
}
"#;
assert!(check(parse(source_code)).is_ok());
}
#[test]
fn test_return_explicit_void() {
let source_code = r#"
test foo() {
Void
}
"#;
assert!(check(parse(source_code)).is_ok());
}
#[test]
fn test_return_implicit_void() {
let source_code = r#"
test foo() {
let data: Data = 42
expect _: Int = data
}
"#;
assert!(check(parse(source_code)).is_ok());
}
#[test]
fn test_return_illegal() {
let source_code = r#"
test foo() {
42
}
"#;
assert!(matches!(
check(parse(source_code)),
Err((_, Error::IllegalTestType { .. }))
))
}

View File

@ -2,7 +2,7 @@ use super::Type;
use crate::{ use crate::{
ast::{Annotation, BinOp, CallArg, LogicalOpChainKind, Span, UntypedFunction, UntypedPattern}, ast::{Annotation, BinOp, CallArg, LogicalOpChainKind, Span, UntypedFunction, UntypedPattern},
error::ExtraData, error::ExtraData,
expr::{self, UntypedExpr}, expr::{self, AssignmentPattern, UntypedAssignmentKind, UntypedExpr},
format::Formatter, format::Formatter,
levenshtein, levenshtein,
pretty::Documentable, pretty::Documentable,
@ -15,6 +15,7 @@ use owo_colors::{
Stream::{Stderr, Stdout}, Stream::{Stderr, Stdout},
}; };
use std::{collections::HashMap, fmt::Display, rc::Rc}; use std::{collections::HashMap, fmt::Display, rc::Rc};
use vec1::Vec1;
#[derive(Debug, Clone, thiserror::Error)] #[derive(Debug, Clone, thiserror::Error)]
#[error( #[error(
@ -470,6 +471,8 @@ If you really meant to return that last expression, try to replace it with the f
#[label("let-binding as last expression")] #[label("let-binding as last expression")]
location: Span, location: Span,
expr: expr::UntypedExpr, expr: expr::UntypedExpr,
patterns: Vec1<AssignmentPattern>,
kind: UntypedAssignmentKind,
}, },
#[error( #[error(
@ -1023,7 +1026,7 @@ The best thing to do from here is to remove it."#))]
}, },
#[error("I caught a test with too many arguments.\n")] #[error("I caught a test with too many arguments.\n")]
#[diagnostic(code("illegal::test_arity"))] #[diagnostic(code("illegal::test::arity"))]
#[diagnostic(help( #[diagnostic(help(
"Tests are allowed to have 0 or 1 argument, but no more. Here I've found a test definition with {count} arguments. If you need to provide multiple values to a test, use a Record or a Tuple.", "Tests are allowed to have 0 or 1 argument, but no more. Here I've found a test definition with {count} arguments. If you need to provide multiple values to a test, use a Record or a Tuple.",
))] ))]
@ -1033,6 +1036,18 @@ The best thing to do from here is to remove it."#))]
location: Span, location: Span,
}, },
#[error("I caught a test with an illegal return type.\n")]
#[diagnostic(code("illegal::test::return"))]
#[diagnostic(help(
"Tests must return either {Bool} or {Void}. Note that `expect` assignment are implicitly typed {Void} (and thus, may be the last expression of a test).",
Bool = "Bool".if_supports_color(Stderr, |s| s.cyan()),
Void = "Void".if_supports_color(Stderr, |s| s.cyan()),
))]
IllegalTestType {
#[label("expected Bool or Void")]
location: Span,
},
#[error("I choked on a generic type left in an outward-facing interface.\n")] #[error("I choked on a generic type left in an outward-facing interface.\n")]
#[diagnostic(code("illegal::generic_in_abi"))] #[diagnostic(code("illegal::generic_in_abi"))]
#[diagnostic(help( #[diagnostic(help(
@ -1102,6 +1117,7 @@ impl ExtraData for Error {
| Error::UpdateMultiConstructorType { .. } | Error::UpdateMultiConstructorType { .. }
| Error::ValidatorImported { .. } | Error::ValidatorImported { .. }
| Error::IncorrectTestArity { .. } | Error::IncorrectTestArity { .. }
| Error::IllegalTestType { .. }
| Error::GenericLeftAtBoundary { .. } | Error::GenericLeftAtBoundary { .. }
| Error::UnexpectedMultiPatternAssignment { .. } | Error::UnexpectedMultiPatternAssignment { .. }
| Error::ExpectOnOpaqueType { .. } | Error::ExpectOnOpaqueType { .. }

View File

@ -1397,9 +1397,16 @@ impl<'a, 'b> ExprTyper<'a, 'b> {
let (then, typed_patterns) = self.in_new_scope(|scope| { let (then, typed_patterns) = self.in_new_scope(|scope| {
let typed_patterns = scope.infer_clause_pattern(patterns, subject, &location)?; let typed_patterns = scope.infer_clause_pattern(patterns, subject, &location)?;
assert_no_assignment(&then)?; let then = if let Some(filler) =
recover_from_no_assignment(assert_no_assignment(&then), then.location())?
let then = scope.infer(then)?; {
TypedExpr::Sequence {
location,
expressions: vec![scope.infer(then)?, filler],
}
} else {
scope.infer(then)?
};
Ok::<_, Error>((then, typed_patterns)) Ok::<_, Error>((then, typed_patterns))
})?; })?;
@ -1521,8 +1528,16 @@ impl<'a, 'b> ExprTyper<'a, 'b> {
typed_branches.push(typed_branch); typed_branches.push(typed_branch);
} }
assert_no_assignment(&final_else)?; let typed_final_else = if let Some(filler) =
let typed_final_else = self.infer(final_else)?; 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( self.unify(
first_body_type.clone(), first_body_type.clone(),
@ -1569,8 +1584,18 @@ impl<'a, 'b> ExprTyper<'a, 'b> {
location: branch.condition.location().union(location), 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)))) Ok((*value, body, Some((pattern, tipo))))
})?, })?,
@ -1584,8 +1609,17 @@ impl<'a, 'b> ExprTyper<'a, 'b> {
false, false,
)?; )?;
assert_no_assignment(&branch.body)?; let body = if let Some(filler) = recover_from_no_assignment(
let body = self.infer(branch.body.clone())?; 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) (condition, body, None)
} }
@ -1631,7 +1665,9 @@ impl<'a, 'b> ExprTyper<'a, 'b> {
body: UntypedExpr, body: UntypedExpr,
return_type: Option<Rc<Type>>, return_type: Option<Rc<Type>>,
) -> Result<(Vec<TypedArg>, TypedExpr, Rc<Type>), Error> { ) -> 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 (body_rigid_names, body_infer) = self.in_new_scope(|body_typer| {
let mut argument_names = HashMap::with_capacity(args.len()); 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))) 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. // Check that any return type is accurate.
let return_type = match return_type { let return_type = match return_type {
@ -1758,7 +1804,6 @@ impl<'a, 'b> ExprTyper<'a, 'b> {
for expression in expressions { for expression in expressions {
assert_no_assignment(&expression)?; assert_no_assignment(&expression)?;
let typed_expression = self.infer(expression)?; let typed_expression = self.infer(expression)?;
self.unify( self.unify(
@ -2046,21 +2091,29 @@ impl<'a, 'b> ExprTyper<'a, 'b> {
let typed_expression = scope.infer(expression)?; 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 // When the expression is the last in a sequence, we enforce it is NOT
// an assignment (kind of treat assignments like statements). // an assignment (kind of treat assignments like statements).
Ordering::Equal => { Ordering::Equal => {
no_assignment?; if let Some(filler) =
typed_expression 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* // This isn't the final expression in the sequence, so it *must*
// be a let-binding; we do not allow anything else. // 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 // Can't actually happen
Ordering::Greater => typed_expression, Ordering::Greater => unreachable!(),
}) }
} }
Ok(expressions) Ok(expressions)
@ -2479,11 +2532,36 @@ 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 {
ref patterns,
ref kind,
..
}) = result
{
if matches!(kind, AssignmentKind::Expect { ..} if patterns.len() == 1) {
return Ok(Some(TypedExpr::void(span)));
}
}
result.map(|()| None)
}
fn assert_no_assignment(expr: &UntypedExpr) -> Result<(), Error> { fn assert_no_assignment(expr: &UntypedExpr) -> Result<(), Error> {
match expr { match expr {
UntypedExpr::Assignment { value, .. } => Err(Error::LastExpressionIsAssignment { UntypedExpr::Assignment {
value,
patterns,
kind,
..
} => Err(Error::LastExpressionIsAssignment {
location: expr.location(), location: expr.location(),
expr: *value.clone(), expr: *value.clone(),
patterns: patterns.clone(),
kind: *kind,
}), }),
UntypedExpr::Trace { then, .. } => assert_no_assignment(then), UntypedExpr::Trace { then, .. } => assert_no_assignment(then),
UntypedExpr::Fn { .. } UntypedExpr::Fn { .. }

View File

@ -392,12 +392,25 @@ fn infer_definition(
let typed_f = infer_function(&f.into(), module_name, hydrators, environment, tracing)?; let typed_f = infer_function(&f.into(), module_name, hydrators, environment, tracing)?;
environment.unify( let is_bool = environment.unify(
typed_f.return_type.clone(), typed_f.return_type.clone(),
builtins::bool(), builtins::bool(),
typed_f.location, typed_f.location,
false, false,
)?; );
let is_void = environment.unify(
typed_f.return_type.clone(),
builtins::void(),
typed_f.location,
false,
);
if is_bool.or(is_void).is_err() {
return Err(Error::IllegalTestType {
location: typed_f.location,
});
}
Ok(Definition::Test(Function { Ok(Definition::Test(Function {
doc: typed_f.doc, doc: typed_f.doc,

View File

@ -39,7 +39,11 @@ impl EvalResult {
} else { } else {
self.result.is_err() self.result.is_err()
|| matches!(self.result, Ok(Term::Error)) || matches!(self.result, Ok(Term::Error))
|| !matches!(self.result, Ok(Term::Constant(ref con)) if matches!(con.as_ref(), Constant::Bool(true))) || !matches!(
self.result,
Ok(Term::Constant(ref con))
if matches!(con.as_ref(), Constant::Bool(true)) || matches!(con.as_ref(), Constant::Unit)
)
} }
} }