Skip to main content

brokk_bifrost_cpp/
call_match.rs

1//! Overload disambiguation for C++ call sites.
2//!
3//! The only C++-only module that lived outside `cpp_graph/`: given a candidate
4//! set of same-named callables and the argument list at a call site, narrow by
5//! parameter type shape. Pure text and AST work over the signatures the
6//! declaration walk already emitted.
7
8use crate::declarations::{node_text as cpp_node_text, normalize_cpp_whitespace};
9use brokk_bifrost_core::analyzer::CodeUnit;
10use tree_sitter::Node;
11
12#[derive(Clone, PartialEq, Eq, Hash)]
13pub struct CppArgType {
14    pub name: String,
15    pub unit: Option<CodeUnit>,
16    pub indirection: i32,
17    pub pointee_const: bool,
18}
19
20/// The portion of C++ argument applicability that the declaration metadata can
21/// prove without running overload resolution or a conversion engine.
22///
23/// `Unknown` is intentional for user-defined conversions, standard
24/// conversions, templates, and incomplete argument/signature facts. The
25/// existing candidate filter remains conservative in all of those cases; this
26/// value only gives the trace a typed explanation for the evidence it has.
27#[derive(Clone, Copy, Debug, PartialEq, Eq)]
28pub enum CppArgumentApplicability {
29    Applicable,
30    Inapplicable,
31    Unknown,
32}
33
34pub fn cpp_signature_param_types(signature: &str) -> Option<Vec<String>> {
35    let inner = cpp_signature_parameter_text(signature)
36        .unwrap_or(signature)
37        .trim();
38    if inner.is_empty() || inner == "void" {
39        return Some(Vec::new());
40    }
41    Some(
42        cpp_split_top_level_commas(inner)
43            .map(cpp_parameter_type_text)
44            .collect(),
45    )
46}
47
48pub fn cpp_parameter_type_text(parameter: &str) -> String {
49    let mut text = parameter
50        .split_once('=')
51        .map(|(before, _)| before)
52        .unwrap_or(parameter)
53        .trim()
54        .trim_end_matches(';')
55        .trim();
56    let pointer_depth = cpp_type_text_pointer_depth(text);
57    if let Some((before, last)) = text.rsplit_once(char::is_whitespace)
58        && cpp_parameter_name_token(last)
59    {
60        text = before.trim();
61    }
62    let pointee_const = pointer_depth > 0 && cpp_type_text_pointee_is_const(text);
63    format!(
64        "{}{}{}",
65        if pointee_const { "const " } else { "" },
66        normalize_cpp_type_name(text),
67        "*".repeat(pointer_depth as usize)
68    )
69}
70
71pub fn normalize_cpp_type_name(text: &str) -> String {
72    let normalized = normalize_cpp_whitespace(text);
73    let base = cpp_type_text_base(&normalized)
74        .trim_start_matches("const ")
75        .trim();
76    strip_tag_type_prefix(base.strip_suffix(" const").unwrap_or(base)).to_string()
77}
78
79pub fn cpp_type_text_pointer_depth(text: &str) -> i32 {
80    cpp_type_text_shape(text).1
81}
82
83fn cpp_type_text_shape(text: &str) -> (usize, i32) {
84    let mut depth = 0i32;
85    let mut nesting = 0i32;
86    let mut base_end = text.len();
87    for (offset, ch) in text.char_indices() {
88        match ch {
89            '<' | '(' | '[' => nesting += 1,
90            '>' | ')' | ']' => nesting -= 1,
91            '*' if nesting <= 0 => {
92                base_end = base_end.min(offset);
93                depth += 1;
94            }
95            '&' if nesting <= 0 => base_end = base_end.min(offset),
96            _ => {}
97        }
98    }
99    (base_end, depth)
100}
101
102/// The single argument a `std::move` or `std::forward` call forwards, or `None`
103/// for every other call.
104///
105/// As far as an overload's parameter types are concerned these two are the
106/// identity: the argument's type is the call's type. Without that,
107/// `Montgomery_Int(m_params, std::move(t))` had one argument of unknown type,
108/// the argument filter kept every candidate rather than guess, and the call
109/// reported *ambiguous* between the `secure_vector<word>` and
110/// `std::span<const word>` overloads (#2552).
111///
112/// Recognized structurally, from the callee's `scope` and `name` fields: a
113/// `qualified_identifier` whose scope is `std` and whose name is `move` or
114/// `forward`, with the template arguments of `std::forward<T>(t)` unwrapped
115/// wherever the grammar attached them. A local `move(x)` or
116/// another namespace's `move` is not this, and neither is a call with any
117/// argument count but one -- that is not the standard signature, so the
118/// argument type stays unknown and the filter keeps every candidate.
119pub fn cpp_forwarding_call_argument<'tree>(call: Node<'tree>, source: &str) -> Option<Node<'tree>> {
120    if call.kind() != "call_expression" {
121        return None;
122    }
123    let mut callee = call.child_by_field_name("function")?;
124    if callee.kind() == "template_function" {
125        callee = callee.child_by_field_name("name")?;
126    }
127    if callee.kind() != "qualified_identifier" {
128        return None;
129    }
130    let scope = callee.child_by_field_name("scope")?;
131    if !matches!(scope.kind(), "namespace_identifier" | "identifier")
132        || cpp_node_text(scope, source).trim() != "std"
133    {
134        return None;
135    }
136    let mut name = callee.child_by_field_name("name")?;
137    // `std::forward<T>(t)` attaches its template arguments to the name half of
138    // the qualified name, so the `name` field is a `template_function` whose own
139    // `name` is the identifier.
140    if name.kind() == "template_function" {
141        name = name.child_by_field_name("name")?;
142    }
143    if !matches!(name.kind(), "identifier" | "field_identifier")
144        || !matches!(cpp_node_text(name, source).trim(), "move" | "forward")
145    {
146        return None;
147    }
148    let arguments = call.child_by_field_name("arguments")?;
149    let mut cursor = arguments.walk();
150    let forwarded = arguments
151        .named_children(&mut cursor)
152        .filter(|argument| argument.kind() != "comment")
153        .collect::<Vec<_>>();
154    let [argument] = forwarded.as_slice() else {
155        return None;
156    };
157    Some(*argument)
158}
159
160pub fn cpp_literal_arg_type(node: Node<'_>, source: &str) -> Option<CppArgType> {
161    let scalar = |name: &str| CppArgType {
162        name: name.to_string(),
163        unit: None,
164        indirection: 0,
165        pointee_const: false,
166    };
167    match node.kind() {
168        "number_literal" => {
169            let text = cpp_node_text(node, source);
170            if cpp_number_literal_is_float(text) {
171                Some(scalar("double"))
172            } else {
173                Some(scalar("int"))
174            }
175        }
176        "true" | "false" => Some(scalar("bool")),
177        "char_literal" => Some(scalar("char")),
178        "string_literal" => {
179            let text = cpp_node_text(node, source).trim_start();
180            (text.starts_with('"') || text.starts_with("R\"")).then(|| CppArgType {
181                name: "char".to_string(),
182                unit: None,
183                indirection: 1,
184                pointee_const: true,
185            })
186        }
187        "unary_expression" => {
188            let operator = node.child_by_field_name("operator")?;
189            let inner = node
190                .child_by_field_name("argument")
191                .or_else(|| node.named_child(0))?;
192            matches!(operator.kind(), "+" | "-")
193                .then(|| cpp_literal_arg_type(inner, source))
194                .flatten()
195        }
196        _ => None,
197    }
198}
199
200#[cfg(test)]
201fn cpp_filter_candidates_by_args(
202    candidates: Vec<CodeUnit>,
203    arg_types: &[Option<CppArgType>],
204    resolve_type: &dyn Fn(&str) -> Option<CodeUnit>,
205    assignable: &dyn Fn(&CodeUnit, &CodeUnit) -> bool,
206) -> Vec<CodeUnit> {
207    cpp_filter_candidates_by_args_with_parameter_types(
208        candidates,
209        arg_types,
210        &|candidate| cpp_signature_param_types(candidate.signature().unwrap_or_default()),
211        &cpp_signature_is_template_candidate,
212        resolve_type,
213        assignable,
214    )
215}
216
217pub fn cpp_filter_candidates_by_args_with_parameter_types(
218    candidates: Vec<CodeUnit>,
219    arg_types: &[Option<CppArgType>],
220    parameter_types: &dyn Fn(&CodeUnit) -> Option<Vec<String>>,
221    is_template: &dyn Fn(&CodeUnit) -> bool,
222    resolve_type: &dyn Fn(&str) -> Option<CodeUnit>,
223    assignable: &dyn Fn(&CodeUnit, &CodeUnit) -> bool,
224) -> Vec<CodeUnit> {
225    if candidates.len() <= 1 || arg_types.iter().any(Option::is_none) {
226        return candidates;
227    }
228    let args: Vec<&CppArgType> = arg_types.iter().flatten().collect();
229    debug_assert_eq!(args.len(), arg_types.len());
230
231    // Exact matches first; standard conversions decide only when nothing
232    // matches exactly. That order is C++'s own -- an identity conversion
233    // sequence beats every other one -- and it is what keeps
234    // `own(std::vector<uint8_t>)` winning over `own(std::span<const uint8_t>)`
235    // for a `std::vector<uint8_t>` argument now that the second is viable too.
236    let exact = cpp_candidates_matching(
237        &candidates,
238        &args,
239        parameter_types,
240        &|param, arg, template_candidate| {
241            cpp_param_matches_arg(param, arg, template_candidate, resolve_type, assignable)
242        },
243    );
244    let filtered = if exact.is_empty() {
245        cpp_candidates_matching(
246            &candidates,
247            &args,
248            parameter_types,
249            &|param, arg, template_candidate| {
250                cpp_param_matches_arg(param, arg, template_candidate, resolve_type, assignable)
251                    || cpp_standard_conversion_applies(arg, param)
252            },
253        )
254    } else {
255        exact
256    };
257    if filtered.is_empty() {
258        candidates
259    } else if filtered.iter().any(is_template) {
260        // A matching function template keeps the entire arity-compatible
261        // overload set alive. The parameter metadata has no template
262        // substitution or constraint ordering, so it cannot prove that a
263        // concrete sibling wins over a macro-constrained template (#2203).
264        candidates
265    } else {
266        filtered
267    }
268}
269
270/// Classify one candidate using the same structured parameter/type comparison
271/// as [`cpp_filter_candidates_by_args_with_parameter_types`]. This is a
272/// deliberately narrower question than overload selection: an exact written
273/// type match is applicable, a proven pointer-shape mismatch is inapplicable,
274/// and every conversion or incomplete fact stays unknown.
275pub fn cpp_candidate_argument_applicability(
276    candidate: &CodeUnit,
277    arg_types: &[Option<CppArgType>],
278    parameter_types: &dyn Fn(&CodeUnit) -> Option<Vec<String>>,
279    is_template: &dyn Fn(&CodeUnit) -> bool,
280    resolve_type: &dyn Fn(&str) -> Option<CodeUnit>,
281    assignable: &dyn Fn(&CodeUnit, &CodeUnit) -> bool,
282) -> CppArgumentApplicability {
283    let Some(parameters) = parameter_types(candidate) else {
284        return CppArgumentApplicability::Unknown;
285    };
286    if arg_types.iter().any(Option::is_none) || is_template(candidate) {
287        return CppArgumentApplicability::Unknown;
288    }
289    if parameters.len() != arg_types.len() {
290        return CppArgumentApplicability::Inapplicable;
291    }
292    let args: Vec<&CppArgType> = arg_types.iter().flatten().collect();
293    debug_assert_eq!(args.len(), arg_types.len());
294
295    let mut uncertain = false;
296    for (parameter, argument) in parameters.iter().zip(args) {
297        if cpp_type_text_pointer_depth(parameter) != argument.indirection
298            || (argument.pointee_const && !cpp_type_text_pointee_is_const(parameter))
299        {
300            return CppArgumentApplicability::Inapplicable;
301        }
302        if cpp_param_matches_arg(parameter, argument, false, resolve_type, assignable) {
303            continue;
304        }
305        // This is a known closed-list conversion in the existing filter. It
306        // is not a direct written type match, so leave it open to the future
307        // C++ conversion producer rather than claiming a rule-level answer.
308        if cpp_standard_conversion_applies(argument, parameter) {
309            uncertain = true;
310            continue;
311        }
312        // A same-shaped but differently named type may be accepted by a
313        // user-defined conversion or a constructor. The adapter has no
314        // complete conversion graph here, so it cannot reject it.
315        uncertain = true;
316    }
317    if uncertain {
318        CppArgumentApplicability::Unknown
319    } else {
320        CppArgumentApplicability::Applicable
321    }
322}
323
324/// The candidates whose parameter list has the call's arity and whose every
325/// parameter accepts the argument in that position under `matches`.
326fn cpp_candidates_matching(
327    candidates: &[CodeUnit],
328    args: &[&CppArgType],
329    parameter_types: &dyn Fn(&CodeUnit) -> Option<Vec<String>>,
330    matches: &dyn Fn(&str, &CppArgType, bool) -> bool,
331) -> Vec<CodeUnit> {
332    candidates
333        .iter()
334        .filter(|candidate| {
335            let template_candidate = cpp_signature_is_template_candidate(candidate);
336            parameter_types(candidate).is_some_and(|params| {
337                params.len() == args.len()
338                    && params
339                        .iter()
340                        .zip(args.iter())
341                        .all(|(param, arg)| matches(param, arg, template_candidate))
342            })
343        })
344        .cloned()
345        .collect()
346}
347fn cpp_signature_is_template_candidate(candidate: &CodeUnit) -> bool {
348    candidate
349        .signature()
350        .is_some_and(|signature| signature.trim_start().starts_with('<'))
351}
352
353fn cpp_param_matches_arg(
354    param: &str,
355    arg: &CppArgType,
356    template_candidate: bool,
357    resolve_type: &dyn Fn(&str) -> Option<CodeUnit>,
358    assignable: &dyn Fn(&CodeUnit, &CodeUnit) -> bool,
359) -> bool {
360    if cpp_type_text_pointer_depth(param) != arg.indirection {
361        return false;
362    }
363    if arg.pointee_const && !cpp_type_text_pointee_is_const(param) {
364        return false;
365    }
366    // Parameter metadata records the declared spelling, but it does not carry
367    // template substitution or constraint semantics. Once pointer shape and
368    // constness agree, a function template remains a live overload candidate:
369    // a type-only comparison such as `Vec256<T>` versus `Vec256<int>` cannot
370    // prove that deduction or a macro-shaped constraint fails (#2203).
371    if template_candidate {
372        return true;
373    }
374    let param_name = normalize_cpp_type_name(param);
375    match (resolve_type(&param_name), arg.unit.as_ref()) {
376        (Some(param_unit), Some(arg_unit)) => assignable(arg_unit, &param_unit),
377        _ => param_name == arg.name,
378    }
379}
380
381/// Whether an argument of type `arg` satisfies a parameter written `param`
382/// through a standard conversion. Asked only after name equality and
383/// derived-to-base have both failed for every candidate.
384///
385/// This is a closed list, not a conversion engine. Every entry is a conversion
386/// the analyzer can state from the two spellings alone -- no user-defined
387/// conversion operator, no converting-constructor lookup, no template
388/// deduction -- and every entry is here because a corpus call site is ambiguous
389/// without it:
390///
391/// - an owning contiguous range (`std::vector<T>`, `std::array<T, N>`)
392///   satisfies `std::span<T>` and `std::span<const T>`. `return DL_Group(ber,
393///   format);` with `const std::vector<uint8_t> ber` matched no candidate at
394///   all, so the filter kept the whole overload set (#2894).
395/// - `std::string` and a `char*` or `const char*` satisfy `std::string_view`.
396///
397/// Deliberately absent:
398///
399/// - `T*` to `std::span<T>`: viable only paired with a count argument, which is
400///   arity's decision rather than a per-parameter one.
401/// - `T[N]` to `std::span<T>`: an array argument arrives here spelled as its
402///   bare element type, because [`CppArgType`] records pointer depth and an
403///   array declarator adds none. Accepting it would accept every scalar `T`.
404/// - an alias of a listed container, such as Botan's `secure_vector<T>` for
405///   `std::vector<T>`: the argument carries the written spelling, and resolving
406///   the alias here would make `Montgomery_Int(m_params, std::move(t))` satisfy
407///   both its `secure_vector<word>` and its `std::span<const word>` overload.
408/// - the argument's own top-level `const`: [`CppArgType`] does not record it,
409///   so `const std::vector<uint8_t>` and `std::vector<uint8_t>` are one type
410///   here and both satisfy `std::span<uint8_t>`. Element `const` is recorded,
411///   and is checked.
412fn cpp_standard_conversion_applies(arg: &CppArgType, param: &str) -> bool {
413    if cpp_type_text_pointer_depth(param) != 0 {
414        return false;
415    }
416    let param_name = normalize_cpp_type_name(param);
417    let span_element = cpp_template_arguments(&param_name, "std::span")
418        .and_then(|arguments| arguments.first().copied());
419    if let Some(span_element) = span_element {
420        return arg.indirection == 0
421            && cpp_contiguous_range_element(&arg.name)
422                .is_some_and(|element| cpp_span_element_accepts(span_element, element));
423    }
424    if param_name == "std::string_view" {
425        return (arg.indirection == 0 && arg.name == "std::string")
426            || (arg.indirection == 1 && arg.name == "char");
427    }
428    false
429}
430
431/// The template arguments of `text` when it names a specialization of `base`.
432///
433/// Bracket aware like the parameter-list reader beside it: the argument split is
434/// [`cpp_split_top_level_commas`], so `std::array<std::pair<int, int>, 4>` reads
435/// as two arguments rather than three, and `std::vector<int>::iterator` is not a
436/// specialization of `std::vector` at all.
437fn cpp_template_arguments<'a>(text: &'a str, base: &str) -> Option<Vec<&'a str>> {
438    let inner = text
439        .strip_prefix(base)?
440        .trim_start()
441        .strip_prefix('<')?
442        .trim_end()
443        .strip_suffix('>')?;
444    Some(cpp_split_top_level_commas(inner).collect())
445}
446
447/// The element type of an owning contiguous range: the `T` of `std::vector<T>`
448/// or of `std::array<T, N>`.
449fn cpp_contiguous_range_element(name: &str) -> Option<&str> {
450    ["std::vector", "std::array"]
451        .into_iter()
452        .find_map(|base| cpp_template_arguments(name, base))
453        .and_then(|arguments| arguments.first().copied())
454}
455
456/// Whether a `std::span` over `param_element` accepts a range over
457/// `arg_element`. A span may add `const` to its element type, never drop it.
458fn cpp_span_element_accepts(param_element: &str, arg_element: &str) -> bool {
459    cpp_type_text_pointer_depth(param_element) == cpp_type_text_pointer_depth(arg_element)
460        && normalize_cpp_type_name(param_element) == normalize_cpp_type_name(arg_element)
461        && (cpp_type_text_pointee_is_const(param_element)
462            || !cpp_type_text_pointee_is_const(arg_element))
463}
464
465fn cpp_type_text_pointee_is_const(text: &str) -> bool {
466    let normalized = normalize_cpp_whitespace(text);
467    let base = cpp_type_text_base(&normalized).trim();
468    base.starts_with("const ") || base.ends_with(" const")
469}
470
471fn cpp_type_text_base(text: &str) -> &str {
472    text[..cpp_type_text_shape(text).0].trim()
473}
474
475pub fn cpp_split_top_level_commas(value: &str) -> impl Iterator<Item = &str> {
476    struct TopLevelCommaSplit<'a> {
477        value: &'a str,
478        start: usize,
479        angle: usize,
480        paren: usize,
481        brace: usize,
482        bracket: usize,
483    }
484
485    impl<'a> Iterator for TopLevelCommaSplit<'a> {
486        type Item = &'a str;
487
488        fn next(&mut self) -> Option<Self::Item> {
489            if self.start > self.value.len() {
490                return None;
491            }
492            for (offset, ch) in self.value[self.start..].char_indices() {
493                let absolute = self.start + offset;
494                match ch {
495                    '<' => self.angle += 1,
496                    '>' => self.angle = self.angle.saturating_sub(1),
497                    '(' => self.paren += 1,
498                    ')' => self.paren = self.paren.saturating_sub(1),
499                    '{' => self.brace += 1,
500                    '}' => self.brace = self.brace.saturating_sub(1),
501                    '[' => self.bracket += 1,
502                    ']' => self.bracket = self.bracket.saturating_sub(1),
503                    ',' if self.angle == 0
504                        && self.paren == 0
505                        && self.brace == 0
506                        && self.bracket == 0 =>
507                    {
508                        let item = self.value[self.start..absolute].trim();
509                        self.start = absolute + ch.len_utf8();
510                        return Some(item);
511                    }
512                    _ => {}
513                }
514            }
515            let item = self.value[self.start..].trim();
516            self.start = self.value.len() + 1;
517            Some(item)
518        }
519    }
520
521    TopLevelCommaSplit {
522        value,
523        start: 0,
524        angle: 0,
525        paren: 0,
526        brace: 0,
527        bracket: 0,
528    }
529    .filter(|item| !item.is_empty())
530}
531
532/// The byte offsets of a signature's outermost parameter-list parentheses.
533fn cpp_signature_parameter_span(signature: &str) -> Option<(usize, usize)> {
534    let open = signature.find('(')?;
535    let mut depth = 0i32;
536    for (offset, ch) in signature[open..].char_indices() {
537        match ch {
538            '(' => depth += 1,
539            ')' => {
540                depth -= 1;
541                if depth == 0 {
542                    return Some((open, open + offset));
543                }
544            }
545            _ => {}
546        }
547    }
548    None
549}
550
551fn cpp_signature_parameter_text(signature: &str) -> Option<&str> {
552    let (open, close) = cpp_signature_parameter_span(signature)?;
553    Some(signature[open + 1..close].trim())
554}
555
556/// What a signature carries after its parameter list: the trailing
557/// cv-/ref-qualifiers and `noexcept` that the signature identity records
558/// (#1827).
559///
560/// Two member declarations with the same parameter types but different
561/// trailing qualifiers are distinct declarations, so a caller deciding whether
562/// one declaration hides another has to compare this alongside the parameter
563/// types.
564pub fn cpp_signature_trailing_qualifiers(signature: &str) -> &str {
565    match cpp_signature_parameter_span(signature) {
566        Some((_, close)) => signature[close + 1..].trim(),
567        None => "",
568    }
569}
570
571#[derive(Clone, Copy, Debug, PartialEq, Eq)]
572pub enum CppRefQualifier {
573    Lvalue,
574    Rvalue,
575}
576
577/// The member ref-qualifier carried by an indexed identity signature.
578///
579/// Declaration extraction emits cv-qualifiers before the ref-qualifier, in
580/// the same order as the C++ declarator nodes. Reading that normalized product
581/// keeps call applicability on the declaration AST's answer without reparsing
582/// source text at every call site.
583pub fn cpp_signature_ref_qualifier(signature: &str) -> Option<CppRefQualifier> {
584    let mut suffix = cpp_signature_trailing_qualifiers(signature);
585    loop {
586        if let Some(rest) = suffix.strip_prefix("const ") {
587            suffix = rest;
588        } else if let Some(rest) = suffix.strip_prefix("volatile ") {
589            suffix = rest;
590        } else {
591            break;
592        }
593    }
594    if suffix == "&&" || suffix.starts_with("&& ") {
595        Some(CppRefQualifier::Rvalue)
596    } else if suffix == "&" || suffix.starts_with("& ") {
597        Some(CppRefQualifier::Lvalue)
598    } else {
599        None
600    }
601}
602
603fn cpp_parameter_name_token(token: &str) -> bool {
604    let token = token.trim_start_matches('*').trim_start_matches('&').trim();
605    token
606        .chars()
607        .next()
608        .is_some_and(|ch| ch == '_' || ch.is_ascii_lowercase())
609        && token
610            .chars()
611            .all(|ch| ch == '_' || ch.is_ascii_alphanumeric())
612}
613
614use crate::declarations::strip_tag_type_prefix;
615
616fn cpp_number_literal_is_float(text: &str) -> bool {
617    let text = text.trim();
618    text.contains('.') || text.contains('e') || text.contains('E') || text.ends_with(['f', 'F'])
619}
620
621#[cfg(test)]
622mod tests {
623    use super::*;
624    use brokk_bifrost_core::analyzer::ProjectFile;
625    use brokk_bifrost_core::analyzer::model::CodeUnitType;
626    use brokk_bifrost_test_macros::hardwon;
627
628    fn test_file() -> ProjectFile {
629        ProjectFile::new(std::env::temp_dir(), "test.cpp")
630    }
631
632    fn function(name: &str, signature: &str) -> CodeUnit {
633        CodeUnit::with_signature(
634            test_file(),
635            CodeUnitType::Function,
636            "ns",
637            name,
638            Some(signature.to_string()),
639            false,
640        )
641    }
642
643    fn class(name: &str) -> CodeUnit {
644        CodeUnit::new(test_file(), CodeUnitType::Class, "ns", name)
645    }
646
647    #[test]
648    #[hardwon("#427: overload call filtering conflated different callable candidates")]
649    fn cpp_filter_candidates_matches_named_unindexed_types() {
650        let candidates = vec![
651            function("format", "std::string format(const std::string& value)"),
652            function("format", "std::string format(int value)"),
653        ];
654        let filtered = cpp_filter_candidates_by_args(
655            candidates,
656            &[Some(CppArgType {
657                name: "std::string".to_string(),
658                unit: None,
659                indirection: 0,
660                pointee_const: false,
661            })],
662            &|_| None,
663            &|_, _| false,
664        );
665        assert_eq!(1, filtered.len());
666        assert!(filtered[0].signature().unwrap().contains("std::string&"));
667    }
668
669    #[test]
670    #[hardwon("#427: overload call filtering conflated different callable candidates")]
671    fn cpp_filter_candidates_matches_assignable_units() {
672        let arg = class("Arg");
673        let param = class("Param");
674        let filtered = cpp_filter_candidates_by_args(
675            vec![function("take", "void take(Param value)")],
676            &[Some(CppArgType {
677                name: "Arg".to_string(),
678                unit: Some(arg.clone()),
679                indirection: 0,
680                pointee_const: false,
681            })],
682            &|name| (name == "Param").then(|| param.clone()),
683            &|from, to| from == &arg && to == &param,
684        );
685        assert_eq!(1, filtered.len());
686    }
687
688    #[test]
689    #[hardwon("#427: overload call filtering conflated different callable candidates")]
690    fn cpp_filter_candidates_rejects_pointer_depth_mismatch() {
691        let candidates = vec![
692            function("take", "void take(int* value)"),
693            function("take", "void take(int value)"),
694        ];
695        let filtered = cpp_filter_candidates_by_args(
696            candidates,
697            &[Some(CppArgType {
698                name: "int".to_string(),
699                unit: None,
700                indirection: 0,
701                pointee_const: false,
702            })],
703            &|_| None,
704            &|_, _| false,
705        );
706        assert_eq!(1, filtered.len());
707        assert_eq!("void take(int value)", filtered[0].signature().unwrap());
708    }
709
710    #[test]
711    #[hardwon("#427: string literal types were missing from overload matching")]
712    fn cpp_filter_candidates_uses_const_string_literal_pointer_evidence() {
713        let literal = Some(CppArgType {
714            name: "char".to_string(),
715            unit: None,
716            indirection: 1,
717            pointee_const: true,
718        });
719        let direct = cpp_filter_candidates_by_args(
720            vec![
721                function("select", "int select(int value)"),
722                function("select", "int select(const char* value)"),
723            ],
724            std::slice::from_ref(&literal),
725            &|_| None,
726            &|_, _| false,
727        );
728        assert_eq!(1, direct.len());
729        assert_eq!(
730            "int select(const char* value)",
731            direct[0].signature().unwrap()
732        );
733
734        for candidates in [
735            vec![
736                function("select", "int select(int value)"),
737                function("select", "int select(char* value)"),
738            ],
739            vec![
740                function("format", "int format(int value)"),
741                function("format", "int format(std::string value)"),
742            ],
743        ] {
744            let filtered = cpp_filter_candidates_by_args(
745                candidates.clone(),
746                std::slice::from_ref(&literal),
747                &|_| None,
748                &|_, _| false,
749            );
750            assert_eq!(
751                candidates, filtered,
752                "unmodeled or invalid conversions must remain conservative"
753            );
754        }
755    }
756
757    #[test]
758    #[hardwon("#427: parameter pointer const was confused with pointee const")]
759    fn cpp_parameter_type_keeps_pointer_const_distinct_from_pointee_const() {
760        assert_eq!("char*", cpp_parameter_type_text("char * const value"));
761        assert_eq!(
762            "const char*",
763            cpp_parameter_type_text("const char * const value")
764        );
765        assert_eq!("char", normalize_cpp_type_name("char * const"));
766    }
767
768    #[test]
769    #[hardwon("#427: unknown argument types must preserve conservative overload candidates")]
770    fn cpp_filter_candidates_keeps_all_for_unknown_arguments() {
771        let candidates = vec![
772            function("format", "void format(std::string value)"),
773            function("format", "void format(int value)"),
774        ];
775        let filtered =
776            cpp_filter_candidates_by_args(candidates.clone(), &[None], &|_| None, &|_, _| false);
777        assert_eq!(candidates, filtered);
778    }
779
780    #[test]
781    #[hardwon("#427: unmatched implicit conversions must preserve conservative candidates")]
782    fn cpp_filter_candidates_keeps_all_when_no_candidate_matches() {
783        let candidates = vec![
784            function("format", "void format(std::string value)"),
785            function("format", "void format(int value)"),
786        ];
787        let filtered = cpp_filter_candidates_by_args(
788            candidates.clone(),
789            &[Some(CppArgType {
790                name: "double".to_string(),
791                unit: None,
792                indirection: 0,
793                pointee_const: false,
794            })],
795            &|_| None,
796            &|_, _| false,
797        );
798        assert_eq!(candidates, filtered);
799    }
800
801    /// Every `call_expression` in `source`, in source order.
802    fn call_expressions(tree: &tree_sitter::Tree) -> Vec<Node<'_>> {
803        let mut calls = Vec::new();
804        let mut stack = vec![tree.root_node()];
805        while let Some(node) = stack.pop() {
806            if node.kind() == "call_expression" {
807                calls.push(node);
808            }
809            let mut cursor = node.walk();
810            stack.extend(node.named_children(&mut cursor));
811        }
812        calls.sort_by_key(Node::start_byte);
813        calls
814    }
815
816    fn parse(source: &str) -> tree_sitter::Tree {
817        crate::diagnostics::parse_cpp_tree(source)
818    }
819
820    /// #2552 shape 4. `std::move` and `std::forward` forward one argument, and
821    /// its type is the call's type. Nothing else is that, including the same
822    /// names outside `std` and a call with the wrong argument count.
823    #[test]
824    #[hardwon("#2894: C++ call argument typing left forwarding/member-field calls ambiguous")]
825    fn a_forwarding_call_reports_the_one_argument_it_forwards() {
826        let source = r#"void f() {
827   sink(std::move(a));
828   sink(std::forward<T>(b));
829   sink(std::move(c, 1));
830   sink(std::swap(d, e));
831   sink(move(g));
832   sink(other::move(h));
833   sink(std::vector<int>(i));
834}
835"#;
836        let tree = parse(source);
837        let forwarded = call_expressions(&tree)
838            .into_iter()
839            .filter_map(|call| cpp_forwarding_call_argument(call, source))
840            .map(|argument| cpp_node_text(argument, source).to_string())
841            .collect::<Vec<_>>();
842        assert_eq!(forwarded, vec!["a".to_string(), "b".to_string()]);
843    }
844
845    fn value_arg(name: &str) -> Option<CppArgType> {
846        Some(CppArgType {
847            name: name.to_string(),
848            unit: None,
849            indirection: 0,
850            pointee_const: false,
851        })
852    }
853
854    /// #2894 gap 2. `DL_Group(ber, format)` with a `std::vector<uint8_t> ber`
855    /// matched neither two-parameter constructor, so the filter kept both.
856    /// A contiguous owning range now satisfies a span over the same element,
857    /// including a span that adds `const`.
858    #[test]
859    #[hardwon("#2894: owning contiguous ranges were excluded from span overloads")]
860    fn a_contiguous_range_satisfies_a_span_over_its_element() {
861        for arg in ["std::vector<uint8_t>", "std::array<uint8_t, 16>"] {
862            for param in ["std::span<const uint8_t>", "std::span<uint8_t>"] {
863                let candidates = vec![
864                    function("take", &format!("void take({param} bytes)")),
865                    function("take", "void take(int count)"),
866                ];
867                let filtered = cpp_filter_candidates_by_args(
868                    candidates,
869                    &[value_arg(arg)],
870                    &|_| None,
871                    &|_, _| false,
872                );
873                assert_eq!(filtered.len(), 1, "{arg} -> {param}");
874                assert!(
875                    filtered[0].signature().unwrap().contains(param),
876                    "{arg} -> {param}: {:?}",
877                    filtered[0].signature()
878                );
879            }
880        }
881    }
882
883    /// The near misses the issue asked for: a different element type, and an
884    /// element that would lose its `const`.
885    #[test]
886    #[hardwon("#2894: span conversion accepted incompatible element shapes")]
887    fn a_range_over_another_element_does_not_satisfy_a_span() {
888        for (arg, param) in [
889            ("std::vector<int>", "std::span<const uint8_t>"),
890            ("std::vector<const uint8_t>", "std::span<uint8_t>"),
891            ("std::vector<uint8_t>", "std::span<const uint8_t*>"),
892            ("std::deque<uint8_t>", "std::span<const uint8_t>"),
893        ] {
894            let candidates = vec![
895                function("take", &format!("void take({param} bytes)")),
896                function("take", "void take(int count)"),
897            ];
898            let filtered = cpp_filter_candidates_by_args(
899                candidates.clone(),
900                &[value_arg(arg)],
901                &|_| None,
902                &|_, _| false,
903            );
904            assert_eq!(
905                candidates, filtered,
906                "{arg} must not satisfy {param}, so every candidate stays"
907            );
908        }
909    }
910
911    #[test]
912    #[hardwon("#2894: string and character pointers did not match string_view overloads")]
913    fn an_owned_string_and_a_character_pointer_satisfy_a_string_view() {
914        let literal = Some(CppArgType {
915            name: "char".to_string(),
916            unit: None,
917            indirection: 1,
918            pointee_const: true,
919        });
920        for arg in [value_arg("std::string"), literal] {
921            let filtered = cpp_filter_candidates_by_args(
922                vec![
923                    function("label", "void label(std::string_view text)"),
924                    function("label", "void label(int count)"),
925                ],
926                std::slice::from_ref(&arg),
927                &|_| None,
928                &|_, _| false,
929            );
930            assert_eq!(filtered.len(), 1, "{:?}", arg.as_ref().map(|arg| &arg.name));
931            assert!(
932                filtered[0]
933                    .signature()
934                    .unwrap()
935                    .contains("std::string_view"),
936                "{:?}",
937                filtered[0].signature()
938            );
939        }
940    }
941
942    /// A standard conversion never outranks an exact match: the overload set
943    /// that has both keeps resolving to the exact one, as it did before the
944    /// conversion table existed.
945    #[test]
946    #[hardwon("#2894: a standard conversion outranked an exact overload")]
947    fn an_exact_match_outranks_a_standard_conversion() {
948        let candidates = vec![
949            function("own", "void own(std::vector<uint8_t> bytes)"),
950            function("own", "void own(std::span<const uint8_t> bytes)"),
951        ];
952        let filtered = cpp_filter_candidates_by_args(
953            candidates,
954            &[value_arg("std::vector<uint8_t>")],
955            &|_| None,
956            &|_, _| false,
957        );
958        assert_eq!(filtered.len(), 1);
959        assert_eq!(
960            "void own(std::vector<uint8_t> bytes)",
961            filtered[0].signature().unwrap()
962        );
963    }
964
965    #[test]
966    #[hardwon("02186273bdc4: unknown template deductions discarded compatible overloads")]
967    fn cpp_filter_candidates_keeps_templates_when_only_type_shape_is_unknown() {
968        let candidates = vec![
969            function("take", "void take(Vec256<float> value)"),
970            function("take", "<typename T>(Vec256<T>)"),
971        ];
972        let filtered = cpp_filter_candidates_by_args(
973            candidates.clone(),
974            &[Some(CppArgType {
975                name: "Vec256<int>".to_string(),
976                unit: Some(class("Vec256")),
977                indirection: 0,
978                pointee_const: false,
979            })],
980            &|_| None,
981            &|_, _| false,
982        );
983        assert_eq!(filtered, candidates);
984    }
985
986    #[test]
987    #[hardwon("#2909: an lvalue receiver retained an rvalue-qualified member")]
988    fn signature_ref_qualifier_comes_from_the_ast_normalized_suffix() {
989        assert_eq!(
990            cpp_signature_ref_qualifier("() const & noexcept"),
991            Some(CppRefQualifier::Lvalue)
992        );
993        assert_eq!(
994            cpp_signature_ref_qualifier("(int) volatile && noexcept"),
995            Some(CppRefQualifier::Rvalue)
996        );
997        assert_eq!(cpp_signature_ref_qualifier("() const noexcept"), None);
998        assert_eq!(
999            cpp_signature_ref_qualifier("() noexcept(left && right)"),
1000            None
1001        );
1002    }
1003}