Skip to content

Commit 1339749

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

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
@@ -34,6 +34,37 @@
3434
than stopping at the syntax error.
3535
([mxtthias](https://github.com/mxtthias))
3636

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

3970
- 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
@@ -673,8 +673,18 @@ impl<'a, A> ModuleAnalyzer<'a, A> {
673673
}
674674

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

680690
// Ensure that the current target has an implementation for the function.
@@ -715,10 +725,13 @@ impl<'a, A> ModuleAnalyzer<'a, A> {
715725
purity,
716726
};
717727

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

747+
// Use the inferred return type for the typed AST node.
748+
// This matches the type stored in the environment above.
734749
let function = Function {
735750
documentation: doc,
736751
location,
@@ -741,7 +756,7 @@ impl<'a, A> ModuleAnalyzer<'a, A> {
741756
body_start,
742757
end_position: end_location,
743758
return_annotation,
744-
return_type: preregistered_type
759+
return_type: type_
745760
.return_type()
746761
.expect("Could not find return type for fn"),
747762
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
@@ -4707,31 +4707,39 @@ impl<'a, 'b> ExprTyper<'a, 'b> {
47074707
if let Ok(body) = Vec1::try_from_vec(body) {
47084708
let mut body = body_typer.infer_statements(body);
47094709

4710-
// Check that any return type is accurate.
4711-
if let Some(return_type) = return_type
4712-
&& let Err(error) = unify(return_type, body.last().type_())
4713-
{
4714-
let error = error
4715-
.return_annotation_mismatch()
4716-
.into_error(body.last().type_defining_location());
4717-
body_typer.problems.error(error);
4718-
4719-
// If the return type doesn't match with the annotation we
4720-
// add a new expression to the end of the function to match
4721-
// the annotated type and allow type inference to keep
4722-
// going.
4723-
body.push(Statement::Expression(TypedExpr::Invalid {
4724-
// This is deliberately an empty span since this
4725-
// placeholder expression is implicitly inserted by the
4726-
// compiler and doesn't actually appear in the source
4727-
// code.
4728-
location: SrcSpan {
4729-
start: body.last().location().end,
4730-
end: body.last().location().end,
4731-
},
4732-
type_: body_typer.new_unbound_var(),
4733-
extra_information: None,
4734-
}))
4710+
// Check that any return type is compatible with the annotation.
4711+
if let Some(return_type) = return_type {
4712+
let mut instantiated_ids = hashmap![];
4713+
let flexible_hydrator = Hydrator::new();
4714+
let instantiated_annotation = body_typer.environment.instantiate(
4715+
return_type.clone(),
4716+
&mut instantiated_ids,
4717+
&flexible_hydrator,
4718+
);
4719+
4720+
if let Err(error) = unify(instantiated_annotation, body.last().type_()) {
4721+
let error = error
4722+
.return_annotation_mismatch()
4723+
.into_error(body.last().type_defining_location());
4724+
body_typer.problems.error(error);
4725+
4726+
// If the return type doesn't match with the annotation we
4727+
// add a new expression to the end of the function to match
4728+
// the annotated type and allow type inference to keep
4729+
// going.
4730+
body.push(Statement::Expression(TypedExpr::Invalid {
4731+
// This is deliberately an empty span since this
4732+
// placeholder expression is implicitly inserted by the
4733+
// compiler and doesn't actually appear in the source
4734+
// code.
4735+
location: SrcSpan {
4736+
start: body.last().location().end,
4737+
end: body.last().location().end,
4738+
},
4739+
type_: body_typer.new_unbound_var(),
4740+
extra_information: None,
4741+
}))
4742+
}
47354743
};
47364744

47374745
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)