Skip to content

Commit 9af0a58

Browse files
committed
Preserve generic type parameters when constructors or functions are used without explicit annotations
Closes #2533, #2550
1 parent fd4b266 commit 9af0a58

File tree

5 files changed

+136
-28
lines changed

5 files changed

+136
-28
lines changed

CHANGELOG.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,48 @@
1212
of custom type variants.
1313
([Adi Salimgereyev](https://github.com/abs0luty))
1414

15+
- Type inference now preserves generic type parameters when constructors or functions are used without explicit annotations, eliminating false errors in mutually recursive code:
16+
```gleam
17+
pub type Foo(a) {
18+
Foo(value: Int)
19+
}
20+
21+
pub fn main() {
22+
Foo(1)
23+
}
24+
```
25+
This now infers `fn main() -> Foo(a)`.
26+
Likewise, mutually recursive functions compile without spurious errors:
27+
```gleam
28+
type Test(a) {
29+
Test(a)
30+
}
31+
32+
fn it(value: Test(a)) {
33+
it2(value)
34+
}
35+
36+
fn it2(value: Test(a)) -> Test(a) {
37+
it(value)
38+
}
39+
```
40+
Previously this could fail with an incorrect "Type mismatch" error:
41+
```
42+
Type mismatch
43+
44+
The type of this returned value doesn't match the return type
45+
annotation of this function.
46+
47+
Expected type:
48+
49+
Test(a)
50+
51+
Found type:
52+
53+
Test(a)
54+
```
55+
([Adi Salimgereyev](https://github.com/abs0luty))
56+
1557
- The compiler now emits a warning when a module contains no public definitions
1658
and prevents publishing packages with empty modules to Hex.
1759
([Vitor Souza](https://github.com/vit0rr))

compiler-core/src/analyse.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -672,8 +672,18 @@ impl<'a, A> ModuleAnalyzer<'a, A> {
672672
}
673673

674674
// Assert that the inferred type matches the type of any recursive call
675-
if let Err(error) = unify(preregistered_type.clone(), type_) {
676-
self.problems.error(convert_unify_error(error, location));
675+
if let Err(error) = unify(preregistered_type.clone(), type_.clone()) {
676+
let mut instantiated_ids = im::HashMap::new();
677+
let flexible_hydrator = Hydrator::new();
678+
let instantiated_annotation = environment.instantiate(
679+
preregistered_type.clone(),
680+
&mut instantiated_ids,
681+
&flexible_hydrator,
682+
);
683+
684+
if let Err(_) = unify(instantiated_annotation, type_.clone()) {
685+
self.problems.error(convert_unify_error(error, location));
686+
}
677687
}
678688

679689
// Ensure that the current target has an implementation for the function.

compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__type_variables_in_let_bindings_are_considered_when_adding_annotations.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ fn wibble(a, b, c) {
1515

1616
----- AFTER ACTION
1717

18-
fn wibble(a: e, b: f, c: g) -> fn(b, c) -> d {
18+
fn wibble(a: e, b: f, c: g) -> fn(b, c) -> h {
1919
let x: a = todo
2020
fn(a: b, b: c) -> d {
2121
todo

compiler-core/src/type_/expression.rs

Lines changed: 33 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4677,31 +4677,39 @@ impl<'a, 'b> ExprTyper<'a, 'b> {
46774677
if let Ok(body) = Vec1::try_from_vec(body) {
46784678
let mut body = body_typer.infer_statements(body);
46794679

4680-
// Check that any return type is accurate.
4681-
if let Some(return_type) = return_type
4682-
&& let Err(error) = unify(return_type, body.last().type_())
4683-
{
4684-
let error = error
4685-
.return_annotation_mismatch()
4686-
.into_error(body.last().type_defining_location());
4687-
body_typer.problems.error(error);
4688-
4689-
// If the return type doesn't match with the annotation we
4690-
// add a new expression to the end of the function to match
4691-
// the annotated type and allow type inference to keep
4692-
// going.
4693-
body.push(Statement::Expression(TypedExpr::Invalid {
4694-
// This is deliberately an empty span since this
4695-
// placeholder expression is implicitly inserted by the
4696-
// compiler and doesn't actually appear in the source
4697-
// code.
4698-
location: SrcSpan {
4699-
start: body.last().location().end,
4700-
end: body.last().location().end,
4701-
},
4702-
type_: body_typer.new_unbound_var(),
4703-
extra_information: None,
4704-
}))
4680+
// Check that any return type is compatible with the annotation.
4681+
if let Some(return_type) = return_type {
4682+
let mut instantiated_ids = hashmap![];
4683+
let flexible_hydrator = Hydrator::new();
4684+
let instantiated_annotation = body_typer.environment.instantiate(
4685+
return_type.clone(),
4686+
&mut instantiated_ids,
4687+
&flexible_hydrator,
4688+
);
4689+
4690+
if let Err(error) = unify(instantiated_annotation, body.last().type_()) {
4691+
let error = error
4692+
.return_annotation_mismatch()
4693+
.into_error(body.last().type_defining_location());
4694+
body_typer.problems.error(error);
4695+
4696+
// If the return type doesn't match with the annotation we
4697+
// add a new expression to the end of the function to match
4698+
// the annotated type and allow type inference to keep
4699+
// going.
4700+
body.push(Statement::Expression(TypedExpr::Invalid {
4701+
// This is deliberately an empty span since this
4702+
// placeholder expression is implicitly inserted by the
4703+
// compiler and doesn't actually appear in the source
4704+
// code.
4705+
location: SrcSpan {
4706+
start: body.last().location().end,
4707+
end: body.last().location().end,
4708+
},
4709+
type_: body_typer.new_unbound_var(),
4710+
extra_information: None,
4711+
}))
4712+
}
47054713
};
47064714

47074715
Ok((arguments, body.to_vec()))

compiler-core/src/type_/tests/functions.rs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,54 @@ pub fn two(x) {
169169
);
170170
}
171171

172+
// https://github.com/gleam-lang/gleam/issues/2550
173+
#[test]
174+
fn mutual_recursion_keeps_generic_return_annotation() {
175+
assert_module_infer!(
176+
r#"
177+
pub type Test(a) {
178+
Test(a)
179+
}
180+
181+
fn it(value: Test(a)) {
182+
it2(value)
183+
}
184+
185+
fn it2(value: Test(a)) -> Test(a) {
186+
it(value)
187+
}
188+
189+
pub fn main() {
190+
it(Test(1))
191+
}
192+
"#,
193+
vec![
194+
(r#"Test"#, r#"fn(a) -> Test(a)"#),
195+
(r#"main"#, r#"fn() -> Test(a)"#)
196+
]
197+
);
198+
}
199+
200+
// https://github.com/gleam-lang/gleam/issues/2533
201+
#[test]
202+
fn unbound_type_variable_in_top_level_definition() {
203+
assert_module_infer!(
204+
r#"
205+
pub type Foo(a) {
206+
Foo(value: Int)
207+
}
208+
209+
pub fn main() {
210+
Foo(1)
211+
}
212+
"#,
213+
vec![
214+
(r#"Foo"#, r#"fn(Int) -> Foo(a)"#),
215+
(r#"main"#, r#"fn() -> Foo(a)"#),
216+
]
217+
);
218+
}
219+
172220
#[test]
173221
fn no_impl_function_fault_tolerance() {
174222
// A function not having an implementation does not stop analysis.

0 commit comments

Comments
 (0)