From 7af4ef53ab2bd92569ae27f7fc3fbf0c6d5e12b8 Mon Sep 17 00:00:00 2001 From: rvcas Date: Tue, 12 Mar 2024 17:31:13 -0400 Subject: [PATCH 01/13] feat: block expects on opaque types --- crates/aiken-lang/src/ast.rs | 1 + crates/aiken-lang/src/builtins.rs | 17 +++++++++ crates/aiken-lang/src/tests/check.rs | 38 +++++++++++++++++++ crates/aiken-lang/src/tipo.rs | 19 ++++++---- crates/aiken-lang/src/tipo/environment.rs | 7 ++++ crates/aiken-lang/src/tipo/error.rs | 8 ++++ crates/aiken-lang/src/tipo/expr.rs | 9 +++++ crates/aiken-lang/src/tipo/pattern.rs | 2 +- crates/aiken-lang/src/tipo/pretty.rs | 15 ++++++++ ...ests__opaque_singleton_multi_variants.snap | 1 + ...tor__tests__opaque_singleton_variants.snap | 3 ++ 11 files changed, 112 insertions(+), 8 deletions(-) diff --git a/crates/aiken-lang/src/ast.rs b/crates/aiken-lang/src/ast.rs index 750d7cc4..e2dec29f 100644 --- a/crates/aiken-lang/src/ast.rs +++ b/crates/aiken-lang/src/ast.rs @@ -1225,6 +1225,7 @@ impl TypedPattern { } pub fn tipo(&self, value: &TypedExpr) -> Option> { + // expect thing: Wow = thing match self { Pattern::Int { .. } => Some(builtins::int()), Pattern::Constructor { tipo, .. } => Some(tipo.clone()), diff --git a/crates/aiken-lang/src/builtins.rs b/crates/aiken-lang/src/builtins.rs index da98a870..962e4ca4 100644 --- a/crates/aiken-lang/src/builtins.rs +++ b/crates/aiken-lang/src/builtins.rs @@ -1267,6 +1267,7 @@ pub fn prelude_data_types(id_gen: &IdGenerator) -> IndexMap Rc { Rc::new(Type::App { public: true, + opaque: false, name: INT.to_string(), module: "".to_string(), args: vec![], @@ -1277,6 +1278,7 @@ pub fn int() -> Rc { pub fn data() -> Rc { Rc::new(Type::App { public: true, + opaque: false, name: DATA.to_string(), module: "".to_string(), args: vec![], @@ -1288,6 +1290,7 @@ pub fn byte_array() -> Rc { Rc::new(Type::App { args: vec![], public: true, + opaque: false, name: BYTE_ARRAY.to_string(), module: "".to_string(), alias: None, @@ -1297,6 +1300,7 @@ pub fn byte_array() -> Rc { pub fn g1_element() -> Rc { Rc::new(Type::App { public: true, + opaque: false, module: "".to_string(), name: G1_ELEMENT.to_string(), args: vec![], @@ -1307,6 +1311,7 @@ pub fn g1_element() -> Rc { pub fn g2_element() -> Rc { Rc::new(Type::App { public: true, + opaque: false, module: "".to_string(), name: G2_ELEMENT.to_string(), args: vec![], @@ -1317,6 +1322,7 @@ pub fn g2_element() -> Rc { pub fn miller_loop_result() -> Rc { Rc::new(Type::App { public: true, + opaque: false, module: "".to_string(), name: MILLER_LOOP_RESULT.to_string(), args: vec![], @@ -1332,6 +1338,7 @@ pub fn bool() -> Rc { Rc::new(Type::App { args: vec![], public: true, + opaque: false, name: BOOL.to_string(), module: "".to_string(), alias: None, @@ -1342,6 +1349,7 @@ pub fn prng() -> Rc { Rc::new(Type::App { args: vec![], public: true, + opaque: false, name: PRNG.to_string(), module: "".to_string(), alias: None, @@ -1355,6 +1363,7 @@ pub fn fuzzer(a: Rc) -> Rc { name: "PRNG".to_string(), arguments: vec![], }; + Rc::new(Type::Fn { args: vec![prng()], ret: option(tuple(vec![prng(), a])), @@ -1391,6 +1400,7 @@ pub fn fuzzer(a: Rc) -> Rc { pub fn list(t: Rc) -> Rc { Rc::new(Type::App { public: true, + opaque: false, name: LIST.to_string(), module: "".to_string(), args: vec![t], @@ -1402,6 +1412,7 @@ pub fn string() -> Rc { Rc::new(Type::App { args: vec![], public: true, + opaque: false, name: STRING.to_string(), module: "".to_string(), alias: None, @@ -1412,6 +1423,7 @@ pub fn void() -> Rc { Rc::new(Type::App { args: vec![], public: true, + opaque: false, name: VOID.to_string(), module: "".to_string(), alias: None, @@ -1421,6 +1433,7 @@ pub fn void() -> Rc { pub fn option(a: Rc) -> Rc { Rc::new(Type::App { public: true, + opaque: false, name: OPTION.to_string(), module: "".to_string(), args: vec![a], @@ -1431,6 +1444,7 @@ pub fn option(a: Rc) -> Rc { pub fn ordering() -> Rc { Rc::new(Type::App { public: true, + opaque: false, name: ORDERING.to_string(), module: "".to_string(), args: vec![], @@ -1448,17 +1462,20 @@ pub fn function(args: Vec>, ret: Rc) -> Rc { pub fn generic_var(id: u64) -> Rc { let tipo = Rc::new(RefCell::new(TypeVar::Generic { id })); + Rc::new(Type::Var { tipo, alias: None }) } pub fn unbound_var(id: u64) -> Rc { let tipo = Rc::new(RefCell::new(TypeVar::Unbound { id })); + Rc::new(Type::Var { tipo, alias: None }) } pub fn wrapped_redeemer(redeemer: Rc) -> Rc { Rc::new(Type::App { public: true, + opaque: false, module: "".to_string(), name: REDEEMER_WRAPPER.to_string(), args: vec![redeemer], diff --git a/crates/aiken-lang/src/tests/check.rs b/crates/aiken-lang/src/tests/check.rs index 6aac1771..0bc67def 100644 --- a/crates/aiken-lang/src/tests/check.rs +++ b/crates/aiken-lang/src/tests/check.rs @@ -1793,3 +1793,41 @@ fn backpassing_type_annotation() { assert!(check(parse(source_code)).is_ok()) } + +fn forbid_expect_into_opaque_type_from_data() { + let source_code = r#" + opaque type Thing { inner: Int } + + fn bar(n: Data) { + expect a: Thing = n + + a + } + "#; + + assert!(matches!( + check(parse(source_code)), + Err((_, Error::ExpectOnOpaqueType { .. })) + )) +} + +#[test] +fn forbid_expect_into_opaque_type_constructor() { + let source_code = r#" + opaque type Thing { + Foo(Int) + Bar(Int) + } + + fn bar(n: Thing) { + expect Foo(a) = n + + a + } + "#; + + assert!(matches!( + check(parse(source_code)), + Err((_, Error::ExpectOnOpaqueType { .. })) + )) +} diff --git a/crates/aiken-lang/src/tipo.rs b/crates/aiken-lang/src/tipo.rs index 95442cae..9a113632 100644 --- a/crates/aiken-lang/src/tipo.rs +++ b/crates/aiken-lang/src/tipo.rs @@ -42,6 +42,7 @@ pub enum Type { /// App { public: bool, + opaque: bool, module: String, name: String, args: Vec>, @@ -152,12 +153,14 @@ impl Type { Rc::new(match self { Type::App { public, + opaque, module, name, args, .. } => Type::App { public, + opaque, module, name, args, @@ -181,17 +184,13 @@ impl Type { } } - pub fn is_result_constructor(&self) -> bool { + pub fn is_opaque(&self) -> bool { match self { - Type::Fn { ret, .. } => ret.is_result(), + Type::App { opaque, .. } => *opaque, _ => false, } } - pub fn is_result(&self) -> bool { - matches!(self, Self::App { name, module, .. } if "Result" == name && module.is_empty()) - } - pub fn is_unbound(&self) -> bool { matches!(self, Self::Var { tipo, .. } if tipo.borrow().is_unbound()) } @@ -459,6 +458,7 @@ impl Type { pub fn get_app_args( &self, public: bool, + opaque: bool, module: &str, name: &str, arity: usize, @@ -481,7 +481,7 @@ impl Type { Self::Var { tipo, alias } => { let args: Vec<_> = match tipo.borrow().deref() { TypeVar::Link { tipo } => { - return tipo.get_app_args(public, module, name, arity, environment); + return tipo.get_app_args(public, opaque, module, name, arity, environment); } TypeVar::Unbound { .. } => { @@ -496,6 +496,7 @@ impl Type { *tipo.borrow_mut() = TypeVar::Link { tipo: Rc::new(Self::App { public, + opaque, name: name.to_string(), module: module.to_owned(), args: args.clone(), @@ -650,6 +651,7 @@ pub fn convert_opaque_type( match t.as_ref() { Type::App { public, + opaque, module, name, args, @@ -662,6 +664,7 @@ pub fn convert_opaque_type( } Type::App { public: *public, + opaque: *opaque, module: module.clone(), name: name.clone(), args: new_args, @@ -736,6 +739,7 @@ pub fn find_and_replace_generics( Type::App { args, public, + opaque, module, name, alias, @@ -748,6 +752,7 @@ pub fn find_and_replace_generics( let t = Type::App { args: new_args, public: *public, + opaque: *opaque, module: module.clone(), name: name.clone(), alias: alias.clone(), diff --git a/crates/aiken-lang/src/tipo/environment.rs b/crates/aiken-lang/src/tipo/environment.rs index 1b240269..b83482a6 100644 --- a/crates/aiken-lang/src/tipo/environment.rs +++ b/crates/aiken-lang/src/tipo/environment.rs @@ -546,6 +546,7 @@ impl<'a> Environment<'a> { match t.deref() { Type::App { public, + opaque, name, module, args, @@ -555,8 +556,10 @@ impl<'a> Environment<'a> { .iter() .map(|t| self.instantiate(t.clone(), ids, hydrator)) .collect(); + Rc::new(Type::App { public: *public, + opaque: *opaque, name: name.clone(), module: module.clone(), alias: alias.clone(), @@ -983,6 +986,7 @@ impl<'a> Environment<'a> { Definition::DataType(DataType { name, public, + opaque, parameters, location, constructors, @@ -997,6 +1001,7 @@ impl<'a> Environment<'a> { let tipo = Rc::new(Type::App { public: *public, + opaque: *opaque, module: module.to_owned(), name: name.clone(), args: parameters.clone(), @@ -1856,6 +1861,7 @@ pub(crate) fn generalise(t: Rc, ctx_level: usize) -> Rc { Type::App { public, + opaque, module, name, args, @@ -1868,6 +1874,7 @@ pub(crate) fn generalise(t: Rc, ctx_level: usize) -> Rc { Rc::new(Type::App { public: *public, + opaque: *opaque, module: module.clone(), name: name.clone(), args, diff --git a/crates/aiken-lang/src/tipo/error.rs b/crates/aiken-lang/src/tipo/error.rs index ca1fbe7c..a8298cb0 100644 --- a/crates/aiken-lang/src/tipo/error.rs +++ b/crates/aiken-lang/src/tipo/error.rs @@ -259,6 +259,13 @@ You can use '{discard}' and numbers to distinguish between similar names. name: String, }, + #[error("I found an expect assignment involving an opaque type. This is not allowed.\n")] + #[diagnostic(code("illegal::expect_on_opaque"))] + ExpectOnOpaqueType { + #[label] + location: Span, + }, + #[error("I found a type definition that has a function type in it. This is not allowed.\n")] #[diagnostic(code("illegal::function_in_type"))] #[diagnostic(help( @@ -1045,6 +1052,7 @@ impl ExtraData for Error { | Error::IncorrectTestArity { .. } | Error::GenericLeftAtBoundary { .. } | Error::UnexpectedMultiPatternAssignment { .. } + | Error::ExpectOnOpaqueType { .. } | Error::ValidatorMustReturnBool { .. } => None, Error::UnknownType { name, .. } diff --git a/crates/aiken-lang/src/tipo/expr.rs b/crates/aiken-lang/src/tipo/expr.rs index b54e32c0..9a7e1abd 100644 --- a/crates/aiken-lang/src/tipo/expr.rs +++ b/crates/aiken-lang/src/tipo/expr.rs @@ -932,6 +932,7 @@ impl<'a, 'b> ExprTyper<'a, 'b> { let mut value_typ = typed_value.tipo(); let value_is_data = value_typ.is_data(); + let value_is_opaque = value_typ.is_opaque(); // Check that any type annotation is accurate. let pattern = if let Some(ann) = annotation { @@ -939,6 +940,10 @@ impl<'a, 'b> ExprTyper<'a, 'b> { .type_from_annotation(ann) .map(|t| self.instantiate(t, &mut HashMap::new()))?; + if kind.is_expect() && (ann_typ.is_opaque() || value_is_opaque) { + return Err(Error::ExpectOnOpaqueType { location }); + } + self.unify( ann_typ.clone(), value_typ.clone(), @@ -975,6 +980,10 @@ impl<'a, 'b> ExprTyper<'a, 'b> { }); } + if kind.is_expect() && value_is_opaque { + return Err(Error::ExpectOnOpaqueType { location }); + } + // Ensure the pattern matches the type of the value PatternTyper::new(self.environment, &self.hydrator).unify( untyped_pattern.clone(), diff --git a/crates/aiken-lang/src/tipo/pattern.rs b/crates/aiken-lang/src/tipo/pattern.rs index cf437ec7..3fb595c3 100644 --- a/crates/aiken-lang/src/tipo/pattern.rs +++ b/crates/aiken-lang/src/tipo/pattern.rs @@ -202,7 +202,7 @@ impl<'a, 'b> PatternTyper<'a, 'b> { location, elements, tail, - } => match tipo.get_app_args(true, "", "List", 1, self.environment) { + } => match tipo.get_app_args(true, false, "", "List", 1, self.environment) { Some(args) => { let tipo = args .first() diff --git a/crates/aiken-lang/src/tipo/pretty.rs b/crates/aiken-lang/src/tipo/pretty.rs index 27da2f6e..3a6d7af5 100644 --- a/crates/aiken-lang/src/tipo/pretty.rs +++ b/crates/aiken-lang/src/tipo/pretty.rs @@ -368,6 +368,7 @@ mod tests { module: "whatever".to_string(), name: "Int".to_string(), public: true, + opaque: false, args: vec![], alias: None }, @@ -378,12 +379,14 @@ mod tests { module: "".to_string(), name: "Pair".to_string(), public: true, + opaque: false, alias: None, args: vec![ Rc::new(Type::App { module: "whatever".to_string(), name: "Int".to_string(), public: true, + opaque: false, args: vec![], alias: None }), @@ -391,6 +394,7 @@ mod tests { module: "whatever".to_string(), name: "Bool".to_string(), public: true, + opaque: false, args: vec![], alias: None }), @@ -406,6 +410,7 @@ mod tests { module: "whatever".to_string(), name: "Int".to_string(), public: true, + opaque: false, alias: None, }), Rc::new(Type::App { @@ -413,6 +418,7 @@ mod tests { module: "whatever".to_string(), name: "Bool".to_string(), public: true, + opaque: false, alias: None, }), ], @@ -421,6 +427,7 @@ mod tests { module: "whatever".to_string(), name: "Bool".to_string(), public: true, + opaque: false, alias: None, }), alias: None, @@ -437,6 +444,7 @@ mod tests { module: "whatever".to_string(), name: "Int".to_string(), public: true, + opaque: false, }), })), }, @@ -479,6 +487,7 @@ mod tests { Type::Fn { args: vec![Rc::new(Type::App { public: true, + opaque: false, module: "".to_string(), name: "PRNG".to_string(), args: vec![], @@ -486,12 +495,14 @@ mod tests { })], ret: Rc::new(Type::App { public: true, + opaque: false, module: "".to_string(), name: "Option".to_string(), args: vec![Rc::new(Type::Tuple { elems: vec![ Rc::new(Type::App { public: true, + opaque: false, module: "".to_string(), name: "PRNG".to_string(), args: vec![], @@ -499,6 +510,7 @@ mod tests { }), Rc::new(Type::App { public: true, + opaque: false, module: "".to_string(), name: "Bool".to_string(), args: vec![], @@ -549,6 +561,7 @@ mod tests { Type::Fn { args: vec![Rc::new(Type::App { public: true, + opaque: false, module: "".to_string(), name: "PRNG".to_string(), args: vec![], @@ -556,12 +569,14 @@ mod tests { })], ret: Rc::new(Type::App { public: true, + opaque: false, module: "".to_string(), name: "Option".to_string(), args: vec![Rc::new(Type::Tuple { elems: vec![ Rc::new(Type::App { public: true, + opaque: false, module: "".to_string(), name: "PRNG".to_string(), args: vec![], diff --git a/crates/aiken-project/src/blueprint/snapshots/aiken_project__blueprint__validator__tests__opaque_singleton_multi_variants.snap b/crates/aiken-project/src/blueprint/snapshots/aiken_project__blueprint__validator__tests__opaque_singleton_multi_variants.snap index 333f1d8b..1b65548e 100644 --- a/crates/aiken-project/src/blueprint/snapshots/aiken_project__blueprint__validator__tests__opaque_singleton_multi_variants.snap +++ b/crates/aiken-project/src/blueprint/snapshots/aiken_project__blueprint__validator__tests__opaque_singleton_multi_variants.snap @@ -8,6 +8,7 @@ Schema { breadcrumbs: [ App { public: true, + opaque: true, module: "test_module", name: "Rational", args: [], diff --git a/crates/aiken-project/src/blueprint/snapshots/aiken_project__blueprint__validator__tests__opaque_singleton_variants.snap b/crates/aiken-project/src/blueprint/snapshots/aiken_project__blueprint__validator__tests__opaque_singleton_variants.snap index f5d9e83c..bcfa9f67 100644 --- a/crates/aiken-project/src/blueprint/snapshots/aiken_project__blueprint__validator__tests__opaque_singleton_variants.snap +++ b/crates/aiken-project/src/blueprint/snapshots/aiken_project__blueprint__validator__tests__opaque_singleton_variants.snap @@ -8,6 +8,7 @@ Schema { breadcrumbs: [ App { public: true, + opaque: true, module: "test_module", name: "Dict", args: [ @@ -16,6 +17,7 @@ Schema { value: Link { tipo: App { public: false, + opaque: false, module: "test_module", name: "UUID", args: [], @@ -30,6 +32,7 @@ Schema { value: Link { tipo: App { public: true, + opaque: false, module: "", name: "Int", args: [], From 3820d2af14eecaf84cb0eb75c4a63397d0ae6337 Mon Sep 17 00:00:00 2001 From: KtorZ Date: Wed, 13 Mar 2024 10:03:08 +0100 Subject: [PATCH 02/13] Remove potentially problematic use of ".." in pattern-match Discard pattern are _dangerous_ is used recklessly. The problem comes from maintenance and when adding new fields. We usually don't get any compiler warnings which may lead to missing spots and confusing behaviors. So I have, in some cases, inline discard to explicitly list all fields. That's a bit more cumbersome to write but hopefully will catch a few things for us in the future. --- crates/aiken-lang/src/tipo.rs | 43 +++++---- crates/aiken-lang/src/tipo/environment.rs | 103 ++++++++++++++++------ crates/aiken-lang/src/tipo/expr.rs | 91 +++++++++++-------- crates/aiken-lang/src/tipo/infer.rs | 69 +++++++++++---- 4 files changed, 208 insertions(+), 98 deletions(-) diff --git a/crates/aiken-lang/src/tipo.rs b/crates/aiken-lang/src/tipo.rs index 9a113632..d60cb53b 100644 --- a/crates/aiken-lang/src/tipo.rs +++ b/crates/aiken-lang/src/tipo.rs @@ -81,19 +81,22 @@ impl PartialEq for Type { module, name, args, - .. + opaque, + alias: _, } => { if let Type::App { public: public2, module: module2, name: name2, args: args2, - .. + opaque: opaque2, + alias: _, } = other { name == name2 && module == module2 && public == public2 + && opaque == opaque2 && args.iter().zip(args2).all(|(left, right)| left == right) } else { false @@ -104,7 +107,7 @@ impl PartialEq for Type { if let Type::Fn { args: args2, ret: ret2, - .. + alias: _, } = other { ret == ret2 && args.iter().zip(args2).all(|(left, right)| left == right) @@ -113,7 +116,7 @@ impl PartialEq for Type { } } - Type::Tuple { elems, .. } => { + Type::Tuple { elems, alias: _ } => { if let Type::Tuple { elems: elems2, .. } = other { elems.iter().zip(elems2).all(|(left, right)| left == right) } else { @@ -121,8 +124,12 @@ impl PartialEq for Type { } } - Type::Var { tipo, .. } => { - if let Type::Var { tipo: tipo2, .. } = other { + Type::Var { tipo, alias: _ } => { + if let Type::Var { + tipo: tipo2, + alias: _, + } = other + { tipo == tipo2 } else { false @@ -157,7 +164,7 @@ impl Type { module, name, args, - .. + alias: _, } => Type::App { public, opaque, @@ -166,9 +173,13 @@ impl Type { args, alias, }, - Type::Fn { args, ret, .. } => Type::Fn { args, ret, alias }, - Type::Var { tipo, .. } => Type::Var { tipo, alias }, - Type::Tuple { elems, .. } => Type::Tuple { elems, alias }, + Type::Fn { + args, + ret, + alias: _, + } => Type::Fn { args, ret, alias }, + Type::Var { tipo, alias: _ } => Type::Var { tipo, alias }, + Type::Tuple { elems, alias: _ } => Type::Tuple { elems, alias }, }) } @@ -959,11 +970,13 @@ impl TypeVar { Self::Link { tipo } => tipo.get_inner_types(), Self::Unbound { .. } => vec![], var => { - vec![Type::Var { - tipo: RefCell::new(var.clone()).into(), - alias: None, - } - .into()] + vec![ + Type::Var { + tipo: RefCell::new(var.clone()).into(), + alias: None, + } + .into(), + ] } } } diff --git a/crates/aiken-lang/src/tipo/environment.rs b/crates/aiken-lang/src/tipo/environment.rs index b83482a6..99fd073c 100644 --- a/crates/aiken-lang/src/tipo/environment.rs +++ b/crates/aiken-lang/src/tipo/environment.rs @@ -145,7 +145,12 @@ impl<'a> Environment<'a> { } } - if let Type::Fn { args, ret, .. } = tipo.deref() { + if let Type::Fn { + args, + ret, + alias: _, + } = tipo.deref() + { return if args.len() != arity { Err(Error::IncorrectFunctionCallArity { expected: args.len(), @@ -730,7 +735,7 @@ impl<'a> Environment<'a> { as_name, unqualified, location, - .. + package: _, }) => { let name = module.join("/"); @@ -765,7 +770,6 @@ impl<'a> Environment<'a> { name, location, as_name, - .. } in unqualified { let mut type_imported = false; @@ -990,7 +994,8 @@ impl<'a> Environment<'a> { parameters, location, constructors, - .. + doc: _, + typed_parameters: _, }) => { assert_unique_type_name(names, name, location)?; @@ -1037,7 +1042,8 @@ impl<'a> Environment<'a> { parameters: args, alias: name, annotation: resolved_type, - .. + doc: _, + tipo: _, }) => { assert_unique_type_name(names, name, location)?; @@ -1178,7 +1184,9 @@ impl<'a> Environment<'a> { fun, other_fun, params, - .. + doc: _, + location: _, + end_position: _, }) if kind.is_validator() => { let default_annotation = |mut arg: UntypedArg| { if arg.annotation.is_none() { @@ -1256,7 +1264,10 @@ impl<'a> Environment<'a> { opaque, name, constructors, - .. + doc: _, + location: _, + parameters: _, + typed_parameters: _, }) => { let mut hydrator = hydrators .remove(name) @@ -1299,7 +1310,8 @@ impl<'a> Environment<'a> { label, annotation, location, - .. + tipo: _, + doc: _, }, ) in constructor.arguments.iter().enumerate() { @@ -1389,13 +1401,18 @@ impl<'a> Environment<'a> { } // Collapse right hand side type links. Left hand side will be collapsed in the next block. - if let Type::Var { tipo, .. } = t2.deref() { - if let TypeVar::Link { tipo, .. } = tipo.borrow().deref() { - return self.unify(t1, tipo.clone(), location, allow_cast); + if let Type::Var { tipo, alias } = t2.deref() { + if let TypeVar::Link { tipo } = tipo.borrow().deref() { + return self.unify( + t1, + Type::with_alias(tipo.clone(), alias.clone()), + location, + allow_cast, + ); } } - if let Type::Var { tipo, .. } = t1.deref() { + if let Type::Var { tipo, alias } = t1.deref() { enum Action { Unify(Rc), CouldNotUnify, @@ -1403,7 +1420,9 @@ impl<'a> Environment<'a> { } let action = match tipo.borrow().deref() { - TypeVar::Link { tipo } => Action::Unify(tipo.clone()), + TypeVar::Link { tipo } => { + Action::Unify(Type::with_alias(tipo.clone(), alias.clone())) + } TypeVar::Unbound { id } => { unify_unbound_type(t2.clone(), *id, location)?; @@ -1411,7 +1430,7 @@ impl<'a> Environment<'a> { } TypeVar::Generic { id } => { - if let Type::Var { tipo, .. } = t2.deref() { + if let Type::Var { tipo, alias: _ } = t2.deref() { if tipo.borrow().is_unbound() { *tipo.borrow_mut() = TypeVar::Generic { id: *id }; return Ok(()); @@ -1451,13 +1470,17 @@ impl<'a> Environment<'a> { module: m1, name: n1, args: args1, - .. + public: _, + opaque: _, + alias: _, }, Type::App { module: m2, name: n2, args: args2, - .. + public: _, + opaque: _, + alias: _, }, ) if m1 == m2 && n1 == n2 && args1.len() == args2.len() => { for (a, b) in args1.iter().zip(args2) { @@ -1470,9 +1493,16 @@ impl<'a> Environment<'a> { Ok(()) } - (Type::Tuple { elems: elems1, .. }, Type::Tuple { elems: elems2, .. }) - if elems1.len() == elems2.len() => - { + ( + Type::Tuple { + elems: elems1, + alias: _, + }, + Type::Tuple { + elems: elems2, + alias: _, + }, + ) if elems1.len() == elems2.len() => { for (a, b) in elems1.iter().zip(elems2) { unify_enclosed_type( t1.clone(), @@ -1487,12 +1517,12 @@ impl<'a> Environment<'a> { Type::Fn { args: args1, ret: retrn1, - .. + alias: _, }, Type::Fn { args: args2, ret: retrn2, - .. + alias: _, }, ) if args1.len() == args2.len() => { for (a, b) in args1.iter().zip(args2) { @@ -1678,10 +1708,14 @@ pub enum EntityKind { /// could cause naively-implemented type checking to diverge. /// While traversing the type tree. fn unify_unbound_type(tipo: Rc, own_id: u64, location: Span) -> Result<(), Error> { - if let Type::Var { tipo, .. } = tipo.deref() { + if let Type::Var { tipo, alias } = tipo.deref() { let new_value = match tipo.borrow().deref() { - TypeVar::Link { tipo, .. } => { - return unify_unbound_type(tipo.clone(), own_id, location); + TypeVar::Link { tipo } => { + return unify_unbound_type( + Type::with_alias(tipo.clone(), alias.clone()), + own_id, + location, + ); } TypeVar::Unbound { id } => { @@ -1702,7 +1736,14 @@ fn unify_unbound_type(tipo: Rc, own_id: u64, location: Span) -> Result<(), } match tipo.deref() { - Type::App { args, .. } => { + Type::App { + args, + module: _, + name: _, + public: _, + alias: _, + opaque: _, + } => { for arg in args { unify_unbound_type(arg.clone(), own_id, location)? } @@ -1710,7 +1751,11 @@ fn unify_unbound_type(tipo: Rc, own_id: u64, location: Span) -> Result<(), Ok(()) } - Type::Fn { args, ret, .. } => { + Type::Fn { + args, + ret, + alias: _, + } => { for arg in args { unify_unbound_type(arg.clone(), own_id, location)?; } @@ -1718,7 +1763,7 @@ fn unify_unbound_type(tipo: Rc, own_id: u64, location: Span) -> Result<(), unify_unbound_type(ret.clone(), own_id, location) } - Type::Tuple { elems, .. } => { + Type::Tuple { elems, alias: _ } => { for elem in elems { unify_unbound_type(elem.clone(), own_id, location)? } @@ -1805,9 +1850,9 @@ pub(super) fn assert_no_labeled_arguments(args: &[CallArg]) -> Option<(Spa } pub(super) fn collapse_links(t: Rc) -> Rc { - if let Type::Var { tipo, .. } = t.deref() { + if let Type::Var { tipo, alias } = t.deref() { if let TypeVar::Link { tipo } = tipo.borrow().deref() { - return tipo.clone(); + return Type::with_alias(tipo.clone(), alias.clone()); } } t diff --git a/crates/aiken-lang/src/tipo/expr.rs b/crates/aiken-lang/src/tipo/expr.rs index 9a7e1abd..5777fe4d 100644 --- a/crates/aiken-lang/src/tipo/expr.rs +++ b/crates/aiken-lang/src/tipo/expr.rs @@ -211,10 +211,12 @@ impl<'a, 'b> ExprTyper<'a, 'b> { match expr { UntypedExpr::ErrorTerm { location } => Ok(self.infer_error_term(location)), - UntypedExpr::Var { location, name, .. } => self.infer_var(name, location), + UntypedExpr::Var { location, name } => self.infer_var(name, location), UntypedExpr::UInt { - location, value, .. + location, + value, + base: _, } => Ok(self.infer_uint(value, location)), UntypedExpr::Sequence { @@ -222,13 +224,9 @@ impl<'a, 'b> ExprTyper<'a, 'b> { location, } => self.infer_seq(location, expressions), - UntypedExpr::Tuple { - location, elems, .. - } => self.infer_tuple(elems, location), + UntypedExpr::Tuple { location, elems } => self.infer_tuple(elems, location), - UntypedExpr::String { - location, value, .. - } => Ok(self.infer_string(value, location)), + UntypedExpr::String { location, value } => Ok(self.infer_string(value, location)), UntypedExpr::LogicalOpChain { kind, @@ -244,7 +242,6 @@ impl<'a, 'b> ExprTyper<'a, 'b> { arguments: args, body, return_annotation, - .. } => self.infer_fn( args, &[], @@ -265,7 +262,6 @@ impl<'a, 'b> ExprTyper<'a, 'b> { patterns, value, kind, - .. } => { // at this point due to backpassing rewrites, // patterns is guaranteed to have one item @@ -295,14 +291,12 @@ impl<'a, 'b> ExprTyper<'a, 'b> { location, elements, tail, - .. } => self.infer_list(elements, tail, location), UntypedExpr::Call { location, fun, arguments: args, - .. } => self.infer_call(*fun, args, location), UntypedExpr::BinOp { @@ -310,21 +304,18 @@ impl<'a, 'b> ExprTyper<'a, 'b> { name, left, right, - .. } => self.infer_binop(name, *left, *right, location), UntypedExpr::FieldAccess { location, label, container, - .. } => self.infer_field_access(*container, label, location), UntypedExpr::TupleIndex { location, index, tuple, - .. } => self.infer_tuple_index(*tuple, index, location), UntypedExpr::ByteArray { @@ -334,7 +325,9 @@ impl<'a, 'b> ExprTyper<'a, 'b> { } => self.infer_bytearray(bytes, preferred_format, location), UntypedExpr::CurvePoint { - location, point, .. + location, + point, + preferred_format: _, } => self.infer_curve_point(*point, location), UntypedExpr::RecordUpdate { @@ -578,7 +571,10 @@ impl<'a, 'b> ExprTyper<'a, 'b> { ValueConstructorVariant::Record { field_map: Some(field_map), constructors_count, - .. + name: _, + arity: _, + location: _, + module: _, } => (field_map, *constructors_count), _ => { return Err(Error::RecordUpdateInvalidConstructor { @@ -704,7 +700,7 @@ impl<'a, 'b> ExprTyper<'a, 'b> { Ok(record_access) => Ok(record_access), Err(err) => match container { - UntypedExpr::Var { name, location, .. } => { + UntypedExpr::Var { name, location } => { let module_access = self.infer_module_access(&name, label, &location, access_location); @@ -894,7 +890,7 @@ impl<'a, 'b> ExprTyper<'a, 'b> { annotation, location, doc, - .. + tipo: _, } = arg; let tipo = annotation @@ -1087,7 +1083,8 @@ impl<'a, 'b> ExprTyper<'a, 'b> { ( Type::Fn { args: expected_arguments, - .. + ret: _, + alias: _, }, UntypedExpr::Fn { arguments, @@ -1095,7 +1092,6 @@ impl<'a, 'b> ExprTyper<'a, 'b> { return_annotation, location, fn_style, - .. }, ) if fn_style != FnStyle::Capture && expected_arguments.len() == arguments.len() => { self.infer_fn( @@ -1398,9 +1394,7 @@ impl<'a, 'b> ExprTyper<'a, 'b> { base, }), - Constant::String { - location, value, .. - } => Ok(Constant::String { location, value }), + Constant::String { location, value } => Ok(Constant::String { location, value }), Constant::ByteArray { location, @@ -1748,7 +1742,6 @@ impl<'a, 'b> ExprTyper<'a, 'b> { value, kind, patterns, - .. } = breakpoint else { unreachable!("backpass misuse: breakpoint isn't an Assignment ?!"); @@ -1776,7 +1769,9 @@ impl<'a, 'b> ExprTyper<'a, 'b> { // in front of the continuation sequence. This is because we do not support patterns in function argument // (which is perhaps something we should support?). match pattern { - Pattern::Var { name, .. } | Pattern::Discard { name, .. } if kind.is_let() => { + Pattern::Var { name, location: _ } | Pattern::Discard { name, location: _ } + if kind.is_let() => + { names.push((name.clone(), annotation)); } _ => { @@ -1806,7 +1801,11 @@ impl<'a, 'b> ExprTyper<'a, 'b> { } match *value { - UntypedExpr::Call { fun, arguments, .. } => { + UntypedExpr::Call { + fun, + arguments, + location: _, + } => { let mut new_arguments = Vec::new(); new_arguments.extend(arguments); new_arguments.push(CallArg { @@ -1832,7 +1831,8 @@ impl<'a, 'b> ExprTyper<'a, 'b> { fn_style, ref arguments, ref return_annotation, - .. + location: _, + body: _, } => { let return_annotation = return_annotation.clone(); @@ -1891,7 +1891,10 @@ impl<'a, 'b> ExprTyper<'a, 'b> { breakpoint = Some(expression); } UntypedExpr::Assignment { - patterns, location, .. + patterns, + location, + value: _, + kind: _, } if patterns.len() > 1 => { return Err(Error::UnexpectedMultiPatternAssignment { arrow: patterns @@ -2012,7 +2015,10 @@ impl<'a, 'b> ExprTyper<'a, 'b> { let tuple = self.infer(tuple)?; let tipo = match *tuple.tipo() { - Type::Tuple { ref elems, .. } => { + Type::Tuple { + ref elems, + alias: _, + } => { let size = elems.len(); if index >= size { Err(Error::TupleIndexOutOfBound { @@ -2341,7 +2347,14 @@ fn assert_assignment(expr: TypedExpr) -> Result { pub fn ensure_serialisable(allow_fn: bool, t: Rc, location: Span) -> Result<(), Error> { match t.deref() { - Type::App { args, .. } => { + Type::App { + args, + name: _, + module: _, + public: _, + opaque: _, + alias: _, + } => { if t.is_ml_result() { return Err(Error::IllegalTypeInData { tipo: t.clone(), @@ -2356,7 +2369,7 @@ pub fn ensure_serialisable(allow_fn: bool, t: Rc, location: Span) -> Resul Ok(()) } - Type::Tuple { elems, .. } => { + Type::Tuple { elems, alias: _ } => { elems .iter() .map(|e| ensure_serialisable(false, e.clone(), location)) @@ -2365,7 +2378,11 @@ pub fn ensure_serialisable(allow_fn: bool, t: Rc, location: Span) -> Resul Ok(()) } - Type::Fn { args, ret, .. } => { + Type::Fn { + args, + ret, + alias: _, + } => { if !allow_fn { return Err(Error::IllegalTypeInData { tipo: t.clone(), @@ -2380,10 +2397,14 @@ pub fn ensure_serialisable(allow_fn: bool, t: Rc, location: Span) -> Resul ensure_serialisable(allow_fn, ret.clone(), location) } - Type::Var { tipo, .. } => match tipo.borrow().deref() { + Type::Var { tipo, alias } => match tipo.borrow().deref() { TypeVar::Unbound { .. } => Ok(()), TypeVar::Generic { .. } => Ok(()), - TypeVar::Link { tipo } => ensure_serialisable(allow_fn, tipo.clone(), location), + TypeVar::Link { tipo } => ensure_serialisable( + allow_fn, + Type::with_alias(tipo.clone(), alias.clone()), + location, + ), }, } } diff --git a/crates/aiken-lang/src/tipo/infer.rs b/crates/aiken-lang/src/tipo/infer.rs index 08f17972..e40c92f6 100644 --- a/crates/aiken-lang/src/tipo/infer.rs +++ b/crates/aiken-lang/src/tipo/infer.rs @@ -197,7 +197,8 @@ fn infer_definition( ArgName::Named { name, is_validator_param, - .. + label: _, + location: _, } if *is_validator_param => { environment.insert_variable( name.to_string(), @@ -383,7 +384,9 @@ fn infer_definition( .get_mut(&f.name) .expect("Could not find preregistered type for test"); if let Type::Fn { - ref ret, ref alias, .. + ref ret, + ref alias, + args: _, } = scope.tipo.as_ref() { scope.tipo = Rc::new(Type::Fn { @@ -425,7 +428,11 @@ fn infer_definition( arguments: match typed_via { Some((via, tipo)) => { let Arg { - arg_name, location, .. + arg_name, + location, + annotation: _, + doc: _, + tipo: _, } = typed_f .arguments .first() @@ -457,7 +464,7 @@ fn infer_definition( alias, parameters, annotation, - .. + tipo: _, }) => { let tipo = environment .get_type_constructor(&None, &alias, location) @@ -484,7 +491,7 @@ fn infer_definition( name, parameters, constructors: untyped_constructors, - .. + typed_parameters: _, }) => { let constructors = untyped_constructors .into_iter() @@ -514,7 +521,7 @@ fn infer_definition( annotation, location, doc, - .. + tipo: _, }, t, )| { @@ -561,7 +568,14 @@ fn infer_definition( }; for constr in &typed_data.constructors { - for RecordConstructorArg { tipo, location, .. } in &constr.arguments { + for RecordConstructorArg { + tipo, + location, + doc: _, + label: _, + annotation: _, + } in &constr.arguments + { if tipo.is_function() { return Err(Error::FunctionTypeInData { location: *location, @@ -585,7 +599,7 @@ fn infer_definition( module, as_name, unqualified, - .. + package: _, }) => { let name = module.join("/"); @@ -616,7 +630,7 @@ fn infer_definition( annotation, public, value, - .. + tipo: _, }) => { let typed_expr = ExprTyper::new(environment, lines, tracing).infer_const(&annotation, *value)?; @@ -672,7 +686,7 @@ fn infer_function( return_annotation, end_position, can_error, - .. + return_type: _, } = f; let preregistered_fn = environment @@ -773,9 +787,18 @@ fn infer_fuzzer( }; match tipo.borrow() { - Type::Fn { ret, .. } => match ret.borrow() { + Type::Fn { + ret, + args: _, + alias: _, + } => match ret.borrow() { Type::App { - module, name, args, .. + module, + name, + args, + public: _, + opaque: _, + alias: _, } if module.is_empty() && name == "Option" && args.len() == 1 => { match args.first().expect("args.len() == 1").borrow() { Type::Tuple { elems, .. } if elems.len() == 2 => { @@ -805,10 +828,13 @@ fn infer_fuzzer( _ => Err(could_not_unify()), }, - Type::Var { tipo, .. } => match &*tipo.deref().borrow() { - TypeVar::Link { tipo } => { - infer_fuzzer(environment, expected_inner_type, tipo, location) - } + Type::Var { tipo, alias } => match &*tipo.deref().borrow() { + TypeVar::Link { tipo } => infer_fuzzer( + environment, + expected_inner_type, + &Type::with_alias(tipo.clone(), alias.clone()), + location, + ), _ => Err(Error::GenericLeftAtBoundary { location: *location, }), @@ -821,7 +847,12 @@ fn infer_fuzzer( fn annotate_fuzzer(tipo: &Type, location: &Span) -> Result { match tipo { Type::App { - name, module, args, .. + name, + module, + args, + public: _, + opaque: _, + alias: _, } => { let arguments = args .iter() @@ -839,7 +870,7 @@ fn annotate_fuzzer(tipo: &Type, location: &Span) -> Result { }) } - Type::Tuple { elems, .. } => { + Type::Tuple { elems, alias: _ } => { let elems = elems .iter() .map(|arg| annotate_fuzzer(arg, location)) @@ -850,7 +881,7 @@ fn annotate_fuzzer(tipo: &Type, location: &Span) -> Result { }) } - Type::Var { tipo, .. } => match &*tipo.deref().borrow() { + Type::Var { tipo, alias: _ } => match &*tipo.deref().borrow() { TypeVar::Link { tipo } => annotate_fuzzer(tipo, location), _ => Err(Error::GenericLeftAtBoundary { location: *location, From 961806617f73536f6b68a702a15ff948878ed6a5 Mon Sep 17 00:00:00 2001 From: KtorZ Date: Wed, 13 Mar 2024 13:05:44 +0100 Subject: [PATCH 03/13] Add more failing tests for expecting on into opaque types. --- crates/aiken-lang/src/tests/check.rs | 82 ++++++++++++++++++++++++++-- 1 file changed, 78 insertions(+), 4 deletions(-) diff --git a/crates/aiken-lang/src/tests/check.rs b/crates/aiken-lang/src/tests/check.rs index 0bc67def..f794d7b2 100644 --- a/crates/aiken-lang/src/tests/check.rs +++ b/crates/aiken-lang/src/tests/check.rs @@ -1812,16 +1812,15 @@ fn forbid_expect_into_opaque_type_from_data() { } #[test] -fn forbid_expect_into_opaque_type_constructor() { +fn forbid_expect_into_opaque_type_constructor_without_typecasting() { let source_code = r#" opaque type Thing { Foo(Int) Bar(Int) } - fn bar(n: Thing) { - expect Foo(a) = n - + fn bar(thing: Thing) { + expect Foo(a) = thing a } "#; @@ -1831,3 +1830,78 @@ fn forbid_expect_into_opaque_type_constructor() { Err((_, Error::ExpectOnOpaqueType { .. })) )) } + +#[test] +fn forbid_expect_into_opaque_type_constructor_with_typecasting() { + let source_code = r#" + opaque type Thing { + Foo(Int) + Bar(Int) + } + + fn bar(data: Data) { + expect Foo(a): Thing = data + a + } + "#; + + assert!(matches!( + check(parse(source_code)), + Err((_, Error::ExpectOnOpaqueType { .. })) + )) +} + +#[test] +fn forbid_expect_into_nested_opaque_in_record_without_typecasting() { + let source_code = r#" + opaque type Thing { inner: Int } + + type Foo { foo: Thing } + + fn bar(thing: Thing) { + expect Foo { foo: Thing { inner } } : Foo = thing + Void + } + "#; + + assert!(matches!( + check(parse(source_code)), + Err((_, Error::ExpectOnOpaqueType { .. })) + )) +} + +#[test] +fn forbid_expect_into_nested_opaque_in_record_with_typecasting() { + let source_code = r#" + opaque type Thing { inner: Int } + + type Foo { foo: Thing } + + fn bar(a: Data) { + expect Foo { foo: Thing { inner } } : Foo = a + Void + } + "#; + + assert!(matches!( + check(parse(source_code)), + Err((_, Error::ExpectOnOpaqueType { .. })) + )) +} + +#[test] +fn forbid_expect_into_nested_opaque_in_list() { + let source_code = r#" + opaque type Thing { inner: Int } + + fn bar(a: Data) { + expect [x]: List = [a] + Void + } + "#; + + assert!(matches!( + check(parse(source_code)), + Err((_, Error::ExpectOnOpaqueType { .. })) + )) +} From 502a13756a9e299624832366f09d825eba1c0f1b Mon Sep 17 00:00:00 2001 From: KtorZ Date: Wed, 13 Mar 2024 13:06:17 +0100 Subject: [PATCH 04/13] remove irrelevant comment. --- crates/aiken-lang/src/ast.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/aiken-lang/src/ast.rs b/crates/aiken-lang/src/ast.rs index e2dec29f..750d7cc4 100644 --- a/crates/aiken-lang/src/ast.rs +++ b/crates/aiken-lang/src/ast.rs @@ -1225,7 +1225,6 @@ impl TypedPattern { } pub fn tipo(&self, value: &TypedExpr) -> Option> { - // expect thing: Wow = thing match self { Pattern::Int { .. } => Some(builtins::int()), Pattern::Constructor { tipo, .. } => Some(tipo.clone()), From 22b618116e6ede3e37517e560f72ec1c4d4fc0dc Mon Sep 17 00:00:00 2001 From: KtorZ Date: Wed, 13 Mar 2024 13:07:07 +0100 Subject: [PATCH 05/13] rename function argument for clarity is_assignment was a bit confusing to me since we do actually categorize expect as 'assignment'. So this is more about whether this is a *let* assignment. Hence 'is_let'. --- crates/aiken-lang/src/tipo/pattern.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/aiken-lang/src/tipo/pattern.rs b/crates/aiken-lang/src/tipo/pattern.rs index 3fb595c3..a8b055fe 100644 --- a/crates/aiken-lang/src/tipo/pattern.rs +++ b/crates/aiken-lang/src/tipo/pattern.rs @@ -141,11 +141,11 @@ impl<'a, 'b> PatternTyper<'a, 'b> { pattern: UntypedPattern, tipo: Rc, ann_type: Option>, - is_assignment: bool, + is_let: bool, ) -> Result { match pattern { Pattern::Discard { name, location } => { - if is_assignment { + if is_let { // Register declaration for the unused variable detection self.environment .warnings From f10cf739057a3e217d0d494183f6e4faedc6cf77 Mon Sep 17 00:00:00 2001 From: KtorZ Date: Wed, 13 Mar 2024 13:14:38 +0100 Subject: [PATCH 06/13] Rework 'is_opaque' to also check for inner types. Also removed the duplication in infer_assignment and moved the check down. --- crates/aiken-lang/src/tipo.rs | 17 ++++++++++--- crates/aiken-lang/src/tipo/expr.rs | 39 ++++++++++++------------------ 2 files changed, 29 insertions(+), 27 deletions(-) diff --git a/crates/aiken-lang/src/tipo.rs b/crates/aiken-lang/src/tipo.rs index d60cb53b..2144c644 100644 --- a/crates/aiken-lang/src/tipo.rs +++ b/crates/aiken-lang/src/tipo.rs @@ -195,10 +195,14 @@ impl Type { } } - pub fn is_opaque(&self) -> bool { + pub fn is_or_holds_opaque(&self) -> bool { match self { - Type::App { opaque, .. } => *opaque, - _ => false, + Type::Var { tipo, .. } => tipo.borrow().is_or_holds_opaque(), + Type::App { opaque, args, .. } => { + *opaque || args.iter().any(|arg| arg.is_or_holds_opaque()) + } + Type::Tuple { elems, .. } => elems.iter().any(|elem| elem.is_or_holds_opaque()), + Type::Fn { .. } => false, } } @@ -845,6 +849,13 @@ impl TypeVar { matches!(self, Self::Unbound { .. }) } + pub fn is_or_holds_opaque(&self) -> bool { + match self { + Self::Link { tipo } => tipo.is_or_holds_opaque(), + _ => false, + } + } + pub fn is_void(&self) -> bool { match self { Self::Link { tipo } => tipo.is_void(), diff --git a/crates/aiken-lang/src/tipo/expr.rs b/crates/aiken-lang/src/tipo/expr.rs index 5777fe4d..2c14a274 100644 --- a/crates/aiken-lang/src/tipo/expr.rs +++ b/crates/aiken-lang/src/tipo/expr.rs @@ -928,18 +928,13 @@ impl<'a, 'b> ExprTyper<'a, 'b> { let mut value_typ = typed_value.tipo(); let value_is_data = value_typ.is_data(); - let value_is_opaque = value_typ.is_opaque(); // Check that any type annotation is accurate. - let pattern = if let Some(ann) = annotation { + let ann_typ = if let Some(ann) = annotation { let ann_typ = self .type_from_annotation(ann) .map(|t| self.instantiate(t, &mut HashMap::new()))?; - if kind.is_expect() && (ann_typ.is_opaque() || value_is_opaque) { - return Err(Error::ExpectOnOpaqueType { location }); - } - self.unify( ann_typ.clone(), value_typ.clone(), @@ -949,13 +944,7 @@ impl<'a, 'b> ExprTyper<'a, 'b> { value_typ = ann_typ.clone(); - // Ensure the pattern matches the type of the value - PatternTyper::new(self.environment, &self.hydrator).unify( - untyped_pattern.clone(), - value_typ.clone(), - Some(ann_typ), - kind.is_let(), - )? + Some(ann_typ) } else { if value_is_data && !untyped_pattern.is_var() && !untyped_pattern.is_discard() { let ann = Annotation::Constructor { @@ -976,19 +965,21 @@ impl<'a, 'b> ExprTyper<'a, 'b> { }); } - if kind.is_expect() && value_is_opaque { - return Err(Error::ExpectOnOpaqueType { location }); - } - - // Ensure the pattern matches the type of the value - PatternTyper::new(self.environment, &self.hydrator).unify( - untyped_pattern.clone(), - value_typ.clone(), - None, - kind.is_let(), - )? + None }; + if kind.is_expect() && value_typ.is_or_holds_opaque() { + return Err(Error::ExpectOnOpaqueType { location }); + } + + // Ensure the pattern matches the type of the value + let pattern = PatternTyper::new(self.environment, &self.hydrator).unify( + untyped_pattern.clone(), + value_typ.clone(), + ann_typ, + kind.is_let(), + )?; + // If `expect` is explicitly used, we still check exhaustiveness but instead of returning an // error we emit a warning which explains that using `expect` is unnecessary. match kind { From 8f31b45e367fd48044d739cd6db00955f2fe9ff8 Mon Sep 17 00:00:00 2001 From: KtorZ Date: Wed, 13 Mar 2024 13:25:30 +0100 Subject: [PATCH 07/13] Fix allow_casting condition in unification We should allow casting from any type to any type. Or at the very least, allow it for well-known types like List. --- crates/aiken-lang/src/tipo/expr.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/aiken-lang/src/tipo/expr.rs b/crates/aiken-lang/src/tipo/expr.rs index 2c14a274..6b4694ae 100644 --- a/crates/aiken-lang/src/tipo/expr.rs +++ b/crates/aiken-lang/src/tipo/expr.rs @@ -939,7 +939,7 @@ impl<'a, 'b> ExprTyper<'a, 'b> { ann_typ.clone(), value_typ.clone(), typed_value.type_defining_location(), - (kind.is_let() && ann_typ.is_data()) || (kind.is_expect() && value_is_data), + (kind.is_let() && ann_typ.is_data()) || kind.is_expect(), )?; value_typ = ann_typ.clone(); From 9127dcdd6eea58bd96e52cadda06e87b1b3cbd90 Mon Sep 17 00:00:00 2001 From: KtorZ Date: Wed, 13 Mar 2024 13:25:51 +0100 Subject: [PATCH 08/13] Add note on the type-casting check. --- crates/aiken-lang/src/tipo/expr.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/crates/aiken-lang/src/tipo/expr.rs b/crates/aiken-lang/src/tipo/expr.rs index 6b4694ae..459ae756 100644 --- a/crates/aiken-lang/src/tipo/expr.rs +++ b/crates/aiken-lang/src/tipo/expr.rs @@ -968,10 +968,6 @@ impl<'a, 'b> ExprTyper<'a, 'b> { None }; - if kind.is_expect() && value_typ.is_or_holds_opaque() { - return Err(Error::ExpectOnOpaqueType { location }); - } - // Ensure the pattern matches the type of the value let pattern = PatternTyper::new(self.environment, &self.hydrator).unify( untyped_pattern.clone(), @@ -980,6 +976,14 @@ impl<'a, 'b> ExprTyper<'a, 'b> { kind.is_let(), )?; + // FIXME: This check is insufficient as we need to also assert the type + // definition itself since there might be nested opaque types on the rhs. + // + // For that, we must lookup + if kind.is_expect() && value_typ.is_or_holds_opaque() { + return Err(Error::ExpectOnOpaqueType { location }); + } + // If `expect` is explicitly used, we still check exhaustiveness but instead of returning an // error we emit a warning which explains that using `expect` is unnecessary. match kind { From e71470747f9c1a5e86cb394c010dc2508737f387 Mon Sep 17 00:00:00 2001 From: rvcas Date: Wed, 13 Mar 2024 20:15:27 -0400 Subject: [PATCH 09/13] feat: fix some tests and add a failing one --- crates/aiken-lang/src/builtins.rs | 28 +++--- crates/aiken-lang/src/tests/check.rs | 17 +++- crates/aiken-lang/src/tipo.rs | 57 +++++++----- crates/aiken-lang/src/tipo/environment.rs | 35 +++++-- crates/aiken-lang/src/tipo/expr.rs | 8 +- crates/aiken-lang/src/tipo/infer.rs | 107 +++++++++++----------- crates/aiken-lang/src/tipo/pattern.rs | 1 + crates/aiken-lang/src/tipo/pretty.rs | 30 +++--- 8 files changed, 164 insertions(+), 119 deletions(-) diff --git a/crates/aiken-lang/src/builtins.rs b/crates/aiken-lang/src/builtins.rs index 962e4ca4..6b18f941 100644 --- a/crates/aiken-lang/src/builtins.rs +++ b/crates/aiken-lang/src/builtins.rs @@ -1267,7 +1267,7 @@ pub fn prelude_data_types(id_gen: &IdGenerator) -> IndexMap Rc { Rc::new(Type::App { public: true, - opaque: false, + contains_opaque: false, name: INT.to_string(), module: "".to_string(), args: vec![], @@ -1278,7 +1278,7 @@ pub fn int() -> Rc { pub fn data() -> Rc { Rc::new(Type::App { public: true, - opaque: false, + contains_opaque: false, name: DATA.to_string(), module: "".to_string(), args: vec![], @@ -1290,7 +1290,7 @@ pub fn byte_array() -> Rc { Rc::new(Type::App { args: vec![], public: true, - opaque: false, + contains_opaque: false, name: BYTE_ARRAY.to_string(), module: "".to_string(), alias: None, @@ -1300,7 +1300,7 @@ pub fn byte_array() -> Rc { pub fn g1_element() -> Rc { Rc::new(Type::App { public: true, - opaque: false, + contains_opaque: false, module: "".to_string(), name: G1_ELEMENT.to_string(), args: vec![], @@ -1311,7 +1311,7 @@ pub fn g1_element() -> Rc { pub fn g2_element() -> Rc { Rc::new(Type::App { public: true, - opaque: false, + contains_opaque: false, module: "".to_string(), name: G2_ELEMENT.to_string(), args: vec![], @@ -1322,7 +1322,7 @@ pub fn g2_element() -> Rc { pub fn miller_loop_result() -> Rc { Rc::new(Type::App { public: true, - opaque: false, + contains_opaque: false, module: "".to_string(), name: MILLER_LOOP_RESULT.to_string(), args: vec![], @@ -1338,7 +1338,7 @@ pub fn bool() -> Rc { Rc::new(Type::App { args: vec![], public: true, - opaque: false, + contains_opaque: false, name: BOOL.to_string(), module: "".to_string(), alias: None, @@ -1349,7 +1349,7 @@ pub fn prng() -> Rc { Rc::new(Type::App { args: vec![], public: true, - opaque: false, + contains_opaque: false, name: PRNG.to_string(), module: "".to_string(), alias: None, @@ -1400,7 +1400,7 @@ pub fn fuzzer(a: Rc) -> Rc { pub fn list(t: Rc) -> Rc { Rc::new(Type::App { public: true, - opaque: false, + contains_opaque: false, name: LIST.to_string(), module: "".to_string(), args: vec![t], @@ -1412,7 +1412,7 @@ pub fn string() -> Rc { Rc::new(Type::App { args: vec![], public: true, - opaque: false, + contains_opaque: false, name: STRING.to_string(), module: "".to_string(), alias: None, @@ -1423,7 +1423,7 @@ pub fn void() -> Rc { Rc::new(Type::App { args: vec![], public: true, - opaque: false, + contains_opaque: false, name: VOID.to_string(), module: "".to_string(), alias: None, @@ -1433,7 +1433,7 @@ pub fn void() -> Rc { pub fn option(a: Rc) -> Rc { Rc::new(Type::App { public: true, - opaque: false, + contains_opaque: false, name: OPTION.to_string(), module: "".to_string(), args: vec![a], @@ -1444,7 +1444,7 @@ pub fn option(a: Rc) -> Rc { pub fn ordering() -> Rc { Rc::new(Type::App { public: true, - opaque: false, + contains_opaque: false, name: ORDERING.to_string(), module: "".to_string(), args: vec![], @@ -1475,7 +1475,7 @@ pub fn unbound_var(id: u64) -> Rc { pub fn wrapped_redeemer(redeemer: Rc) -> Rc { Rc::new(Type::App { public: true, - opaque: false, + contains_opaque: false, module: "".to_string(), name: REDEEMER_WRAPPER.to_string(), args: vec![redeemer], diff --git a/crates/aiken-lang/src/tests/check.rs b/crates/aiken-lang/src/tests/check.rs index f794d7b2..9d635cec 100644 --- a/crates/aiken-lang/src/tests/check.rs +++ b/crates/aiken-lang/src/tests/check.rs @@ -1858,7 +1858,7 @@ fn forbid_expect_into_nested_opaque_in_record_without_typecasting() { type Foo { foo: Thing } - fn bar(thing: Thing) { + fn bar(thing: Foo) { expect Foo { foo: Thing { inner } } : Foo = thing Void } @@ -1905,3 +1905,18 @@ fn forbid_expect_into_nested_opaque_in_list() { Err((_, Error::ExpectOnOpaqueType { .. })) )) } + +#[test] +fn allow_expect_on_var_patterns_that_are_opaque() { + let source_code = r#" + opaque type Thing { inner: Int } + + fn bar(a: Option) { + expect Some(thing) = a + + thing.inner + } + "#; + + assert!(check(parse(source_code)).is_ok()) +} diff --git a/crates/aiken-lang/src/tipo.rs b/crates/aiken-lang/src/tipo.rs index 2144c644..ad206b9e 100644 --- a/crates/aiken-lang/src/tipo.rs +++ b/crates/aiken-lang/src/tipo.rs @@ -42,7 +42,7 @@ pub enum Type { /// App { public: bool, - opaque: bool, + contains_opaque: bool, module: String, name: String, args: Vec>, @@ -81,7 +81,7 @@ impl PartialEq for Type { module, name, args, - opaque, + contains_opaque: opaque, alias: _, } => { if let Type::App { @@ -89,7 +89,7 @@ impl PartialEq for Type { module: module2, name: name2, args: args2, - opaque: opaque2, + contains_opaque: opaque2, alias: _, } = other { @@ -160,14 +160,14 @@ impl Type { Rc::new(match self { Type::App { public, - opaque, + contains_opaque: opaque, module, name, args, alias: _, } => Type::App { public, - opaque, + contains_opaque: opaque, module, name, args, @@ -195,17 +195,30 @@ impl Type { } } - pub fn is_or_holds_opaque(&self) -> bool { + pub fn contains_opaque(&self) -> bool { match self { Type::Var { tipo, .. } => tipo.borrow().is_or_holds_opaque(), - Type::App { opaque, args, .. } => { - *opaque || args.iter().any(|arg| arg.is_or_holds_opaque()) - } - Type::Tuple { elems, .. } => elems.iter().any(|elem| elem.is_or_holds_opaque()), + Type::App { + contains_opaque: opaque, + args, + .. + } => *opaque || args.iter().any(|arg| arg.contains_opaque()), + Type::Tuple { elems, .. } => elems.iter().any(|elem| elem.contains_opaque()), Type::Fn { .. } => false, } } + pub fn set_opaque(&mut self, opaque: bool) { + match self { + Type::App { + contains_opaque, .. + } => { + *contains_opaque = opaque; + } + Type::Fn { .. } | Type::Var { .. } | Type::Tuple { .. } => (), + } + } + pub fn is_unbound(&self) -> bool { matches!(self, Self::Var { tipo, .. } if tipo.borrow().is_unbound()) } @@ -511,7 +524,7 @@ impl Type { *tipo.borrow_mut() = TypeVar::Link { tipo: Rc::new(Self::App { public, - opaque, + contains_opaque: opaque, name: name.to_string(), module: module.to_owned(), args: args.clone(), @@ -666,7 +679,7 @@ pub fn convert_opaque_type( match t.as_ref() { Type::App { public, - opaque, + contains_opaque: opaque, module, name, args, @@ -679,7 +692,7 @@ pub fn convert_opaque_type( } Type::App { public: *public, - opaque: *opaque, + contains_opaque: *opaque, module: module.clone(), name: name.clone(), args: new_args, @@ -754,7 +767,7 @@ pub fn find_and_replace_generics( Type::App { args, public, - opaque, + contains_opaque: opaque, module, name, alias, @@ -767,7 +780,7 @@ pub fn find_and_replace_generics( let t = Type::App { args: new_args, public: *public, - opaque: *opaque, + contains_opaque: *opaque, module: module.clone(), name: name.clone(), alias: alias.clone(), @@ -851,7 +864,7 @@ impl TypeVar { pub fn is_or_holds_opaque(&self) -> bool { match self { - Self::Link { tipo } => tipo.is_or_holds_opaque(), + Self::Link { tipo } => tipo.contains_opaque(), _ => false, } } @@ -981,13 +994,11 @@ impl TypeVar { Self::Link { tipo } => tipo.get_inner_types(), Self::Unbound { .. } => vec![], var => { - vec![ - Type::Var { - tipo: RefCell::new(var.clone()).into(), - alias: None, - } - .into(), - ] + vec![Type::Var { + tipo: RefCell::new(var.clone()).into(), + alias: None, + } + .into()] } } } diff --git a/crates/aiken-lang/src/tipo/environment.rs b/crates/aiken-lang/src/tipo/environment.rs index 99fd073c..ec104130 100644 --- a/crates/aiken-lang/src/tipo/environment.rs +++ b/crates/aiken-lang/src/tipo/environment.rs @@ -301,6 +301,25 @@ impl<'a> Environment<'a> { } } + pub fn get_type_constructor_mut( + &mut self, + name: &str, + location: Span, + ) -> Result<&mut TypeConstructor, Error> { + let types = self.module_types.keys().map(|t| t.to_string()).collect(); + + let constructor = self + .module_types + .get_mut(name) + .ok_or_else(|| Error::UnknownType { + location, + name: name.to_string(), + types, + })?; + + Ok(constructor) + } + /// Lookup a type in the current scope. pub fn get_type_constructor( &mut self, @@ -551,7 +570,7 @@ impl<'a> Environment<'a> { match t.deref() { Type::App { public, - opaque, + contains_opaque: opaque, name, module, args, @@ -564,7 +583,7 @@ impl<'a> Environment<'a> { Rc::new(Type::App { public: *public, - opaque: *opaque, + contains_opaque: *opaque, name: name.clone(), module: module.clone(), alias: alias.clone(), @@ -1006,7 +1025,7 @@ impl<'a> Environment<'a> { let tipo = Rc::new(Type::App { public: *public, - opaque: *opaque, + contains_opaque: *opaque, module: module.to_owned(), name: name.clone(), args: parameters.clone(), @@ -1471,7 +1490,7 @@ impl<'a> Environment<'a> { name: n1, args: args1, public: _, - opaque: _, + contains_opaque: _, alias: _, }, Type::App { @@ -1479,7 +1498,7 @@ impl<'a> Environment<'a> { name: n2, args: args2, public: _, - opaque: _, + contains_opaque: _, alias: _, }, ) if m1 == m2 && n1 == n2 && args1.len() == args2.len() => { @@ -1742,7 +1761,7 @@ fn unify_unbound_type(tipo: Rc, own_id: u64, location: Span) -> Result<(), name: _, public: _, alias: _, - opaque: _, + contains_opaque: _, } => { for arg in args { unify_unbound_type(arg.clone(), own_id, location)? @@ -1906,7 +1925,7 @@ pub(crate) fn generalise(t: Rc, ctx_level: usize) -> Rc { Type::App { public, - opaque, + contains_opaque: opaque, module, name, args, @@ -1919,7 +1938,7 @@ pub(crate) fn generalise(t: Rc, ctx_level: usize) -> Rc { Rc::new(Type::App { public: *public, - opaque: *opaque, + contains_opaque: *opaque, module: module.clone(), name: name.clone(), args, diff --git a/crates/aiken-lang/src/tipo/expr.rs b/crates/aiken-lang/src/tipo/expr.rs index 459ae756..40e5f3e1 100644 --- a/crates/aiken-lang/src/tipo/expr.rs +++ b/crates/aiken-lang/src/tipo/expr.rs @@ -976,11 +976,7 @@ impl<'a, 'b> ExprTyper<'a, 'b> { kind.is_let(), )?; - // FIXME: This check is insufficient as we need to also assert the type - // definition itself since there might be nested opaque types on the rhs. - // - // For that, we must lookup - if kind.is_expect() && value_typ.is_or_holds_opaque() { + if kind.is_expect() && value_typ.contains_opaque() { return Err(Error::ExpectOnOpaqueType { location }); } @@ -2347,7 +2343,7 @@ pub fn ensure_serialisable(allow_fn: bool, t: Rc, location: Span) -> Resul name: _, module: _, public: _, - opaque: _, + contains_opaque: _, alias: _, } => { if t.is_ml_result() { diff --git a/crates/aiken-lang/src/tipo/infer.rs b/crates/aiken-lang/src/tipo/infer.rs index e40c92f6..e9369acb 100644 --- a/crates/aiken-lang/src/tipo/infer.rs +++ b/crates/aiken-lang/src/tipo/infer.rs @@ -89,6 +89,7 @@ impl UntypedModule { &self.lines, tracing, )?; + definitions.push(definition); } @@ -495,60 +496,62 @@ fn infer_definition( }) => { let constructors = untyped_constructors .into_iter() - .map( - |RecordConstructor { - location, - name, - arguments: args, - doc, - sugar, - }| { - let preregistered_fn = environment - .get_variable(&name) - .expect("Could not find preregistered type for function"); + .map(|constructor| { + let preregistered_fn = environment + .get_variable(&constructor.name) + .expect("Could not find preregistered type for function"); - let preregistered_type = preregistered_fn.tipo.clone(); + let preregistered_type = preregistered_fn.tipo.clone(); - let args = if let Some((args_types, _return_type)) = - preregistered_type.function_types() - { - args.into_iter() + let args = preregistered_type.function_types().map_or( + Ok(vec![]), + |(args_types, _return_type)| { + constructor + .arguments + .into_iter() .zip(&args_types) - .map( - |( - RecordConstructorArg { - label, - annotation, - location, - doc, - tipo: _, - }, - t, - )| { - RecordConstructorArg { - label, - annotation, - location, - tipo: t.clone(), - doc, - } - }, - ) - .collect() - } else { - vec![] - }; + .map(|(arg, t)| { + if t.is_function() { + return Err(Error::FunctionTypeInData { + location: arg.location, + }); + } - RecordConstructor { - location, - name, - arguments: args, - doc, - sugar, - } - }, - ) - .collect(); + if t.is_ml_result() { + return Err(Error::IllegalTypeInData { + location: arg.location, + tipo: t.clone(), + }); + } + + if t.contains_opaque() { + let parent = environment + .get_type_constructor_mut(&name, location)?; + + Rc::make_mut(&mut parent.tipo).set_opaque(true) + } + + Ok(RecordConstructorArg { + label: arg.label, + annotation: arg.annotation, + location: arg.location, + doc: arg.doc, + tipo: t.clone(), + }) + }) + .collect() + }, + )?; + + Ok(RecordConstructor { + location: constructor.location, + name: constructor.name, + arguments: args, + doc: constructor.doc, + sugar: constructor.sugar, + }) + }) + .collect::>()?; let typed_parameters = environment .get_type_constructor(&None, &name, location) @@ -797,7 +800,7 @@ fn infer_fuzzer( name, args, public: _, - opaque: _, + contains_opaque: _, alias: _, } if module.is_empty() && name == "Option" && args.len() == 1 => { match args.first().expect("args.len() == 1").borrow() { @@ -851,7 +854,7 @@ fn annotate_fuzzer(tipo: &Type, location: &Span) -> Result { module, args, public: _, - opaque: _, + contains_opaque: _, alias: _, } => { let arguments = args diff --git a/crates/aiken-lang/src/tipo/pattern.rs b/crates/aiken-lang/src/tipo/pattern.rs index a8b055fe..f2f7582f 100644 --- a/crates/aiken-lang/src/tipo/pattern.rs +++ b/crates/aiken-lang/src/tipo/pattern.rs @@ -154,6 +154,7 @@ impl<'a, 'b> PatternTyper<'a, 'b> { location, }); }; + Ok(Pattern::Discard { name, location }) } diff --git a/crates/aiken-lang/src/tipo/pretty.rs b/crates/aiken-lang/src/tipo/pretty.rs index 3a6d7af5..65745820 100644 --- a/crates/aiken-lang/src/tipo/pretty.rs +++ b/crates/aiken-lang/src/tipo/pretty.rs @@ -368,7 +368,7 @@ mod tests { module: "whatever".to_string(), name: "Int".to_string(), public: true, - opaque: false, + contains_opaque: false, args: vec![], alias: None }, @@ -379,14 +379,14 @@ mod tests { module: "".to_string(), name: "Pair".to_string(), public: true, - opaque: false, + contains_opaque: false, alias: None, args: vec![ Rc::new(Type::App { module: "whatever".to_string(), name: "Int".to_string(), public: true, - opaque: false, + contains_opaque: false, args: vec![], alias: None }), @@ -394,7 +394,7 @@ mod tests { module: "whatever".to_string(), name: "Bool".to_string(), public: true, - opaque: false, + contains_opaque: false, args: vec![], alias: None }), @@ -410,7 +410,7 @@ mod tests { module: "whatever".to_string(), name: "Int".to_string(), public: true, - opaque: false, + contains_opaque: false, alias: None, }), Rc::new(Type::App { @@ -418,7 +418,7 @@ mod tests { module: "whatever".to_string(), name: "Bool".to_string(), public: true, - opaque: false, + contains_opaque: false, alias: None, }), ], @@ -427,7 +427,7 @@ mod tests { module: "whatever".to_string(), name: "Bool".to_string(), public: true, - opaque: false, + contains_opaque: false, alias: None, }), alias: None, @@ -444,7 +444,7 @@ mod tests { module: "whatever".to_string(), name: "Int".to_string(), public: true, - opaque: false, + contains_opaque: false, }), })), }, @@ -487,7 +487,7 @@ mod tests { Type::Fn { args: vec![Rc::new(Type::App { public: true, - opaque: false, + contains_opaque: false, module: "".to_string(), name: "PRNG".to_string(), args: vec![], @@ -495,14 +495,14 @@ mod tests { })], ret: Rc::new(Type::App { public: true, - opaque: false, + contains_opaque: false, module: "".to_string(), name: "Option".to_string(), args: vec![Rc::new(Type::Tuple { elems: vec![ Rc::new(Type::App { public: true, - opaque: false, + contains_opaque: false, module: "".to_string(), name: "PRNG".to_string(), args: vec![], @@ -510,7 +510,7 @@ mod tests { }), Rc::new(Type::App { public: true, - opaque: false, + contains_opaque: false, module: "".to_string(), name: "Bool".to_string(), args: vec![], @@ -561,7 +561,7 @@ mod tests { Type::Fn { args: vec![Rc::new(Type::App { public: true, - opaque: false, + contains_opaque: false, module: "".to_string(), name: "PRNG".to_string(), args: vec![], @@ -569,14 +569,14 @@ mod tests { })], ret: Rc::new(Type::App { public: true, - opaque: false, + contains_opaque: false, module: "".to_string(), name: "Option".to_string(), args: vec![Rc::new(Type::Tuple { elems: vec![ Rc::new(Type::App { public: true, - opaque: false, + contains_opaque: false, module: "".to_string(), name: "PRNG".to_string(), args: vec![], From 191a3e9134b07c82b5d742bc683fa916799f24e8 Mon Sep 17 00:00:00 2001 From: rvcas Date: Wed, 13 Mar 2024 20:21:33 -0400 Subject: [PATCH 10/13] chore: weird thing from rebase --- crates/aiken-lang/src/tests/check.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/aiken-lang/src/tests/check.rs b/crates/aiken-lang/src/tests/check.rs index 9d635cec..620141b0 100644 --- a/crates/aiken-lang/src/tests/check.rs +++ b/crates/aiken-lang/src/tests/check.rs @@ -1761,7 +1761,7 @@ fn backpassing_type_annotation() { (Foo(1), inputs) } [input, ..remaining_inputs] -> { - + callback(input)( fn(foo) { transition_fold4( @@ -1779,7 +1779,7 @@ fn backpassing_type_annotation() { transition_fold4( x, ) - + fn(g){ g(if input.foo == 1{ 1 @@ -1787,13 +1787,14 @@ fn backpassing_type_annotation() { 2 }) } - + } "#; assert!(check(parse(source_code)).is_ok()) } +#[test] fn forbid_expect_into_opaque_type_from_data() { let source_code = r#" opaque type Thing { inner: Int } From 3055c5ef52b52fa37844df2ba8a5f8a431f6d89c Mon Sep 17 00:00:00 2001 From: KtorZ Date: Thu, 14 Mar 2024 11:00:17 +0100 Subject: [PATCH 11/13] Do not allow casting when rhs or lhs contain an opaque type. Also slightly extended the check test 'framework' to allow registering side-dependency and using them from another module. This allows to check the interplay between opaque type from within and outside of their host module. --- crates/aiken-lang/src/tests/check.rs | 77 ++++++++++++++++++++--- crates/aiken-lang/src/tipo/environment.rs | 5 ++ crates/aiken-lang/src/tipo/expr.rs | 4 -- 3 files changed, 75 insertions(+), 11 deletions(-) diff --git a/crates/aiken-lang/src/tests/check.rs b/crates/aiken-lang/src/tests/check.rs index 620141b0..53bc42c0 100644 --- a/crates/aiken-lang/src/tests/check.rs +++ b/crates/aiken-lang/src/tests/check.rs @@ -16,6 +16,7 @@ fn parse(source_code: &str) -> UntypedModule { fn check_module( ast: UntypedModule, + extra: Vec<(String, UntypedModule)>, kind: ModuleKind, ) -> Result<(Vec, TypedModule), (Vec, Error)> { let id_gen = IdGenerator::new(); @@ -26,6 +27,21 @@ fn check_module( module_types.insert("aiken".to_string(), builtins::prelude(&id_gen)); module_types.insert("aiken/builtin".to_string(), builtins::plutus(&id_gen)); + for (package, module) in extra { + let mut warnings = vec![]; + let typed_module = module + .infer( + &id_gen, + kind, + &package, + &module_types, + Tracing::All(TraceLevel::Verbose), + &mut warnings, + ) + .expect("extra dependency did not compile"); + module_types.insert(package.clone(), typed_module.type_info.clone()); + } + let result = ast.infer( &id_gen, kind, @@ -41,13 +57,20 @@ fn check_module( } fn check(ast: UntypedModule) -> Result<(Vec, TypedModule), (Vec, Error)> { - check_module(ast, ModuleKind::Lib) + check_module(ast, Vec::new(), ModuleKind::Lib) +} + +fn check_with_deps( + ast: UntypedModule, + extra: Vec<(String, UntypedModule)>, +) -> Result<(Vec, TypedModule), (Vec, Error)> { + check_module(ast, extra, ModuleKind::Lib) } fn check_validator( ast: UntypedModule, ) -> Result<(Vec, TypedModule), (Vec, Error)> { - check_module(ast, ModuleKind::Validator) + check_module(ast, Vec::new(), ModuleKind::Validator) } #[test] @@ -1813,7 +1836,7 @@ fn forbid_expect_into_opaque_type_from_data() { } #[test] -fn forbid_expect_into_opaque_type_constructor_without_typecasting() { +fn forbid_expect_into_opaque_type_constructor_without_typecasting_in_module() { let source_code = r#" opaque type Thing { Foo(Int) @@ -1826,10 +1849,51 @@ fn forbid_expect_into_opaque_type_constructor_without_typecasting() { } "#; + assert!(check(parse(source_code)).is_ok()); +} + +#[test] +fn forbid_importing_or_using_opaque_constructors() { + let dependency = r#" + pub opaque type Thing { + Foo(Int) + Bar(Int) + } + "#; + + let source_code = r#" + use foo/thing.{Thing, Foo} + + fn bar(thing: Thing) { + expect Foo(a) = thing + a + } + "#; + assert!(matches!( - check(parse(source_code)), - Err((_, Error::ExpectOnOpaqueType { .. })) - )) + check_with_deps( + parse(source_code), + vec![("foo/thing".to_string(), parse(dependency))], + ), + Err((_, Error::UnknownModuleField { .. })), + )); + + let source_code = r#" + use foo/thing.{Thing} + + fn bar(thing: Thing) { + expect Foo(a) = thing + a + } + "#; + + assert!(matches!( + check_with_deps( + parse(source_code), + vec![("foo/thing".to_string(), parse(dependency))], + ), + Err((_, Error::UnknownTypeConstructor { .. })), + )); } #[test] @@ -1914,7 +1978,6 @@ fn allow_expect_on_var_patterns_that_are_opaque() { fn bar(a: Option) { expect Some(thing) = a - thing.inner } "#; diff --git a/crates/aiken-lang/src/tipo/environment.rs b/crates/aiken-lang/src/tipo/environment.rs index ec104130..f35779cd 100644 --- a/crates/aiken-lang/src/tipo/environment.rs +++ b/crates/aiken-lang/src/tipo/environment.rs @@ -1415,10 +1415,15 @@ impl<'a> Environment<'a> { && !(t1.is_function() || t2.is_function()) && !(t1.is_generic() || t2.is_generic()) && !(t1.is_string() || t2.is_string()) + && !(t1.contains_opaque() || t2.contains_opaque()) { return Ok(()); } + if allow_cast && (t1.contains_opaque() || t2.contains_opaque()) { + return Err(Error::ExpectOnOpaqueType { location }); + } + // Collapse right hand side type links. Left hand side will be collapsed in the next block. if let Type::Var { tipo, alias } = t2.deref() { if let TypeVar::Link { tipo } = tipo.borrow().deref() { diff --git a/crates/aiken-lang/src/tipo/expr.rs b/crates/aiken-lang/src/tipo/expr.rs index 40e5f3e1..f1cedb2a 100644 --- a/crates/aiken-lang/src/tipo/expr.rs +++ b/crates/aiken-lang/src/tipo/expr.rs @@ -976,10 +976,6 @@ impl<'a, 'b> ExprTyper<'a, 'b> { kind.is_let(), )?; - if kind.is_expect() && value_typ.contains_opaque() { - return Err(Error::ExpectOnOpaqueType { location }); - } - // If `expect` is explicitly used, we still check exhaustiveness but instead of returning an // error we emit a warning which explains that using `expect` is unnecessary. match kind { From 0343aeca3442d39f96709a7ca29a5e8f3b05446d Mon Sep 17 00:00:00 2001 From: KtorZ Date: Thu, 14 Mar 2024 11:06:53 +0100 Subject: [PATCH 12/13] Fix remaining insta snapshots. --- ...__validator__tests__opaque_singleton_multi_variants.snap | 2 +- ...eprint__validator__tests__opaque_singleton_variants.snap | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/aiken-project/src/blueprint/snapshots/aiken_project__blueprint__validator__tests__opaque_singleton_multi_variants.snap b/crates/aiken-project/src/blueprint/snapshots/aiken_project__blueprint__validator__tests__opaque_singleton_multi_variants.snap index 1b65548e..e90fb15b 100644 --- a/crates/aiken-project/src/blueprint/snapshots/aiken_project__blueprint__validator__tests__opaque_singleton_multi_variants.snap +++ b/crates/aiken-project/src/blueprint/snapshots/aiken_project__blueprint__validator__tests__opaque_singleton_multi_variants.snap @@ -8,7 +8,7 @@ Schema { breadcrumbs: [ App { public: true, - opaque: true, + contains_opaque: true, module: "test_module", name: "Rational", args: [], diff --git a/crates/aiken-project/src/blueprint/snapshots/aiken_project__blueprint__validator__tests__opaque_singleton_variants.snap b/crates/aiken-project/src/blueprint/snapshots/aiken_project__blueprint__validator__tests__opaque_singleton_variants.snap index bcfa9f67..2fc9e2ff 100644 --- a/crates/aiken-project/src/blueprint/snapshots/aiken_project__blueprint__validator__tests__opaque_singleton_variants.snap +++ b/crates/aiken-project/src/blueprint/snapshots/aiken_project__blueprint__validator__tests__opaque_singleton_variants.snap @@ -8,7 +8,7 @@ Schema { breadcrumbs: [ App { public: true, - opaque: true, + contains_opaque: true, module: "test_module", name: "Dict", args: [ @@ -17,7 +17,7 @@ Schema { value: Link { tipo: App { public: false, - opaque: false, + contains_opaque: false, module: "test_module", name: "UUID", args: [], @@ -32,7 +32,7 @@ Schema { value: Link { tipo: App { public: true, - opaque: false, + contains_opaque: false, module: "", name: "Int", args: [], From cdf564fc9dc3f2d5630739720f34df0233ac38e8 Mon Sep 17 00:00:00 2001 From: KtorZ Date: Thu, 14 Mar 2024 11:20:34 +0100 Subject: [PATCH 13/13] Expand 'ExpectOnOpaqueType' error help and label. --- crates/aiken-lang/src/tipo/error.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/crates/aiken-lang/src/tipo/error.rs b/crates/aiken-lang/src/tipo/error.rs index a8298cb0..c9f41e88 100644 --- a/crates/aiken-lang/src/tipo/error.rs +++ b/crates/aiken-lang/src/tipo/error.rs @@ -259,10 +259,16 @@ You can use '{discard}' and numbers to distinguish between similar names. name: String, }, - #[error("I found an expect assignment involving an opaque type. This is not allowed.\n")] + #[error("I caught an opaque type possibly breaking its abstraction boundary.\n")] #[diagnostic(code("illegal::expect_on_opaque"))] + #[diagnostic(url("https://aiken-lang.org/language-tour/modules#opaque-types"))] + #[diagnostic(help( + "This expression is trying to convert something unknown into an opaque type. An opaque type is a data-type which hides its internal details; usually because it enforces some specific invariant on its internal structure. For example, you might define a {Natural} type that holds an {Integer} but ensures that it never gets negative.\n\nA direct consequence means that it isn't generally possible, nor safe, to turn *any* value into an opaque type. Instead, use the constructors and methods provided for lifting values into that opaque type while ensuring that any structural invariant is checked for.", + Natural = "Natural".if_supports_color(Stdout, |s| s.cyan()), + Integer = "Integer".if_supports_color(Stdout, |s| s.cyan()), + ))] ExpectOnOpaqueType { - #[label] + #[label("reckless opaque cast")] location: Span, },