diff --git a/CHANGELOG.md b/CHANGELOG.md index f441ed5d1de..e01f575c4b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,17 @@ containing scientific notation or trailing zeros (i.e. `100` and `1e2`). ([ptdewey](https://github.com/ptdewey)) +- Let bindings now continue to assume their annotated type after mismatches, + preventing misleading follow-up errors: + ```gleam + pub fn main() { + let x: String = 5 // type error: expected String, got Int + let y: Int = x // valid + let z: String = x // type error: expected String, got Int + } + ``` + ([Adi Salimgereyev](https://github.com/abs0luty)) + ### Build tool - The help text displayed by `gleam dev --help`, `gleam test --help`, and diff --git a/compiler-core/src/type_/expression.rs b/compiler-core/src/type_/expression.rs index 1398ac9051f..3da9f8b5eb8 100644 --- a/compiler-core/src/type_/expression.rs +++ b/compiler-core/src/type_/expression.rs @@ -2000,24 +2000,7 @@ impl<'a, 'b> ExprTyper<'a, 'b> { let type_ = value.type_(); let kind = self.infer_assignment_kind(kind.clone()); - // Ensure the pattern matches the type of the value - let mut pattern_typer = pattern::PatternTyper::new( - self.environment, - &self.implementations, - &self.current_function_definition, - &self.hydrator, - self.problems, - PatternPosition::LetAssignment, - ); - - let pattern = pattern_typer.infer_single_pattern(pattern, &value); - - let minimum_required_version = pattern_typer.minimum_required_version; - if minimum_required_version > self.minimum_required_version { - self.minimum_required_version = minimum_required_version; - } - - let pattern_typechecked_successfully = !pattern_typer.error_encountered; + let mut annotation_type_override = None; // Check that any type annotation is accurate. if let Some(annotation) = &annotation { @@ -2026,11 +2009,13 @@ impl<'a, 'b> ExprTyper<'a, 'b> { .map(|type_| self.instantiate(type_, &mut hashmap![])) { Ok(annotated_type) => { + let annotation_type_for_pattern = annotated_type.clone(); if let Err(error) = unify(annotated_type, type_.clone()) .map_err(|e| convert_unify_error(e, value.type_defining_location())) { self.problems.error(error); } + annotation_type_override = Some(annotation_type_for_pattern); } Err(error) => { self.problems.error(error); @@ -2038,6 +2023,26 @@ impl<'a, 'b> ExprTyper<'a, 'b> { } } + // Ensure the pattern matches the type of the value + let mut pattern_typer = pattern::PatternTyper::new( + self.environment, + &self.implementations, + &self.current_function_definition, + &self.hydrator, + self.problems, + PatternPosition::LetAssignment, + ); + + let pattern = + pattern_typer.infer_single_pattern(pattern, &value, annotation_type_override.clone()); + + let minimum_required_version = pattern_typer.minimum_required_version; + if minimum_required_version > self.minimum_required_version { + self.minimum_required_version = minimum_required_version; + } + + let pattern_typechecked_successfully = !pattern_typer.error_encountered; + // The exhaustiveness checker expects patterns to be valid and to type check; // if they are invalid, it will crash. Therefore, if any errors were found // when type checking the pattern, we don't perform the exhaustiveness check. @@ -2053,7 +2058,7 @@ impl<'a, 'b> ExprTyper<'a, 'b> { } let (output, not_exhaustive_error) = - self.check_let_exhaustiveness(location, value.type_(), &pattern); + self.check_let_exhaustiveness(location, type_.clone(), &pattern); match (&kind, not_exhaustive_error) { // The pattern is exhaustive in a let assignment, there's no problem here. diff --git a/compiler-core/src/type_/pattern.rs b/compiler-core/src/type_/pattern.rs index 061c51150db..fce29e5765e 100644 --- a/compiler-core/src/type_/pattern.rs +++ b/compiler-core/src/type_/pattern.rs @@ -317,7 +317,7 @@ impl<'a, 'b> PatternTyper<'a, 'b> { for (pattern, subject) in multi_pattern.into_iter().zip(subjects) { let subject_variable = Self::subject_variable(subject); - let pattern = self.unify(pattern, subject.type_(), subject_variable); + let pattern = self.unify(pattern, subject.type_(), subject_variable, None); typed_multi.push(pattern); } @@ -330,10 +330,11 @@ impl<'a, 'b> PatternTyper<'a, 'b> { &mut self, pattern: UntypedPattern, subject: &TypedExpr, + annotation_type: Option>, ) -> TypedPattern { let subject_variable = Self::subject_variable(subject); - - let typed_pattern = self.unify(pattern, subject.type_(), subject_variable); + let subject_type = subject.type_(); + let typed_pattern = self.unify(pattern, subject_type, subject_variable, annotation_type); self.register_variables(); typed_pattern } @@ -453,7 +454,7 @@ impl<'a, 'b> PatternTyper<'a, 'b> { .into_iter() .map(|option| { analyse::infer_bit_array_option(option, |value, type_| { - Ok(self.unify(value, type_, None)) + Ok(self.unify(value, type_, None, None)) }) }) .try_collect() @@ -529,7 +530,7 @@ impl<'a, 'b> PatternTyper<'a, 'b> { _ => segment_type, }; - let typed_value = self.unify(*segment.value, type_.clone(), None); + let typed_value = self.unify(*segment.value, type_.clone(), None, None); match &typed_value { // We can't directly match on the contents of a `Box`, so we must @@ -586,6 +587,18 @@ impl<'a, 'b> PatternTyper<'a, 'b> { // in the inner scope, we can infer that the `some_wibble` variable is the `Wibble` variant // subject_variable: Option, + // An optional type annotation provided for the pattern. + // + // Example: + // ```gleam + // let assert [first, ..rest]: List(String) = numbers + // ``` + // + // We pass `List(String)` annotation down so sub-patterns know + // that `first` must be a `String`. We still check `numbers` + // against its real inferred type and emit a mismatch if it is, say, + // `List(Int)`. + annotation_type: Option>, ) -> TypedPattern { match pattern { Pattern::Discard { name, location, .. } => { @@ -595,7 +608,7 @@ impl<'a, 'b> PatternTyper<'a, 'b> { .discarded_names .insert(name.clone(), location); Pattern::Discard { - type_, + type_: annotation_type.unwrap_or_else(|| type_.clone()), name, location, } @@ -614,10 +627,11 @@ impl<'a, 'b> PatternTyper<'a, 'b> { Pattern::Invalid { location, type_ } } _ => { - self.insert_variable(&name, type_.clone(), location, origin.clone()); + let variable_type = annotation_type.unwrap_or_else(|| type_.clone()); + self.insert_variable(&name, variable_type.clone(), location, origin.clone()); Pattern::Variable { - type_, + type_: variable_type, name, location, origin, @@ -697,7 +711,13 @@ impl<'a, 'b> PatternTyper<'a, 'b> { pattern, location, } => { - let pattern = self.unify(*pattern, type_, subject_variable); + let pattern_annotation = annotation_type.clone(); + let pattern = self.unify( + *pattern, + type_.clone(), + subject_variable, + pattern_annotation, + ); if pattern.is_discard() { self.problems.warning(Warning::UnusedDiscardPattern { @@ -705,9 +725,10 @@ impl<'a, 'b> PatternTyper<'a, 'b> { name: name.clone(), }); } + let assigned_type = annotation_type.unwrap_or_else(|| pattern.type_().clone()); self.insert_variable( &name, - pattern.type_().clone(), + assigned_type.clone(), location, VariableOrigin { syntax: VariableSyntax::AssignmentPattern, @@ -780,20 +801,45 @@ impl<'a, 'b> PatternTyper<'a, 'b> { self.environment, ) { Some(arguments) => { - let type_ = arguments + let element_type = arguments .first() .expect("Failed to get type argument of List") .clone(); + let list_type = list(element_type.clone()); + let annotation_element_type = annotation_type.as_ref().and_then(|annotation| { + annotation + .get_app_arguments( + Publicity::Public, + PRELUDE_PACKAGE_NAME, + PRELUDE_MODULE_NAME, + "List", + 1, + self.environment, + ) + .and_then(|arguments| arguments.into_iter().next()) + }); let elements = elements .into_iter() - .map(|element| self.unify(element, type_.clone(), None)) + .map(|element| { + self.unify( + element, + element_type.clone(), + None, + annotation_element_type.clone(), + ) + }) .collect(); - let type_ = list(type_); let tail = tail.map(|tail| { + let tail_annotation = annotation_type.clone(); Box::new(TailPattern { location: tail.location, - pattern: self.unify(tail.pattern, type_.clone(), None), + pattern: self.unify( + tail.pattern, + list_type.clone(), + None, + tail_annotation, + ), }) }); @@ -801,7 +847,7 @@ impl<'a, 'b> PatternTyper<'a, 'b> { location, elements, tail, - type_, + type_: list_type, } } @@ -833,10 +879,22 @@ impl<'a, 'b> PatternTyper<'a, 'b> { return Pattern::Invalid { location, type_ }; } + let annotation_elements = annotation_type.as_ref().and_then(|annotation| { + match collapse_links(annotation.clone()).deref() { + Type::Tuple { elements } => Some(elements.clone()), + _ => None, + } + }); let elements = elements .into_iter() .zip(type_elements) - .map(|(pattern, type_)| self.unify(pattern, type_.clone(), None)) + .enumerate() + .map(|(index, (pattern, type_))| { + let annotation = annotation_elements + .as_ref() + .and_then(|elements| elements.get(index).cloned()); + self.unify(pattern, type_.clone(), None, annotation) + }) .collect(); Pattern::Tuple { elements, location } } @@ -846,10 +904,22 @@ impl<'a, 'b> PatternTyper<'a, 'b> { .map(|_| self.environment.new_unbound_var()) .collect(); self.unify_types(tuple(elements_types.clone()), type_, location); + let annotation_elements = annotation_type.as_ref().and_then(|annotation| { + match collapse_links(annotation.clone()).deref() { + Type::Tuple { elements } => Some(elements.clone()), + _ => None, + } + }); let elements = elements .into_iter() .zip(elements_types) - .map(|(pattern, type_)| self.unify(pattern, type_, None)) + .enumerate() + .map(|(index, (pattern, type_))| { + let annotation = annotation_elements + .as_ref() + .and_then(|elements| elements.get(index).cloned()); + self.unify(pattern, type_, None, annotation) + }) .collect(); Pattern::Tuple { elements, location } } @@ -1227,7 +1297,8 @@ impl<'a, 'b> PatternTyper<'a, 'b> { .cloned() .unwrap_or_else(|| self.environment.new_unbound_var()); - let value = self.unify(value, type_, None); + let annotation = Some(type_.clone()); + let value = self.unify(value, type_, None, annotation); CallArg { value, location, diff --git a/compiler-core/src/type_/tests/errors.rs b/compiler-core/src/type_/tests/errors.rs index 14d5e515708..4d2398d230f 100644 --- a/compiler-core/src/type_/tests/errors.rs +++ b/compiler-core/src/type_/tests/errors.rs @@ -743,6 +743,37 @@ fn module_could_not_unify6() { assert_module_error!("fn main() { let x: String = 5 x }"); } +#[test] +fn module_could_not_unify_let_binding_annotation_follow_up1() { + assert_module_error!( + " +fn main() { + let x: String = 5 + let y: Int = x + let z: String = x +} +" + ); +} + +#[test] +fn module_could_not_unify_let_binding_annotation_follow_up2() { + assert_module_error!( + " +fn main() { + let assert #( + a, + [[_, ..] as b, ..], + _ as c + ): #(String, List(List(Int)), String) = #(1, [[\"Hello\"]], 3) + + let d: String = a <> c + let e: List(Int) = [0, ..b] +} +" + ); +} + #[test] fn module_could_not_unify7() { assert_module_error!("fn main() { let assert 5 = \"\" }"); diff --git a/compiler-core/src/type_/tests/functions.rs b/compiler-core/src/type_/tests/functions.rs index 7bac5c0813e..7429a82a9ae 100644 --- a/compiler-core/src/type_/tests/functions.rs +++ b/compiler-core/src/type_/tests/functions.rs @@ -261,33 +261,6 @@ pub fn main() { ); } -#[test] -fn multiple_bad_statement_assignment_with_annotation_fault_tolerance() { - assert_module_error!( - r#" -pub fn main() { - let a: Int = "not an int" - let b: String = 1 - let c = a + 2 -} -"# - ); -} - -#[test] -fn multiple_bad_statement_assignment_with_annotation_fault_tolerance2() { - assert_module_error!( - r#" -pub fn main() { - // Since the value is invalid the type is the annotation - let a: Int = Junk - let b: String = 1 - let c = a + 2 -} -"# - ); -} - #[test] fn multiple_bad_statement_assignment_with_pattern_fault_tolerance2() { assert_module_error!( diff --git a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__errors__module_could_not_unify9.snap b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__errors__module_could_not_unify9.snap index 77c056fe8fc..72453e078e9 100644 --- a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__errors__module_could_not_unify9.snap +++ b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__errors__module_could_not_unify9.snap @@ -1,6 +1,8 @@ --- source: compiler-core/src/type_/tests/errors.rs +assertion_line: 771 expression: "fn main() { let assert [1, 2, ..x]: List(String) = [1,2,3] x }" +snapshot_kind: text --- ----- SOURCE CODE fn main() { let assert [1, 2, ..x]: List(String) = [1,2,3] x } diff --git a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__errors__module_could_not_unify_let_binding_annotation_follow_up1.snap b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__errors__module_could_not_unify_let_binding_annotation_follow_up1.snap new file mode 100644 index 00000000000..0cb97cfa1ed --- /dev/null +++ b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__errors__module_could_not_unify_let_binding_annotation_follow_up1.snap @@ -0,0 +1,41 @@ +--- +source: compiler-core/src/type_/tests/errors.rs +expression: "\nfn main() {\n let x: String = 5\n let y: Int = x\n let z: String = x\n}\n" +--- +----- SOURCE CODE + +fn main() { + let x: String = 5 + let y: Int = x + let z: String = x +} + + +----- ERROR +error: Type mismatch + ┌─ /src/one/two.gleam:3:19 + │ +3 │ let x: String = 5 + │ ^ + +Expected type: + + String + +Found type: + + Int + +error: Type mismatch + ┌─ /src/one/two.gleam:4:16 + │ +4 │ let y: Int = x + │ ^ + +Expected type: + + Int + +Found type: + + String diff --git a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__errors__module_could_not_unify_let_binding_annotation_follow_up2.snap b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__errors__module_could_not_unify_let_binding_annotation_follow_up2.snap new file mode 100644 index 00000000000..b63ab15ae76 --- /dev/null +++ b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__errors__module_could_not_unify_let_binding_annotation_follow_up2.snap @@ -0,0 +1,32 @@ +--- +source: compiler-core/src/type_/tests/errors.rs +expression: "\nfn main() {\n let assert #(\n a, \n [[_, ..] as b, ..], \n _ as c\n ): #(String, List(List(Int)), String) = #(1, [[\"Hello\"]], 3)\n\n let d: String = a <> c\n let e: List(Int) = [0, ..b]\n}\n" +--- +----- SOURCE CODE + +fn main() { + let assert #( + a, + [[_, ..] as b, ..], + _ as c + ): #(String, List(List(Int)), String) = #(1, [["Hello"]], 3) + + let d: String = a <> c + let e: List(Int) = [0, ..b] +} + + +----- ERROR +error: Type mismatch + ┌─ /src/one/two.gleam:7:45 + │ +7 │ ): #(String, List(List(Int)), String) = #(1, [["Hello"]], 3) + │ ^^^^^^^^^^^^^^^^^^^^ + +Expected type: + + #(String, List(List(Int)), String) + +Found type: + + #(Int, List(List(String)), Int) diff --git a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__functions__multiple_bad_statement_assignment_with_annotation_fault_tolerance.snap b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__functions__multiple_bad_statement_assignment_with_annotation_fault_tolerance.snap index 51118fe8d94..aa4ac394a9d 100644 --- a/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__functions__multiple_bad_statement_assignment_with_annotation_fault_tolerance.snap +++ b/compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__functions__multiple_bad_statement_assignment_with_annotation_fault_tolerance.snap @@ -39,19 +39,3 @@ Expected type: Found type: Int - -error: Type mismatch - ┌─ /src/one/two.gleam:5:11 - │ -5 │ let c = a + 2 - │ ^ - -The + operator expects arguments of this type: - - Int - -But this argument has this type: - - String - -Hint: Strings can be joined using the `<>` operator.