Skip to content

Commit 948dcc9

Browse files
committed
Preserve generic type parameters when constructors or functions are used without explicit annotations
Closes #2533, #2550
1 parent 2453c47 commit 948dcc9

File tree

5 files changed

+135
-30
lines changed

5 files changed

+135
-30
lines changed

CHANGELOG.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,38 @@
120120

121121
([fruno](https://github.com/fruno-bulax/))
122122

123+
- Type inference now preserves generic type parameters when constructors or functions are used without explicit annotations, eliminating false errors in mutually recursive code:
124+
```gleam
125+
type Test(a) {
126+
Test(a)
127+
}
128+
129+
fn it(value: Test(a)) {
130+
it2(value)
131+
}
132+
133+
fn it2(value: Test(a)) -> Test(a) {
134+
it(value)
135+
}
136+
```
137+
Previously this could fail with an incorrect "Type mismatch" error:
138+
```
139+
Type mismatch
140+
141+
The type of this returned value doesn't match the return type
142+
annotation of this function.
143+
144+
Expected type:
145+
146+
Test(a)
147+
148+
Found type:
149+
150+
Test(a)
151+
```
152+
153+
([Adi Salimgereyev](https://github.com/abs0luty))
154+
123155
### Build tool
124156

125157
- 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
@@ -695,8 +695,18 @@ impl<'a, A> ModuleAnalyzer<'a, A> {
695695
}
696696

697697
// Assert that the inferred type matches the type of any recursive call
698-
if let Err(error) = unify(preregistered_type.clone(), type_) {
699-
self.problems.error(convert_unify_error(error, location));
698+
if let Err(error) = unify(preregistered_type.clone(), type_.clone()) {
699+
let mut instantiated_ids = im::HashMap::new();
700+
let flexible_hydrator = Hydrator::new();
701+
let instantiated_annotation = environment.instantiate(
702+
preregistered_type.clone(),
703+
&mut instantiated_ids,
704+
&flexible_hydrator,
705+
);
706+
707+
if unify(instantiated_annotation, type_.clone()).is_err() {
708+
self.problems.error(convert_unify_error(error, location));
709+
}
700710
}
701711

702712
// Ensure that the current target has an implementation for the function.
@@ -737,10 +747,13 @@ impl<'a, A> ModuleAnalyzer<'a, A> {
737747
purity,
738748
};
739749

750+
// Store the inferred type (not the preregistered type) in the environment.
751+
// This ensures concrete type information flows through recursive calls - e.g., if we infer
752+
// `fn() -> Test(Int)`, callers see that instead of the generic `fn() -> Test(a)`.
740753
environment.insert_variable(
741754
name.clone(),
742755
variant,
743-
preregistered_type.clone(),
756+
type_.clone(),
744757
publicity,
745758
deprecation.clone(),
746759
);
@@ -753,6 +766,8 @@ impl<'a, A> ModuleAnalyzer<'a, A> {
753766
ReferenceKind::Definition,
754767
);
755768

769+
// Use the inferred return type for the typed AST node.
770+
// This matches the type stored in the environment above.
756771
let function = Function {
757772
documentation: doc,
758773
location,
@@ -763,7 +778,7 @@ impl<'a, A> ModuleAnalyzer<'a, A> {
763778
body_start,
764779
end_position: end_location,
765780
return_annotation,
766-
return_type: preregistered_type
781+
return_type: type_
767782
.return_type()
768783
.expect("Could not find return type for fn"),
769784
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
@@ -4697,31 +4697,39 @@ impl<'a, 'b> ExprTyper<'a, 'b> {
46974697
if let Ok(body) = Vec1::try_from_vec(body) {
46984698
let mut body = body_typer.infer_statements(body);
46994699

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

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