Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,37 @@
than stopping at the syntax error.
([mxtthias](https://github.com/mxtthias))

- Type inference now preserves generic type parameters when constructors or functions are used without explicit annotations, eliminating false errors in mutually recursive code:
```gleam
type Test(a) {
Test(a)
}

fn it(value: Test(a)) {
it2(value)
}

fn it2(value: Test(a)) -> Test(a) {
it(value)
}
```
Previously this could fail with an incorrect "Type mismatch" error:
```
Type mismatch

The type of this returned value doesn't match the return type
annotation of this function.

Expected type:

Test(a)

Found type:

Test(a)
```
([Adi Salimgereyev](https://github.com/abs0luty))

### Build tool

- The help text displayed by `gleam dev --help`, `gleam test --help`, and
Expand Down
23 changes: 19 additions & 4 deletions compiler-core/src/analyse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -673,8 +673,18 @@ impl<'a, A> ModuleAnalyzer<'a, A> {
}

// Assert that the inferred type matches the type of any recursive call
if let Err(error) = unify(preregistered_type.clone(), type_) {
self.problems.error(convert_unify_error(error, location));
if let Err(error) = unify(preregistered_type.clone(), type_.clone()) {
let mut instantiated_ids = im::HashMap::new();
let flexible_hydrator = Hydrator::new();
let instantiated_annotation = environment.instantiate(
preregistered_type.clone(),
&mut instantiated_ids,
&flexible_hydrator,
);

if unify(instantiated_annotation, type_.clone()).is_err() {
self.problems.error(convert_unify_error(error, location));
}
}

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

// Store the inferred type (not the preregistered type) in the environment.
// This ensures concrete type information flows through recursive calls - e.g., if we infer
// `fn() -> Test(Int)`, callers see that instead of the generic `fn() -> Test(a)`.
environment.insert_variable(
name.clone(),
variant,
preregistered_type.clone(),
type_.clone(),
publicity,
deprecation.clone(),
);
Expand All @@ -731,6 +744,8 @@ impl<'a, A> ModuleAnalyzer<'a, A> {
ReferenceKind::Definition,
);

// Use the inferred return type for the typed AST node.
// This matches the type stored in the environment above.
let function = Function {
documentation: doc,
location,
Expand All @@ -741,7 +756,7 @@ impl<'a, A> ModuleAnalyzer<'a, A> {
body_start,
end_position: end_location,
return_annotation,
return_type: preregistered_type
return_type: type_
.return_type()
.expect("Could not find return type for fn"),
body,
Expand Down
174 changes: 174 additions & 0 deletions compiler-core/src/language_server/code_action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1371,13 +1371,187 @@ fn collect_type_variables(printer: &mut Printer<'_>, function: &ast::TypedFuncti
}

impl<'ast, 'a, 'b> ast::visit::Visit<'ast> for TypeVariableCollector<'a, 'b> {
fn visit_typed_function(&mut self, fun: &'ast ast::TypedFunction) {
for argument in fun.arguments.iter() {
if let Some(annotation) = &argument.annotation {
register_type_variables_from_annotation(
self.printer,
annotation,
argument.type_.as_ref(),
);
}
}

if let Some(annotation) = &fun.return_annotation {
register_type_variables_from_annotation(
self.printer,
annotation,
fun.return_type.as_ref(),
);
}

ast::visit::visit_typed_function(self, fun);
}

fn visit_typed_expr_fn(
&mut self,
location: &'ast SrcSpan,
type_: &'ast Arc<Type>,
kind: &'ast FunctionLiteralKind,
arguments: &'ast [TypedArg],
body: &'ast Vec1<TypedStatement>,
return_annotation: &'ast Option<ast::TypeAst>,
) {
if let Type::Fn {
arguments: argument_types,
return_: return_type,
..
} = type_.as_ref()
{
for (argument, argument_type) in arguments.iter().zip(argument_types) {
if let Some(annotation) = &argument.annotation {
register_type_variables_from_annotation(
self.printer,
annotation,
argument_type.as_ref(),
);
}
}

if let Some(annotation) = return_annotation {
register_type_variables_from_annotation(
self.printer,
annotation,
return_type.as_ref(),
);
}
}

ast::visit::visit_typed_expr_fn(
self,
location,
type_,
kind,
arguments,
body,
return_annotation,
);
}

fn visit_type_ast_var(&mut self, _location: &'ast SrcSpan, name: &'ast EcoString) {
// Register this type variable so that we don't duplicate names when
// adding annotations.
self.printer.register_type_variable(name.clone());
}
}

fn register_type_variables_from_annotation(
printer: &mut Printer<'_>,
annotation: &ast::TypeAst,
type_: &Type,
) {
// fn wibble(a, b, c) {
// fn(a: b, b: c) -> d { ... }
// ^
// Without this tracking the printer could rename `d` to a fresh `h`.
match (annotation, type_) {
(ast::TypeAst::Var(ast::TypeAstVar { name, .. }), Type::Var { type_ }) => {
match &*type_.borrow() {
TypeVar::Generic { id } | TypeVar::Unbound { id } => {
let id = *id;
printer.register_type_variable(name.clone());
printer.register_type_variable_with_id(id, name.clone());
}
TypeVar::Link { type_ } => {
register_type_variables_from_annotation(printer, annotation, type_.as_ref());
}
}
}

(
ast::TypeAst::Fn(ast::TypeAstFn {
arguments: annotation_arguments,
return_: annotation_return,
..
}),
Type::Fn {
arguments: type_arguments,
return_: type_return,
..
},
) => {
for (argument_annotation, argument_type) in
annotation_arguments.iter().zip(type_arguments)
{
// Maintain the names from each `fn(arg: name, ...)` position.
register_type_variables_from_annotation(
printer,
argument_annotation,
argument_type.as_ref(),
);
}

// And likewise propagate the annotated return variable.
register_type_variables_from_annotation(
printer,
annotation_return.as_ref(),
type_return.as_ref(),
);
}

(
ast::TypeAst::Constructor(ast::TypeAstConstructor {
arguments: annotation_arguments,
..
}),
Type::Named {
arguments: type_arguments,
..
},
) => {
for (argument_annotation, argument_type) in
annotation_arguments.iter().zip(type_arguments)
{
// Track aliases introduced inside named type arguments.
register_type_variables_from_annotation(
printer,
argument_annotation,
argument_type.as_ref(),
);
}
}

(
ast::TypeAst::Tuple(ast::TypeAstTuple {
elements: annotation_elements,
..
}),
Type::Tuple {
elements: type_elements,
..
},
) => {
for (element_annotation, element_type) in annotation_elements.iter().zip(type_elements)
{
// Tuples can hide extra annotations; ensure each slot retains its label.
register_type_variables_from_annotation(
printer,
element_annotation,
element_type.as_ref(),
);
}
}

(_, Type::Var { type_ }) => {
if let TypeVar::Link { type_ } = &*type_.borrow() {
register_type_variables_from_annotation(printer, annotation, type_.as_ref());
}
}

_ => {}
}
}

pub struct QualifiedConstructor<'a> {
import: &'a Import<EcoString>,
used_name: EcoString,
Expand Down
58 changes: 33 additions & 25 deletions compiler-core/src/type_/expression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4707,31 +4707,39 @@ impl<'a, 'b> ExprTyper<'a, 'b> {
if let Ok(body) = Vec1::try_from_vec(body) {
let mut body = body_typer.infer_statements(body);

// Check that any return type is accurate.
if let Some(return_type) = return_type
&& let Err(error) = unify(return_type, body.last().type_())
{
let error = error
.return_annotation_mismatch()
.into_error(body.last().type_defining_location());
body_typer.problems.error(error);

// If the return type doesn't match with the annotation we
// add a new expression to the end of the function to match
// the annotated type and allow type inference to keep
// going.
body.push(Statement::Expression(TypedExpr::Invalid {
// This is deliberately an empty span since this
// placeholder expression is implicitly inserted by the
// compiler and doesn't actually appear in the source
// code.
location: SrcSpan {
start: body.last().location().end,
end: body.last().location().end,
},
type_: body_typer.new_unbound_var(),
extra_information: None,
}))
// Check that any return type is compatible with the annotation.
if let Some(return_type) = return_type {
let mut instantiated_ids = hashmap![];
let flexible_hydrator = Hydrator::new();
let instantiated_annotation = body_typer.environment.instantiate(
return_type.clone(),
&mut instantiated_ids,
&flexible_hydrator,
);

if let Err(error) = unify(instantiated_annotation, body.last().type_()) {
let error = error
.return_annotation_mismatch()
.into_error(body.last().type_defining_location());
body_typer.problems.error(error);

// If the return type doesn't match with the annotation we
// add a new expression to the end of the function to match
// the annotated type and allow type inference to keep
// going.
body.push(Statement::Expression(TypedExpr::Invalid {
// This is deliberately an empty span since this
// placeholder expression is implicitly inserted by the
// compiler and doesn't actually appear in the source
// code.
location: SrcSpan {
start: body.last().location().end,
end: body.last().location().end,
},
type_: body_typer.new_unbound_var(),
extra_information: None,
}))
}
};

Ok((arguments, body.to_vec()))
Expand Down
6 changes: 6 additions & 0 deletions compiler-core/src/type_/printer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,12 @@ impl<'a> Printer<'a> {
_ = self.printed_type_variable_names.insert(name);
}

/// Record that the given type-variable id is already using the supplied name.
pub fn register_type_variable_with_id(&mut self, id: u64, name: EcoString) {
_ = self.printed_type_variable_names.insert(name.clone());
_ = self.printed_type_variables.insert(id, name);
}

pub fn print_type(&mut self, type_: &Type) -> EcoString {
let mut buffer = EcoString::new();
self.print(type_, &mut buffer, PrintMode::Normal);
Expand Down
Loading
Loading