From 5dcf33a69ab359186f0635b702f9bb70a2806c11 Mon Sep 17 00:00:00 2001 From: Tim Fennis Date: Fri, 13 Nov 2026 22:15:49 +0100 Subject: [PATCH] =?UTF-8?q?fix(analyser):=20don't=20report=20errors=20that?= =?UTF-8?q?=20follow=20from=20an=20earlier=20one=20=F0=9F=A7=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- ndc_analyser/src/analyser/assignments.rs | 6 +- ndc_analyser/src/analyser/calls.rs | 11 ++- ndc_analyser/src/analyser/declarations.rs | 27 +++--- ndc_analyser/src/analyser/function_values.rs | 6 ++ ndc_analyser/src/analyser/mod.rs | 27 +++--- ndc_analyser/src/analyser/solver.rs | 70 +++++++++++----- ndc_analyser/src/analyser/tests.rs | 82 +++++++++++++++++++ ndc_analyser/src/types.rs | 42 +++++++++- ndc_core/src/static_type.rs | 49 +++++++++-- ndc_lsp/src/features/inlay_hints.rs | 8 ++ ...failed_call_in_a_list_is_reported_once.ndc | 5 ++ ...erating_a_failed_call_is_reported_once.ndc | 8 ++ ...9_a_failed_field_read_is_reported_once.ndc | 5 ++ 13 files changed, 289 insertions(+), 57 deletions(-) create mode 100644 tests/functional/programs/006_lists/059_a_failed_call_in_a_list_is_reported_once.ndc create mode 100644 tests/functional/programs/008_iterators/009_iterating_a_failed_call_is_reported_once.ndc create mode 100644 tests/functional/programs/015_struct/039_a_failed_field_read_is_reported_once.ndc diff --git a/ndc_analyser/src/analyser/assignments.rs b/ndc_analyser/src/analyser/assignments.rs index 13d0abb8..c87bfaad 100644 --- a/ndc_analyser/src/analyser/assignments.rs +++ b/ndc_analyser/src/analyser/assignments.rs @@ -68,6 +68,10 @@ impl Analyser { span: Span, ) -> StaticType { let arguments = [left_type.clone(), right_type.clone()]; + // An operand whose check failed was reported there. + if arguments.iter().any(StaticType::has_errors) { + return StaticType::Error; + } let ResolvedCall { binding, .. } = self.overloads().resolve_call( &self.resolver.augmented_candidates(id).in_place, &arguments, @@ -77,7 +81,7 @@ impl Analyser { self.emit(AnalysisError::function_not_found( operation, &arguments, span, )); - return StaticType::Any; + return StaticType::Error; } self.record_augmented_pick(id, binding); left_type.clone() diff --git a/ndc_analyser/src/analyser/calls.rs b/ndc_analyser/src/analyser/calls.rs index 1623e6db..fef46dc0 100644 --- a/ndc_analyser/src/analyser/calls.rs +++ b/ndc_analyser/src/analyser/calls.rs @@ -140,10 +140,17 @@ impl Analyser { &arguments, span, )); - StaticType::Any + StaticType::Error } } callee @ StaticType::Var(_) => self.call_unknown_function(&callee, arguments), + // The callee's check failed, which was reported there. + StaticType::Error => { + for argument in &arguments { + self.vars.settle_unknowns_as_errors(argument); + } + StaticType::Error + } // Nothing tells what the callee does with its arguments. callee if callee.could_be_callable() => { for argument in &arguments { @@ -156,7 +163,7 @@ impl Analyser { } other => { self.emit(AnalysisError::not_callable(&self.vars.shown(&other), span)); - StaticType::Any + StaticType::Error } }; }; diff --git a/ndc_analyser/src/analyser/declarations.rs b/ndc_analyser/src/analyser/declarations.rs index 8d205185..a5fe79ce 100644 --- a/ndc_analyser/src/analyser/declarations.rs +++ b/ndc_analyser/src/analyser/declarations.rs @@ -226,8 +226,9 @@ impl Analyser { /// `typ`, the type of the generic function `symbol`, unless it has more /// parts than the analyser can work with, as when each function in a /// chain doubles what the previous one returns: `fn f1(x) => f0(f0(x))` - /// with `fn f0(x) => (x, x)`. Then it's reported, and the function takes - /// and returns `Any`, so what calls it isn't built from its type. + /// with `fn f0(x) => (x, x)`. Then it's reported, and what the function + /// takes and returns are failed checks, so what calls it isn't built + /// from its type, and isn't reported again. fn within_size_limit( &mut self, symbol: SymbolId, @@ -251,11 +252,11 @@ impl Analyser { )); self.schemes.remove(&symbol); let StaticType::Function { parameters, .. } = typ else { - return StaticType::Any; + return StaticType::Error; }; StaticType::Function { - parameters: parameters.map(|parameters| vec![StaticType::Any; parameters.len()]), - return_type: Box::new(StaticType::Any), + parameters: parameters.map(|parameters| vec![StaticType::Error; parameters.len()]), + return_type: Box::new(StaticType::Error), } } @@ -336,7 +337,7 @@ impl Analyser { return; } // If the type is a fixed-length Tuple whose arity doesn't match - // the number of bindings, fall back to Any for each element. This + // the number of bindings, bind each name as a failed check. This // can happen when a variable is declared with one type (e.g. ()) // and later reassigned to a tuple of a different arity — the // analyser doesn't track reassignment types. @@ -351,7 +352,7 @@ impl Analyser { elems.len(), span, )); - self.bind_patterns_as_any(seq, span); + self.bind_patterns_as_errors(seq, span); return; } else { Box::new(elems.iter()) @@ -360,7 +361,7 @@ impl Analyser { iter } else { self.emit(AnalysisError::unable_to_unpack_type(&resolved_type, span)); - self.bind_patterns_as_any(seq, span); + self.bind_patterns_as_errors(seq, span); return; }; @@ -390,18 +391,18 @@ impl Analyser { if desired_length != actual_len { self.emit(AnalysisError::unable_to_unpack_type(&found_type, span)); - self.bind_patterns_as_any(&seq[actual_len..], span); + self.bind_patterns_as_errors(&seq[actual_len..], span); } } } } - /// Binds every name in `patterns` as `Any`, so a pattern that failed to - /// type-check still declares its names. In `let (a, b, c) = (1, 2)`, `c` + /// Binds every name in `patterns` as a failed check, so a pattern that + /// failed to type-check still declares its names. In `let (a, b, c) = (1, 2)`, `c` /// is bound and a later `print(c)` reports nothing further. - fn bind_patterns_as_any(&mut self, patterns: &[BindingPatternLocation], span: Span) { + fn bind_patterns_as_errors(&mut self, patterns: &[BindingPatternLocation], span: Span) { for pattern in patterns { - self.declare_binding_pattern(pattern, None, StaticType::Any, span); + self.declare_binding_pattern(pattern, None, StaticType::Error, span); } } } diff --git a/ndc_analyser/src/analyser/function_values.rs b/ndc_analyser/src/analyser/function_values.rs index 5254a646..e05c6f8e 100644 --- a/ndc_analyser/src/analyser/function_values.rs +++ b/ndc_analyser/src/analyser/function_values.rs @@ -283,6 +283,12 @@ impl Analyser { /// of it decides it, and returns whether it did. pub(super) fn try_function_value(&mut self, value: &FunctionValue) -> bool { let expected = self.vars.resolve(&value.expected); + // What's expected of it comes from a check that failed, as in + // `missing().map(abs)`, which was reported there. + if expected.has_errors() { + self.give_up_on_function_value(value); + return true; + } match self.choose_function_value(value, &expected) { Choice::One(symbol) => { self.commit_function_value(value, symbol, expected.has_vars()); diff --git a/ndc_analyser/src/analyser/mod.rs b/ndc_analyser/src/analyser/mod.rs index ee638d07..9464dfe5 100644 --- a/ndc_analyser/src/analyser/mod.rs +++ b/ndc_analyser/src/analyser/mod.rs @@ -202,7 +202,7 @@ impl Analyser { } let expected = self .vars - .resolve(&expected) + .finish(&expected) .replace(&|part| matches!(part, StaticType::Generic(_)).then_some(StaticType::Any)); if expected == StaticType::Any { continue; @@ -222,10 +222,13 @@ impl Analyser { &mut analysis.function_types, ] { for typ in table.values_mut() { - *typ = self.vars.resolve(typ); + *typ = self.vars.finish(typ); debug_assert!(!typ.has_vars(), "unsettled type variable in {typ}"); } } + for typ in analysis.cast_checks.values_mut().flatten() { + *typ = self.vars.finish(typ); + } let mut result = std::mem::take(&mut self.result); Layout::new(&self.resolver).lay_out( @@ -256,7 +259,8 @@ impl Analyser { } /// Records an analysis error. The analyser continues with a fallback type - /// (usually `Any`) so that subsequent code is still checked. An error in + /// (usually [`StaticType::Error`], which nothing reports again) so that + /// the rest of the program is still checked. An error in /// a copy of a generic function's constraint is the call's: it's /// reported there, with notes pointing into the function. fn emit(&mut self, err: AnalysisError) { @@ -279,7 +283,7 @@ impl Analyser { /// and a struct name becomes that struct's `StaticType::Struct`. The /// resolver has already matched each name to its type symbol. A wrong /// generic-argument count is reported as an analysis error at the - /// annotation's span, and the annotation falls back to `Any` so the rest + /// annotation's span, and the annotation is a failed check so the rest /// of the program is still checked. fn lower_type_expr(&mut self, expr: &TypeExpr) -> StaticType { match expr { @@ -310,7 +314,7 @@ impl Analyser { match self.struct_types.get(&symbol) { Some(_) if has_args => { self.emit(AnalysisError::type_does_not_take_generic_args(name, *span)); - StaticType::Any + StaticType::Error } Some(info) => info.static_type(), // A built-in type: `from_name_and_args` checks its generic arity. @@ -318,7 +322,7 @@ impl Analyser { Ok(typ) => typ, Err(err) => { self.emit(AnalysisError::invalid_type_annotation(&err, *span)); - StaticType::Any + StaticType::Error } }, } @@ -341,18 +345,19 @@ impl Analyser { self.without_iterables(typ, expr.span()) } - /// `typ`, reporting each `Iterable` in it as `Any`. + /// `typ`, reporting each `Iterable` in it, which becomes the type of + /// a failed check. fn without_iterables(&mut self, typ: StaticType, span: Span) -> StaticType { if !typ.contains(&|part| matches!(part, StaticType::Iterable(_))) { return typ; } self.emit(AnalysisError::iterable_in_an_annotation(span)); - typ.replace(&|part| matches!(part, StaticType::Iterable(_)).then_some(StaticType::Any)) + typ.replace(&|part| matches!(part, StaticType::Iterable(_)).then_some(StaticType::Error)) } /// Analyses a top-level statement. Names resolve first; a statement with /// unresolved names isn't analysed further, which would only repeat its errors. - /// The names it still declared are bound as `Any`, so in + /// The names it still declared are bound as failed checks, so in /// `fn f() { missing } let g = f`, only `missing` is reported. pub fn analyse(&mut self, expr_loc: &ExpressionLocation) -> StaticType { let bound_before = self.resolver.current_bindings().len(); @@ -364,9 +369,9 @@ impl Analyser { .extend(name_errors.into_iter().map(AnalysisError::from)); let declared = self.resolver.current_bindings()[bound_before..].to_vec(); for symbol in declared { - self.types.declare(symbol, StaticType::Any); + self.types.declare(symbol, StaticType::Error); } - return StaticType::Any; + return StaticType::Error; } self.analyse_expr(expr_loc) } diff --git a/ndc_analyser/src/analyser/solver.rs b/ndc_analyser/src/analyser/solver.rs index 1ff42c29..91907052 100644 --- a/ndc_analyser/src/analyser/solver.rs +++ b/ndc_analyser/src/analyser/solver.rs @@ -333,7 +333,7 @@ impl Analyser { } let error = AnalysisError::not_iterable(&self.vars.shown(sequence), span); self.emit(error); - StaticType::Any + StaticType::Error } /// The types `let (a, b, …) = value` gives each name when the kind of @@ -459,6 +459,11 @@ impl Analyser { .iter() .map(|argument| self.vars.resolve(argument)) .collect::>(); + // A call passed a value whose check failed later picks + // nothing, and the program doesn't run. + if arguments.iter().any(StaticType::has_errors) { + continue; + } // A pick left to the program among one overload is a // call of it, with its `Any` arguments checked. let binding = match self @@ -489,6 +494,9 @@ impl Analyser { } EarlyPick::FunctionValue { value, picked } => { let expected = self.vars.resolve(value.expected()); + if expected.has_errors() { + continue; + } let choice = self.choose_function_value(&value, &expected); assert!( choice == Choice::One(picked), @@ -1109,7 +1117,7 @@ impl Analyser { found } }; - let found = found.unwrap_or_else(|| vec![StaticType::Any; parts.len()]); + let found = found.unwrap_or_else(|| vec![StaticType::Error; parts.len()]); for (part, found) in parts.iter().zip(found) { self.check_write(part, &found, *span, None); } @@ -1173,6 +1181,9 @@ impl Analyser { .iter() .map(|argument| self.vars.resolve(argument)) .collect::>(); + if arguments.iter().any(StaticType::has_errors) { + return Some(self.skip_failed_call(call, &arguments)); + } // A tuple indexed by a number written in the code gives that // element, of its own type: `(1, "a")[1]` is a String, and // `(1, "a")[-2]` an Int. @@ -1196,7 +1207,7 @@ impl Analyser { elements.len(), call.span, )); - return Some(StaticType::Any); + return Some(StaticType::Error); }; return Some(element); } @@ -1236,22 +1247,28 @@ impl Analyser { Binding::Resolved(Candidate::Scalar(function)), &call.arguments, ); - return Some(StaticType::Any); + return Some(StaticType::Error); } - // A variable whose type isn't known yet, like the parameter `f` in - // `fn(f) => f(1)`, holds a function that takes these arguments. - if let [only] = reachable[..] - && let callee @ StaticType::Var(var) = self.vars.resolve(self.types.get_type(only)) - && !self.vars.is_literal(var) - { - let typ = self.call_unknown_function(&callee, arguments); - self.record_pick( - call.site, - Binding::Resolved(Candidate::Scalar(only)), - &call.arguments, - ); - return Some(typ); + if let [only] = reachable[..] { + match self.vars.resolve(self.types.get_type(only)) { + // A variable whose type isn't known yet, like the parameter + // `f` in `fn(f) => f(1)`, holds a function that takes these + // arguments. + callee @ StaticType::Var(var) if !self.vars.is_literal(var) => { + let typ = self.call_unknown_function(&callee, arguments); + self.record_pick( + call.site, + Binding::Resolved(Candidate::Scalar(only)), + &call.arguments, + ); + return Some(typ); + } + // A variable whose value's check failed, like `f` in `let f + // = missing(); f(1)`, was reported there. + StaticType::Error => return Some(self.skip_failed_call(call, &arguments)), + _ => {} + } } // A variable holding a literal, like `n` in `let n = 1; n(2)`, can't @@ -1318,7 +1335,7 @@ impl Analyser { EarlyOverload::Only(only) => { let Some(typ) = self.commit_early(call, &arguments, only) else { self.report_failed_call(call, &arguments); - return Some(StaticType::Any); + return Some(StaticType::Error); }; Some(typ) } @@ -1326,6 +1343,19 @@ impl Analyser { } } + /// Skips a call that's passed a value whose check failed, or that calls + /// one, as that was reported where it failed: the call picks nothing, + /// settles what only it could have said about its arguments as failed + /// too, and gives a value whose check failed. In `missing().len()`, only + /// `missing` is reported. + fn skip_failed_call(&mut self, call: &CallConstraint, arguments: &[StaticType]) -> StaticType { + for argument in arguments { + self.vars.settle_unknowns_as_errors(argument); + } + self.record_pick(call.site, Binding::None, arguments); + StaticType::Error + } + /// Whether an operator might still apply to each element of a tuple: an /// operand that isn't known yet, or is `Any`, can be one. fn operand_could_be_a_tuple(kind: CallKind, arguments: &[StaticType]) -> bool { @@ -1599,7 +1629,7 @@ impl Analyser { if matches!(binding, Binding::None) { self.report_failed_call(call, arguments); self.record_pick(call.site, binding, arguments); - return StaticType::Any; + return StaticType::Error; } let typ = self.commit_call(&binding, arguments, &call.nodes.arguments, return_type); self.record_pick(call.site, binding, arguments); @@ -1609,7 +1639,7 @@ impl Analyser { } else { self.pending.clear(); self.report_failed_call(call, arguments); - StaticType::Any + StaticType::Error } } diff --git a/ndc_analyser/src/analyser/tests.rs b/ndc_analyser/src/analyser/tests.rs index d3183046..f7d95e92 100644 --- a/ndc_analyser/src/analyser/tests.rs +++ b/ndc_analyser/src/analyser/tests.rs @@ -1743,3 +1743,85 @@ fn a_call_no_overload_takes_says_what_the_overloads_take() { result.errors ); } + +/// Natives for the tests of what's built from a failed check. +fn failed_check_natives() -> Vec<(String, StaticType)> { + let mut globals = natives(&[ + ("len", "(Iterable) -> Int"), + ("map", "(Iterable, Fn(T) -> U) -> List"), + ("sum", "(Iterable) -> Int"), + ("sum", "(Iterable) -> Float"), + ("abs", "(Int) -> Int"), + ("abs", "(Float) -> Float"), + ("++", "(List, List) -> List"), + ("++", "(String, String) -> String"), + ]); + globals.extend(index_natives()); + globals +} + +#[test] +fn what_is_built_from_a_failed_call_is_not_reported_again() { + let globals = failed_check_natives(); + assert_eq!( + errors( + "struct Player { name: String, score: Int } + let players = [Player(\"ann\", 3), Player(\"bob\", 5)]; + players.map(fn(p) => p.len).sum();", + globals, + ), + vec!["No function called 'len' found that matches the arguments 'Player'"] + ); +} + +#[test] +fn a_failed_call_is_reported_once_wherever_its_value_goes() { + let globals = failed_check_natives(); + let failed = "No function called 'len' found that matches the arguments 'Bool'"; + for source in [ + // Indexed. + "let x = len(true)[0]; [x].sum();", + // Iterated. + "for x in len(true) { [x].sum(); }", + "let xs: List = [x for x in len(true)];", + // Passed on. + "[len(true)].sum();", + "len(true).map(abs);", + "len(true).map(fn(x) => [x]).sum();", + "abs(len(true)) ++ \"a\";", + // Assigned to a typed variable. + "let xs: List = [len(true)];", + "let s: String = len(true);", + // Called. + "let f = len(true); [f(1)].sum();", + // Unpacked. + "let (a, b) = len(true); [a, b].sum();", + ] { + assert_eq!(errors(source, globals.clone()), vec![failed], "{source}"); + } +} + +#[test] +fn an_error_after_a_failed_call_is_still_reported() { + let globals = failed_check_natives(); + assert_eq!( + errors( + "let n = len(true); let s: String = 1; [\"a\"].sum();", + globals, + ), + vec![ + "No function called 'len' found that matches the arguments 'Bool'", + "mismatched types: found Int but expected String", + "No function called 'sum' found that matches the arguments 'List'", + ] + ); +} + +#[test] +fn a_failed_annotation_is_not_reported_again() { + let globals = failed_check_natives(); + assert_eq!( + errors("let x: Int = 1; [x].sum();", globals), + vec!["type `Int` does not take generic arguments"] + ); +} diff --git a/ndc_analyser/src/types.rs b/ndc_analyser/src/types.rs index e62efb8c..a7e68ed7 100644 --- a/ndc_analyser/src/types.rs +++ b/ndc_analyser/src/types.rs @@ -520,7 +520,11 @@ impl TypeVars { typ => bounds.iter().all(|bound| match bound { Bound::Number(..) => matches!( typ, - StaticType::Int | StaticType::Float | StaticType::Number | StaticType::Any + StaticType::Int + | StaticType::Float + | StaticType::Number + | StaticType::Any + | StaticType::Error ), Bound::Holds(held, _, _) => self.bind_argument(&typ, held), }), @@ -636,6 +640,17 @@ impl TypeVars { }) } + /// `typ` as the analysis hands it on, once every variable in it is + /// settled: [`Self::resolve`]d, and with each failed check's type shown + /// as `Any`, which is what's known about it. + pub(crate) fn finish(&self, typ: &StaticType) -> StaticType { + typ.replace(&|part| match part { + StaticType::Var(var) => self.solution(*var).map(|solution| self.finish(solution)), + StaticType::Error => Some(StaticType::Any), + _ => None, + }) + } + /// `typ`, or what it stands for if it's a settled variable. fn shallow(&self, typ: &StaticType) -> StaticType { match typ { @@ -668,6 +683,9 @@ impl TypeVars { } self.settle(*var, other.clone()) } + // A failed check's type is every type, so nothing built from it + // fails again. + (StaticType::Error, _) | (_, StaticType::Error) => true, (StaticType::Option(x), StaticType::Option(y)) | (StaticType::Iterable(x), StaticType::Iterable(y)) | (StaticType::List(x), StaticType::List(y)) @@ -794,8 +812,12 @@ impl TypeVars { (_, StaticType::Never) | (StaticType::Any, _) => true, // A value of type `Any` is checked where it goes, so it leaves a // literal's type what the literal says: `n` in `let n = 1; - // n = a` is still an Int. - (StaticType::Var(var), StaticType::Any) if self.is_literal(*var) => true, + // n = a` is still an Int. So does a value whose check failed. + (StaticType::Var(var), StaticType::Any | StaticType::Error) + if self.is_literal(*var) => + { + true + } (StaticType::Iterable(_), StaticType::Var(var)) if !self.is_literal(*var) => true, // A literal passed for an `Iterable` is the sequence it holds, // as `["a"]` passed to `len` is a `List`: only a @@ -810,7 +832,7 @@ impl TypeVars { self.settle_literal(*var, &held) && self.bind_argument(¶meter, &held) } (StaticType::Var(_), _) | (_, StaticType::Var(_)) => self.unify(¶meter, &argument), - (_, StaticType::Any) => true, + (_, StaticType::Any | StaticType::Error) | (StaticType::Error, _) => true, ( StaticType::Function { parameters: expected, @@ -915,6 +937,18 @@ impl TypeVars { } } + /// Settles every unsettled variable in `typ` but a literal's as the + /// type of a failed check: in `missing(xs).map(fn(x) => x.size)`, the + /// call of `map` says nothing about `x`, which then isn't checked. + /// Literals take their defaults as usual. + pub(crate) fn settle_unknowns_as_errors(&mut self, typ: &StaticType) { + for var in self.unsettled(typ) { + if self.solution(var).is_none() && !self.is_literal(var) { + self.settle(var, StaticType::Error); + } + } + } + /// Settles every variable made since `first` that nothing settled with /// its fallback. pub(crate) fn settle_from(&mut self, first: usize) { diff --git a/ndc_core/src/static_type.rs b/ndc_core/src/static_type.rs index ec20a299..a7211aee 100644 --- a/ndc_core/src/static_type.rs +++ b/ndc_core/src/static_type.rs @@ -47,6 +47,13 @@ pub enum StaticType { /// it, which the analyser works out from the arguments. Every method /// here treats one as `Any`. Generic(Box), + /// The type of an expression whose check failed, which was reported. + /// Nothing built from it is reported again: it fits anything, anything + /// fits it, and a call it's passed to fails silently. In `[p].map(fn(p) + /// => p.size).sum()`, only the missing `size` is reported, not `sum` of + /// a list of what it gave. Only the analyser makes it, and it shows as + /// `Any`. + Error, /// Bottom type for diverging expressions (`return`, `break`, `continue`). /// A value of it fits anywhere, and it joins with any type as that type. Never, @@ -216,6 +223,7 @@ impl StaticType { pub fn join(&self, other: &Self) -> Self { match (self, other) { (Self::Never, other) | (other, Self::Never) => other.clone(), + (Self::Error, _) | (_, Self::Error) => Self::Error, _ if self == other => self.clone(), _ => Self::Any, } @@ -267,6 +275,7 @@ impl StaticType { key.as_ref().clone(), value.as_ref().clone(), ])), + Self::Error => Some(Self::Error), unknown if unknown.is_unknown() => Some(Self::Any), _ => None, } @@ -299,7 +308,11 @@ impl StaticType { /// this type too if `unknowns_match`. fn matches(&self, parameter: &Self, unknowns_match: bool) -> bool { let wildcard = |typ: &Self| matches!(typ, Self::Var(_) | Self::Generic(_)); - if wildcard(parameter) || (unknowns_match && wildcard(self)) { + if wildcard(parameter) + || (unknowns_match && wildcard(self)) + || self.is_error() + || parameter.is_error() + { return true; } let matches = |a: &Self, p: &Self| a.matches(p, unknowns_match); @@ -357,10 +370,25 @@ impl StaticType { } } - /// Whether this type stands for every type: `Any`, a type variable or - /// a type parameter, which are all `Any` to the methods here. + /// Whether this type stands for every type: `Any`, a type variable, a + /// type parameter or a failed check's type, which are all `Any` to the + /// methods here. pub fn is_unknown(&self) -> bool { - matches!(self, Self::Any | Self::Var(_) | Self::Generic(_)) + matches!( + self, + Self::Any | Self::Var(_) | Self::Generic(_) | Self::Error + ) + } + + /// Whether this is the type of an expression whose check failed. + pub fn is_error(&self) -> bool { + matches!(self, Self::Error) + } + + /// Whether a failed check's type is part of this type: `List` + /// is the type of a list holding a value whose check failed. + pub fn has_errors(&self) -> bool { + self.contains(&Self::is_error) } /// This type with each part that `replace` gives a type for replaced by @@ -399,6 +427,7 @@ impl StaticType { Self::Any | Self::Var(_) | Self::Generic(_) + | Self::Error | Self::Never | Self::Bool | Self::Struct { .. } @@ -437,6 +466,7 @@ impl StaticType { Self::Any | Self::Var(_) | Self::Generic(_) + | Self::Error | Self::Never | Self::Bool | Self::Struct { .. } @@ -678,7 +708,12 @@ impl StaticType { /// Whether this type and `other` could be one type, where what isn't /// known yet in either could be anything. fn agrees_with(&self, other: &Self) -> bool { - let unknown = |typ: &Self| matches!(typ, Self::Var(_) | Self::Generic(_) | Self::Never); + let unknown = |typ: &Self| { + matches!( + typ, + Self::Var(_) | Self::Generic(_) | Self::Never | Self::Error + ) + }; if unknown(self) || unknown(other) { return true; } @@ -725,6 +760,7 @@ impl StaticType { Self::Any | Self::Var(_) | Self::Generic(_) => { Some(Box::new(std::iter::repeat(&Self::Any))) } + Self::Error => Some(Box::new(std::iter::repeat(&Self::Error))), Self::List(elem) | Self::Iterable(elem) | Self::Iterator(elem) @@ -749,7 +785,8 @@ impl StaticType { impl fmt::Display for StaticType { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Self::Any => write!(f, "Any"), + // A failed check's type shows as what it was before there was one. + Self::Any | Self::Error => write!(f, "Any"), Self::Var(_) => write!(f, "_"), Self::Never => write!(f, "Never"), Self::Bool => write!(f, "Bool"), diff --git a/ndc_lsp/src/features/inlay_hints.rs b/ndc_lsp/src/features/inlay_hints.rs index 0d54b612..c252d209 100644 --- a/ndc_lsp/src/features/inlay_hints.rs +++ b/ndc_lsp/src/features/inlay_hints.rs @@ -110,6 +110,14 @@ mod tests { assert!(has_label(&hints, |l| l == ": Int")); } + #[test] + fn a_failed_check_is_shown_as_any() { + // `len(true)` fails, and what it gives is shown as `Any`. + let hints = collect_hints("let n = len(true);\nlet m = [n, n];"); + assert!(has_label(&hints, |l| l == ": Any"), "{hints:?}"); + assert!(has_label(&hints, |l| l == ": List"), "{hints:?}"); + } + #[test] fn annotated_let_binding_skips_type_inlay() { let hints = collect_hints("let value: Int = 1;"); diff --git a/tests/functional/programs/006_lists/059_a_failed_call_in_a_list_is_reported_once.ndc b/tests/functional/programs/006_lists/059_a_failed_call_in_a_list_is_reported_once.ndc new file mode 100644 index 00000000..de8396a6 --- /dev/null +++ b/tests/functional/programs/006_lists/059_a_failed_call_in_a_list_is_reported_once.ndc @@ -0,0 +1,5 @@ +// expect-error: No function called 'len' found that matches the arguments 'Bool' +// The list holds what a failed call gave, so `sum` isn't reported as well. +let x = len(true)[0]; +let xs = [x, x]; +print(xs.sum()); diff --git a/tests/functional/programs/008_iterators/009_iterating_a_failed_call_is_reported_once.ndc b/tests/functional/programs/008_iterators/009_iterating_a_failed_call_is_reported_once.ndc new file mode 100644 index 00000000..4e2bc254 --- /dev/null +++ b/tests/functional/programs/008_iterators/009_iterating_a_failed_call_is_reported_once.ndc @@ -0,0 +1,8 @@ +// expect-error: No function called 'len' found that matches the arguments 'Bool' +// What iterating gives comes from the failed call, so nothing built from it +// is reported. +let n = len(true); +let words: List = [x for x in n]; +for x in n.map(fn(i) => [i]) { + print(x.sum()); +} diff --git a/tests/functional/programs/015_struct/039_a_failed_field_read_is_reported_once.ndc b/tests/functional/programs/015_struct/039_a_failed_field_read_is_reported_once.ndc new file mode 100644 index 00000000..9848eaf0 --- /dev/null +++ b/tests/functional/programs/015_struct/039_a_failed_field_read_is_reported_once.ndc @@ -0,0 +1,5 @@ +// expect-error: No function called 'len' found that matches the arguments 'Player' +// Only the missing `len` is reported, not `sum` of the list of what it gave. +struct Player { name: String, score: Int } +let players = [Player("ann", 3), Player("bob", 5)]; +print(players.map(fn(p) => p.len).sum());