Skip to content

Commit d3d31b2

Browse files
committed
Emphasize mismatching part of complex type mismatches
1 parent 8762fea commit d3d31b2

10 files changed

+509
-21
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@
88
on the Erlang target.
99
([Giacomo Cavalieri](https://github.com/giacomocavalieri))
1010

11+
- Type mismatch errors now emphasize the exact location of mismatches in complex
12+
types by highlighting the mismatched type parameters. This makes it easier to
13+
spot errors in types with multiple parameters or deep nesting.
14+
([Adi Salimgereyev](https://github.com/abs0luty))
15+
1116
### Build tool
1217

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

compiler-core/src/error.rs

Lines changed: 244 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use crate::type_::error::{
1212
IncorrectArityContext, InvalidImportKind, MissingAnnotation, ModuleValueUsageContext, Named,
1313
UnknownField, UnknownTypeHint, UnsafeRecordUpdateReason,
1414
};
15-
use crate::type_::printer::{Names, Printer};
15+
use crate::type_::printer::{Names, Printer, TypeAnnotation, TypePathStep};
1616
use crate::type_::{FieldAccessUsage, error::PatternMatchKind};
1717
use crate::{ast::BinOp, parse::error::ParseErrorType, type_::Type};
1818
use crate::{bit_array, diagnostic::Level, type_::UnifyErrorSituation};
@@ -2143,6 +2143,9 @@ But function expects:
21432143
situation,
21442144
} => {
21452145
let mut printer = Printer::new(names);
2146+
let expected = collapse_links(expected.clone());
2147+
let given = collapse_links(given.clone());
2148+
21462149
let mut text = if let Some(description) =
21472150
situation.as_ref().and_then(|s| s.description())
21482151
{
@@ -2153,10 +2156,42 @@ But function expects:
21532156
} else {
21542157
"".into()
21552158
};
2156-
text.push_str("Expected type:\n\n ");
2157-
text.push_str(&printer.print_type(expected));
2158-
text.push_str("\n\nFound type:\n\n ");
2159-
text.push_str(&printer.print_type(given));
2159+
2160+
let expected_display = printer.print_type(&expected);
2161+
let (given_display, annotations) =
2162+
printer.print_type_with_annotations(&given);
2163+
let differences = collect_type_differences(expected.clone(), given.clone());
2164+
2165+
// Only show smart diff suggestions if the type is complex enough
2166+
let highlight_lines = if should_show_smart_diff(&differences) {
2167+
build_highlight_lines(&differences, &annotations, &mut printer)
2168+
} else {
2169+
Vec::new()
2170+
};
2171+
2172+
// Only show "Expected type:" if there are no smart diff suggestions
2173+
if highlight_lines.is_empty() {
2174+
text.push_str("Expected type:\n\n ");
2175+
text.push_str(&expected_display);
2176+
text.push_str("\n\n");
2177+
}
2178+
2179+
text.push_str("Found type:\n\n ");
2180+
text.push_str(&given_display);
2181+
2182+
for highlight in highlight_lines {
2183+
text.push('\n');
2184+
text.push_str(" ");
2185+
for _ in 0..highlight.start {
2186+
text.push(' ');
2187+
}
2188+
let span_len = highlight.end.saturating_sub(highlight.start).max(1);
2189+
for _ in 0..span_len {
2190+
text.push('^');
2191+
}
2192+
text.push_str(" expected: ");
2193+
text.push_str(&highlight.expected);
2194+
}
21602195

21612196
let (main_message_location, main_message_text, extra_labels) =
21622197
match situation {
@@ -2170,9 +2205,11 @@ But function expects:
21702205
}) => (clause_location, None, vec![]),
21712206
// In all other cases we just highlight the offending expression, optionally
21722207
// adding the wrapping hint if it makes sense.
2173-
Some(_) | None => {
2174-
(location, hint_wrap_value_in_result(expected, given), vec![])
2175-
}
2208+
Some(_) | None => (
2209+
location,
2210+
hint_wrap_value_in_result(&expected, &given),
2211+
vec![],
2212+
),
21762213
};
21772214

21782215
Diagnostic {
@@ -4578,6 +4615,205 @@ fn hint_alternative_operator(op: &BinOp, given: &Type) -> Option<String> {
45784615
}
45794616
}
45804617

4618+
#[derive(Debug, Clone)]
4619+
struct TypeDifference {
4620+
path: Vec<TypePathStep>,
4621+
expected: Arc<Type>,
4622+
}
4623+
4624+
fn collect_type_differences(expected: Arc<Type>, given: Arc<Type>) -> Vec<TypeDifference> {
4625+
let mut differences = Vec::new();
4626+
let mut path = Vec::new();
4627+
collect_type_differences_inner(expected, given, &mut path, &mut differences);
4628+
differences
4629+
}
4630+
4631+
fn collect_type_differences_inner(
4632+
expected: Arc<Type>,
4633+
given: Arc<Type>,
4634+
path: &mut Vec<TypePathStep>,
4635+
differences: &mut Vec<TypeDifference>,
4636+
) {
4637+
let expected = collapse_links(expected);
4638+
let given = collapse_links(given);
4639+
4640+
if Arc::ptr_eq(&expected, &given) {
4641+
return;
4642+
}
4643+
4644+
match (&*expected, &*given) {
4645+
(
4646+
Type::Named {
4647+
module: module_expected,
4648+
name: name_expected,
4649+
arguments: arguments_expected,
4650+
..
4651+
},
4652+
Type::Named {
4653+
module: module_given,
4654+
name: name_given,
4655+
arguments: arguments_given,
4656+
..
4657+
},
4658+
) if module_expected == module_given
4659+
&& name_expected == name_given
4660+
&& arguments_expected.len() == arguments_given.len() =>
4661+
{
4662+
for (index, (expected_argument, given_argument)) in arguments_expected
4663+
.iter()
4664+
.zip(arguments_given.iter())
4665+
.enumerate()
4666+
{
4667+
path.push(TypePathStep::NamedArgument(index));
4668+
collect_type_differences_inner(
4669+
expected_argument.clone(),
4670+
given_argument.clone(),
4671+
path,
4672+
differences,
4673+
);
4674+
let _ = path.pop();
4675+
}
4676+
}
4677+
(
4678+
Type::Fn {
4679+
arguments: arguments_expected,
4680+
return_: return_expected,
4681+
},
4682+
Type::Fn {
4683+
arguments: arguments_given,
4684+
return_: return_given,
4685+
},
4686+
) if arguments_expected.len() == arguments_given.len() => {
4687+
for (index, (expected_argument, given_argument)) in arguments_expected
4688+
.iter()
4689+
.zip(arguments_given.iter())
4690+
.enumerate()
4691+
{
4692+
path.push(TypePathStep::FnArgument(index));
4693+
collect_type_differences_inner(
4694+
expected_argument.clone(),
4695+
given_argument.clone(),
4696+
path,
4697+
differences,
4698+
);
4699+
let _ = path.pop();
4700+
}
4701+
path.push(TypePathStep::FnReturn);
4702+
collect_type_differences_inner(
4703+
return_expected.clone(),
4704+
return_given.clone(),
4705+
path,
4706+
differences,
4707+
);
4708+
let _ = path.pop();
4709+
}
4710+
(
4711+
Type::Tuple {
4712+
elements: elements_expected,
4713+
..
4714+
},
4715+
Type::Tuple {
4716+
elements: elements_given,
4717+
..
4718+
},
4719+
) if elements_expected.len() == elements_given.len() => {
4720+
for (index, (expected_element, given_element)) in elements_expected
4721+
.iter()
4722+
.zip(elements_given.iter())
4723+
.enumerate()
4724+
{
4725+
path.push(TypePathStep::TupleElement(index));
4726+
collect_type_differences_inner(
4727+
expected_element.clone(),
4728+
given_element.clone(),
4729+
path,
4730+
differences,
4731+
);
4732+
let _ = path.pop();
4733+
}
4734+
}
4735+
_ => differences.push(TypeDifference {
4736+
path: path.clone(),
4737+
expected,
4738+
}),
4739+
}
4740+
}
4741+
4742+
#[derive(Debug)]
4743+
struct HighlightLine {
4744+
start: usize,
4745+
end: usize,
4746+
expected: EcoString,
4747+
}
4748+
4749+
fn build_highlight_lines(
4750+
differences: &[TypeDifference],
4751+
annotations: &[TypeAnnotation],
4752+
printer: &mut Printer<'_>,
4753+
) -> Vec<HighlightLine> {
4754+
let mut highlights = Vec::new();
4755+
4756+
for difference in differences {
4757+
if difference.path.is_empty() {
4758+
continue;
4759+
}
4760+
4761+
if let Some(annotation) = annotations
4762+
.iter()
4763+
.find(|annotation| annotation.path == difference.path)
4764+
{
4765+
if annotation.range.start == annotation.range.end {
4766+
continue;
4767+
}
4768+
let expected = printer.print_type(&difference.expected);
4769+
highlights.push(HighlightLine {
4770+
start: annotation.range.start,
4771+
end: annotation.range.end,
4772+
expected,
4773+
});
4774+
}
4775+
}
4776+
4777+
highlights.sort_by_key(|highlight| highlight.start);
4778+
highlights
4779+
}
4780+
4781+
/// Determines if the type mismatch is "complex enough" to warrant showing smart diff suggestions.
4782+
///
4783+
/// Smart diffs are helpful when:
4784+
/// 1. There are multiple type parameter mismatches (2+)
4785+
/// 2. The mismatch occurs at depth >= 2 (nested within nested types)
4786+
/// 3. The parent type has multiple parameters (2+) with at least one mismatch
4787+
fn should_show_smart_diff(differences: &[TypeDifference]) -> bool {
4788+
// Filter out empty path differences (top-level mismatches)
4789+
let nested_diffs: Vec<_> = differences.iter().filter(|d| !d.path.is_empty()).collect();
4790+
4791+
if nested_diffs.is_empty() {
4792+
return false;
4793+
}
4794+
4795+
// Count how many distinct mismatches we have
4796+
let num_mismatches = nested_diffs.len();
4797+
4798+
// Check the maximum depth of mismatches
4799+
let max_depth = nested_diffs.iter().map(|d| d.path.len()).max().unwrap_or(0);
4800+
4801+
// Criterion 1: Multiple mismatches (2+) at any depth
4802+
if num_mismatches >= 2 {
4803+
return true;
4804+
}
4805+
4806+
// Criterion 2: Deep nesting (depth >= 2)
4807+
// For example: Wrapper(List(Int)) has depth 2
4808+
if max_depth >= 2 {
4809+
return true;
4810+
}
4811+
4812+
// If we get here, we have exactly 1 mismatch at depth 1
4813+
// This is like List(Int) vs List(String) - simple enough to not need smart diff
4814+
false
4815+
}
4816+
45814817
fn hint_wrap_value_in_result(expected: &Arc<Type>, given: &Arc<Type>) -> Option<String> {
45824818
let expected = collapse_links(expected.clone());
45834819
let (expected_ok_type, expected_error_type) = expected.result_types()?;

0 commit comments

Comments
 (0)