Skip to content

Commit 893c9f8

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

File tree

5 files changed

+134
-30
lines changed

5 files changed

+134
-30
lines changed

CHANGELOG.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,37 @@
2929

3030
([Surya Rose](https://github.com/GearsDatapacks))
3131

32+
- Type inference now preserves generic type parameters when constructors or functions are used without explicit annotations, eliminating false errors in mutually recursive code:
33+
```gleam
34+
type Test(a) {
35+
Test(a)
36+
}
37+
38+
fn it(value: Test(a)) {
39+
it2(value)
40+
}
41+
42+
fn it2(value: Test(a)) -> Test(a) {
43+
it(value)
44+
}
45+
```
46+
Previously this could fail with an incorrect "Type mismatch" error:
47+
```
48+
Type mismatch
49+
50+
The type of this returned value doesn't match the return type
51+
annotation of this function.
52+
53+
Expected type:
54+
55+
Test(a)
56+
57+
Found type:
58+
59+
Test(a)
60+
```
61+
([Adi Salimgereyev](https://github.com/abs0luty))
62+
3263
### Build tool
3364

3465
- The help text displayed by `gleam dev --help`, `gleam test --help`, and

compiler-core/src/analyse.rs

Lines changed: 19 additions & 4 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 unify(instantiated_annotation, type_.clone()).is_err() {
685+
self.problems.error(convert_unify_error(error, location));
686+
}
677687
}
678688

679689
// Ensure that the current target has an implementation for the function.
@@ -714,10 +724,13 @@ impl<'a, A> ModuleAnalyzer<'a, A> {
714724
purity,
715725
};
716726

727+
// Store the inferred type (not the preregistered type) in the environment.
728+
// This ensures concrete type information flows through recursive calls - e.g., if we infer
729+
// `fn() -> Test(Int)`, callers see that instead of the generic `fn() -> Test(a)`.
717730
environment.insert_variable(
718731
name.clone(),
719732
variant,
720-
preregistered_type.clone(),
733+
type_.clone(),
721734
publicity,
722735
deprecation.clone(),
723736
);
@@ -730,6 +743,8 @@ impl<'a, A> ModuleAnalyzer<'a, A> {
730743
ReferenceKind::Definition,
731744
);
732745

746+
// Use the inferred return type for the typed AST node.
747+
// This matches the type stored in the environment above.
733748
let function = Function {
734749
documentation: doc,
735750
location,
@@ -740,7 +755,7 @@ impl<'a, A> ModuleAnalyzer<'a, A> {
740755
body_start,
741756
end_position: end_location,
742757
return_annotation,
743-
return_type: preregistered_type
758+
return_type: type_
744759
.return_type()
745760
.expect("Could not find return type for fn"),
746761
body,

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: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,56 @@ 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+
pub fn it(value: Test(a)) {
182+
it2(value)
183+
}
184+
185+
pub 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#"it"#, r#"fn(Test(a)) -> Test(a)"#),
196+
(r#"it2"#, r#"fn(Test(a)) -> Test(a)"#),
197+
(r#"main"#, r#"fn() -> Test(Int)"#)
198+
]
199+
);
200+
}
201+
202+
// https://github.com/gleam-lang/gleam/issues/2533
203+
#[test]
204+
fn unbound_type_variable_in_top_level_definition() {
205+
assert_module_infer!(
206+
r#"
207+
pub type Foo(a) {
208+
Foo(value: Int)
209+
}
210+
211+
pub fn main() {
212+
Foo(1)
213+
}
214+
"#,
215+
vec![
216+
(r#"Foo"#, r#"fn(Int) -> Foo(a)"#),
217+
(r#"main"#, r#"fn() -> Foo(a)"#),
218+
]
219+
);
220+
}
221+
172222
#[test]
173223
fn no_impl_function_fault_tolerance() {
174224
// A function not having an implementation does not stop analysis.

0 commit comments

Comments
 (0)