From f4307decc53d72290d2527aa0908fff129544585 Mon Sep 17 00:00:00 2001 From: Tim Fennis Date: Fri, 13 Nov 2026 17:41:43 +0100 Subject: [PATCH] =?UTF-8?q?fix(analyser):=20say=20what=20a=20function=20ta?= =?UTF-8?q?kes=20when=20no=20overload=20fits=20a=20call=20=F0=9F=92=AC?= 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/calls.rs | 25 +++++++++++++++++++++++++ ndc_analyser/src/analyser/tests.rs | 20 ++++++++++++++++++++ ndc_analyser/src/error.rs | 14 ++++++++++++++ 3 files changed, 59 insertions(+) diff --git a/ndc_analyser/src/analyser/calls.rs b/ndc_analyser/src/analyser/calls.rs index 52be8bf1..18c9b38c 100644 --- a/ndc_analyser/src/analyser/calls.rs +++ b/ndc_analyser/src/analyser/calls.rs @@ -2,6 +2,7 @@ use super::Analyser; use super::solver::{CallNodes, CallSite, Constraint}; use crate::error::AnalysisError; use crate::overloads::{CallKind, Overloads}; +use itertools::Itertools; use ndc_core::{NodeId, StaticType}; use ndc_lexer::{NumericLiteral, Span}; use ndc_parser::{Expression, ExpressionLocation}; @@ -32,9 +33,33 @@ impl Analyser { AnalysisError::function_not_found_suggest_cast(name, types, span) } else { AnalysisError::function_not_found(name, types, span) + .with_overloads(name, &self.what_the_overloads_take(candidates)) } } + /// What each overload `candidates` can reach takes, as `(Int, String)`, + /// when there are few enough to list: why `put("a")` doesn't fit a `put` + /// whose body made its parameter an Int. + fn what_the_overloads_take(&self, candidates: &Candidates) -> Vec { + const MOST: usize = 3; + let symbols = self.overloads().reachable_symbols(candidates); + if symbols.len() > MOST { + return Vec::new(); + } + symbols + .into_iter() + .filter_map( + |symbol| match self.vars.shown(self.types.get_type(symbol)) { + StaticType::Function { + parameters: Some(parameters), + .. + } => Some(format!("({})", parameters.iter().join(", "))), + _ => None, + }, + ) + .collect() + } + /// Overload resolution over the types declared so far. pub(super) fn overloads(&self) -> Overloads<'_> { Overloads::new(&self.types, &self.vars, self.resolver.symbols()) diff --git a/ndc_analyser/src/analyser/tests.rs b/ndc_analyser/src/analyser/tests.rs index 80a50a53..b75ae679 100644 --- a/ndc_analyser/src/analyser/tests.rs +++ b/ndc_analyser/src/analyser/tests.rs @@ -1721,3 +1721,23 @@ fn a_list_literal_holding_the_list_it_is_pushed_to_is_analysed() { assert!(result.errors.len() <= 1, "{source}: {:?}", result.errors); } } + +#[test] +fn a_call_no_overload_takes_says_what_the_overloads_take() { + // `put`'s body puts `x` in `r`, which the first call settles as holding + // Ints, so `put` takes an Int from then on. + let (_, result) = analyse_with_globals( + "let r = []; fn put(x) { r.push(x); 0 } put(1); put(\"a\");", + natives(&[("push", "(List, T)")]), + ); + let help = result + .errors + .iter() + .find_map(|error| error.help_text().map(str::to_string)); + assert_eq!( + help.as_deref(), + Some("`put` takes (Int)"), + "{:?}", + result.errors + ); +} diff --git a/ndc_analyser/src/error.rs b/ndc_analyser/src/error.rs index 926cc88c..757a2679 100644 --- a/ndc_analyser/src/error.rs +++ b/ndc_analyser/src/error.rs @@ -264,6 +264,20 @@ impl AnalysisError { } } + /// This error with help saying what the overloads of `name` take, given + /// as `(Int, String)` each, if any. + pub(crate) fn with_overloads(self, name: &str, overloads: &[String]) -> Self { + let help_text = match overloads { + [] => return self, + [only] => format!("`{name}` takes {only}"), + several => format!("The overloads of `{name}` take {}", several.join(", ")), + }; + Self { + help_text: Some(help_text), + ..self + } + } + /// Same as [`Self::function_not_found`], for a call an overload would /// accept if the arguments were known more precisely. pub(crate) fn function_not_found_suggest_cast(