Skip to content

Commit c03651d

Browse files
committed
better error spans
1 parent 38738f8 commit c03651d

3 files changed

Lines changed: 87 additions & 10 deletions

File tree

src/typechecker/infer.rs

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -917,7 +917,7 @@ impl InferCtx {
917917
.collect();
918918

919919
// Unify the argument with the instantiated param
920-
self.state.unify(span, &arg_ty, &instantiated_param)?;
920+
self.state.unify(arg.span(), &arg_ty, &instantiated_param)?;
921921

922922
// Post-check 1: verify no forall var leaked into ambient vars' solutions.
923923
// Catches escapes like `\x -> foo x` where x's type gets constrained
@@ -988,15 +988,15 @@ impl InferCtx {
988988

989989
let result_ty = Type::Unif(self.state.fresh_var());
990990
let expected_func_ty = Type::fun(arg_ty, result_ty.clone());
991-
self.state.unify(span, &func_ty, &expected_func_ty)?;
991+
self.state.unify(arg.span(), &func_ty, &expected_func_ty)?;
992992

993993
Ok(result_ty)
994994
}
995995

996996
fn infer_if(
997997
&mut self,
998998
env: &Env,
999-
span: crate::span::Span,
999+
_span: crate::span::Span,
10001000
cond: &Expr,
10011001
then_expr: &Expr,
10021002
else_expr: &Expr,
@@ -1009,7 +1009,7 @@ impl InferCtx {
10091009

10101010
let then_ty = self.infer(env, then_expr)?;
10111011
let else_ty = self.infer(env, else_expr)?;
1012-
self.state.unify(span, &then_ty, &else_ty)?;
1012+
self.state.unify(else_expr.span(), &then_ty, &else_ty)?;
10131013

10141014
if is_underscore {
10151015
Ok(Type::fun(Type::boolean(), then_ty))
@@ -1725,7 +1725,11 @@ impl InferCtx {
17251725

17261726
// Infer the body and unify with result type
17271727
let body_ty = self.infer_guarded(&alt_env, &alt.result)?;
1728-
self.state.unify(span, &result_ty, &body_ty)?;
1728+
let body_span = match &alt.result {
1729+
GuardedExpr::Unconditional(e) => e.span(),
1730+
GuardedExpr::Guarded(_) => alt.span,
1731+
};
1732+
self.state.unify(body_span, &result_ty, &body_ty)?;
17291733
}
17301734

17311735
// Exhaustiveness check: for each scrutinee, verify all constructors are covered
@@ -1782,14 +1786,14 @@ impl InferCtx {
17821786
fn infer_array(
17831787
&mut self,
17841788
env: &Env,
1785-
span: crate::span::Span,
1789+
_span: crate::span::Span,
17861790
elements: &[Expr],
17871791
) -> Result<Type, TypeError> {
17881792
let elem_ty = Type::Unif(self.state.fresh_var());
17891793

17901794
for elem in elements {
17911795
let t = self.infer(env, elem)?;
1792-
self.state.unify(span, &elem_ty, &t)?;
1796+
self.state.unify(elem.span(), &elem_ty, &t)?;
17931797
}
17941798

17951799
Ok(Type::array(elem_ty))
@@ -2193,7 +2197,7 @@ impl InferCtx {
21932197

21942198
// Apply: func expr (\_ -> rest)
21952199
let after_first = Type::Unif(self.state.fresh_var());
2196-
self.state.unify(span, &func_ty, &Type::fun(expr_ty, after_first.clone()))?;
2200+
self.state.unify(expr.span(), &func_ty, &Type::fun(expr_ty, after_first.clone()))?;
21972201
let discard_arg = Type::Unif(self.state.fresh_var());
21982202
let cont_ty = Type::fun(discard_arg, rest_ty);
21992203
let result = Type::Unif(self.state.fresh_var());
@@ -2223,7 +2227,7 @@ impl InferCtx {
22232227

22242228
// Apply: bind expr (\binder -> rest)
22252229
let after_first = Type::Unif(self.state.fresh_var());
2226-
self.state.unify(span, &func_ty, &Type::fun(expr_ty, after_first.clone()))?;
2230+
self.state.unify(expr.span(), &func_ty, &Type::fun(expr_ty, after_first.clone()))?;
22272231
let cont_ty = Type::fun(binder_ty, rest_ty);
22282232
let result = Type::Unif(self.state.fresh_var());
22292233
self.state.unify(span, &after_first, &Type::fun(cont_ty, result.clone()))?;

tests/snapshots.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ fn snap_expr_negate() {
100100
fn snap_expr_error_branch_mismatch() {
101101
insta::assert_snapshot!(
102102
format_expr_type(r#"if true then 1 else "x""#),
103-
@"ERROR: Could not match type Int with type String at 0:23"
103+
@"ERROR: Could not match type Int with type String at 20:23"
104104
);
105105
}
106106

tests/typechecker_comprehensive.rs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,20 @@ fn assert_module_not_implemented(source: &str) {
142142
);
143143
}
144144

145+
/// Assert that a type error's span covers exactly the expected source text.
146+
fn assert_error_span_text(source: &str, error_code: &str, expected_text: &str) {
147+
let (_, errors) = check_module_types(source);
148+
let err = errors.iter().find(|e| e.code() == error_code)
149+
.unwrap_or_else(|| panic!("expected {} error, got errors: {:?}", error_code, errors.iter().map(|e| format!("{} ({})", e.code(), e)).collect::<Vec<_>>()));
150+
let span = err.span();
151+
assert!(span.start <= span.end && span.end <= source.len(),
152+
"error span for {} is invalid: start={}, end={}, source len={}",
153+
error_code, span.start, span.end, source.len());
154+
let actual = &source[span.start..span.end];
155+
assert_eq!(actual, expected_text,
156+
"error span for {} should cover '{}' but covers '{}'", error_code, expected_text, actual);
157+
}
158+
145159
// ═══════════════════════════════════════════════════════════════════════════
146160
// 1. LITERALS
147161
// ═══════════════════════════════════════════════════════════════════════════
@@ -7945,3 +7959,62 @@ x = Wrap 42";
79457959
Type::app(Type::con("A", "Wrapper"), Type::int())
79467960
);
79477961
}
7962+
7963+
// ═══════════════════════════════════════════════════════════════════════════
7964+
// ERROR SPAN PRECISION TESTS
7965+
// ═══════════════════════════════════════════════════════════════════════════
7966+
7967+
#[test]
7968+
fn error_span_if_else_branch_mismatch() {
7969+
assert_error_span_text(
7970+
r#"module Test where
7971+
x = if true then 1 else "a""#,
7972+
"UnificationError",
7973+
"\"a\""
7974+
);
7975+
}
7976+
7977+
#[test]
7978+
fn error_span_case_alternative_body() {
7979+
assert_error_span_text(
7980+
"module Test where\nx = case true of\n true -> 1\n false -> \"a\"",
7981+
"UnificationError",
7982+
"\"a\""
7983+
);
7984+
}
7985+
7986+
#[test]
7987+
fn error_span_array_element_mismatch() {
7988+
assert_error_span_text(
7989+
"module Test where\nx = [1, 2, \"three\"]",
7990+
"UnificationError",
7991+
"\"three\""
7992+
);
7993+
}
7994+
7995+
#[test]
7996+
fn error_span_function_arg_mismatch() {
7997+
assert_error_span_text(
7998+
"module Test where\nf :: Int -> Int\nf n = n\nx = f \"hello\"",
7999+
"UnificationError",
8000+
"\"hello\""
8001+
);
8002+
}
8003+
8004+
#[test]
8005+
fn error_span_if_condition_not_boolean() {
8006+
assert_error_span_text(
8007+
"module Test where\nx = if 42 then 1 else 2",
8008+
"UnificationError",
8009+
"42"
8010+
);
8011+
}
8012+
8013+
#[test]
8014+
fn error_span_case_multiple_alternatives() {
8015+
assert_error_span_text(
8016+
"module Test where\nx = case 1 of\n 1 -> \"a\"\n 2 -> \"b\"\n _ -> true",
8017+
"UnificationError",
8018+
"true"
8019+
);
8020+
}

0 commit comments

Comments
 (0)