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
20pub fn cpp_signature_param_types(signature: &str) -> Option<Vec<String>> {
21    let inner = cpp_signature_parameter_text(signature)
22        .unwrap_or(signature)
23        .trim();
24    if inner.is_empty() || inner == "void" {
25        return Some(Vec::new());
26    }
27    Some(
28        cpp_split_top_level_commas(inner)
29            .map(cpp_parameter_type_text)
30            .collect(),
31    )
32}
33
34pub fn cpp_parameter_type_text(parameter: &str) -> String {
35    let mut text = parameter
36        .split_once('=')
37        .map(|(before, _)| before)
38        .unwrap_or(parameter)
39        .trim()
40        .trim_end_matches(';')
41        .trim();
42    let pointer_depth = cpp_type_text_pointer_depth(text);
43    if let Some((before, last)) = text.rsplit_once(char::is_whitespace)
44        && cpp_parameter_name_token(last)
45    {
46        text = before.trim();
47    }
48    let pointee_const = pointer_depth > 0 && cpp_type_text_pointee_is_const(text);
49    format!(
50        "{}{}{}",
51        if pointee_const { "const " } else { "" },
52        normalize_cpp_type_name(text),
53        "*".repeat(pointer_depth as usize)
54    )
55}
56
57pub fn normalize_cpp_type_name(text: &str) -> String {
58    let normalized = normalize_cpp_whitespace(text);
59    let base = cpp_type_text_base(&normalized)
60        .trim_start_matches("const ")
61        .trim();
62    strip_tag_type_prefix(base.strip_suffix(" const").unwrap_or(base)).to_string()
63}
64
65pub fn cpp_type_text_pointer_depth(text: &str) -> i32 {
66    cpp_type_text_shape(text).1
67}
68
69fn cpp_type_text_shape(text: &str) -> (usize, i32) {
70    let mut depth = 0i32;
71    let mut nesting = 0i32;
72    let mut base_end = text.len();
73    for (offset, ch) in text.char_indices() {
74        match ch {
75            '<' | '(' | '[' => nesting += 1,
76            '>' | ')' | ']' => nesting -= 1,
77            '*' if nesting <= 0 => {
78                base_end = base_end.min(offset);
79                depth += 1;
80            }
81            '&' if nesting <= 0 => base_end = base_end.min(offset),
82            _ => {}
83        }
84    }
85    (base_end, depth)
86}
87
88pub fn cpp_literal_arg_type(node: Node<'_>, source: &str) -> Option<CppArgType> {
89    let scalar = |name: &str| CppArgType {
90        name: name.to_string(),
91        unit: None,
92        indirection: 0,
93        pointee_const: false,
94    };
95    match node.kind() {
96        "number_literal" => {
97            let text = cpp_node_text(node, source);
98            if cpp_number_literal_is_float(text) {
99                Some(scalar("double"))
100            } else {
101                Some(scalar("int"))
102            }
103        }
104        "true" | "false" => Some(scalar("bool")),
105        "char_literal" => Some(scalar("char")),
106        "string_literal" => {
107            let text = cpp_node_text(node, source).trim_start();
108            (text.starts_with('"') || text.starts_with("R\"")).then(|| CppArgType {
109                name: "char".to_string(),
110                unit: None,
111                indirection: 1,
112                pointee_const: true,
113            })
114        }
115        "unary_expression" => {
116            let operator = node.child_by_field_name("operator")?;
117            let inner = node
118                .child_by_field_name("argument")
119                .or_else(|| node.named_child(0))?;
120            matches!(operator.kind(), "+" | "-")
121                .then(|| cpp_literal_arg_type(inner, source))
122                .flatten()
123        }
124        _ => None,
125    }
126}
127
128pub fn cpp_filter_candidates_by_args(
129    candidates: Vec<CodeUnit>,
130    arg_types: &[Option<CppArgType>],
131    resolve_type: &dyn Fn(&str) -> Option<CodeUnit>,
132    assignable: &dyn Fn(&CodeUnit, &CodeUnit) -> bool,
133) -> Vec<CodeUnit> {
134    cpp_filter_candidates_by_args_with_parameter_types(
135        candidates,
136        arg_types,
137        &|candidate| cpp_signature_param_types(candidate.signature().unwrap_or_default()),
138        resolve_type,
139        assignable,
140    )
141}
142
143pub fn cpp_filter_candidates_by_args_with_parameter_types(
144    candidates: Vec<CodeUnit>,
145    arg_types: &[Option<CppArgType>],
146    parameter_types: &dyn Fn(&CodeUnit) -> Option<Vec<String>>,
147    resolve_type: &dyn Fn(&str) -> Option<CodeUnit>,
148    assignable: &dyn Fn(&CodeUnit, &CodeUnit) -> bool,
149) -> Vec<CodeUnit> {
150    if candidates.len() <= 1 || arg_types.iter().any(Option::is_none) {
151        return candidates;
152    }
153
154    let filtered: Vec<_> = candidates
155        .iter()
156        .filter(|candidate| {
157            let template_candidate = cpp_signature_is_template_candidate(candidate);
158            parameter_types(candidate).is_some_and(|params| {
159                params.len() == arg_types.len()
160                    && params.iter().zip(arg_types.iter()).all(|(param, arg)| {
161                        cpp_param_matches_arg(
162                            param,
163                            arg,
164                            template_candidate,
165                            resolve_type,
166                            assignable,
167                        )
168                    })
169            })
170        })
171        .cloned()
172        .collect();
173    if filtered.is_empty() {
174        candidates
175    } else if filtered.iter().any(cpp_signature_is_template_candidate) {
176        // A matching function template keeps the entire arity-compatible
177        // overload set alive. The parameter metadata has no template
178        // substitution or constraint ordering, so it cannot prove that a
179        // concrete sibling wins over a macro-constrained template (#2203).
180        candidates
181    } else {
182        filtered
183    }
184}
185
186fn cpp_signature_is_template_candidate(candidate: &CodeUnit) -> bool {
187    candidate
188        .signature()
189        .is_some_and(|signature| signature.trim_start().starts_with('<'))
190}
191
192fn cpp_param_matches_arg(
193    param: &str,
194    arg: &Option<CppArgType>,
195    template_candidate: bool,
196    resolve_type: &dyn Fn(&str) -> Option<CodeUnit>,
197    assignable: &dyn Fn(&CodeUnit, &CodeUnit) -> bool,
198) -> bool {
199    let Some(arg) = arg else {
200        return false;
201    };
202    if cpp_type_text_pointer_depth(param) != arg.indirection {
203        return false;
204    }
205    if arg.pointee_const && !cpp_type_text_pointee_is_const(param) {
206        return false;
207    }
208    // Parameter metadata records the declared spelling, but it does not carry
209    // template substitution or constraint semantics. Once pointer shape and
210    // constness agree, a function template remains a live overload candidate:
211    // a type-only comparison such as `Vec256<T>` versus `Vec256<int>` cannot
212    // prove that deduction or a macro-shaped constraint fails (#2203).
213    if template_candidate {
214        return true;
215    }
216    let param_name = normalize_cpp_type_name(param);
217    match (resolve_type(&param_name), arg.unit.as_ref()) {
218        (Some(param_unit), Some(arg_unit)) => assignable(arg_unit, &param_unit),
219        _ => param_name == arg.name,
220    }
221}
222
223fn cpp_type_text_pointee_is_const(text: &str) -> bool {
224    let normalized = normalize_cpp_whitespace(text);
225    let base = cpp_type_text_base(&normalized).trim();
226    base.starts_with("const ") || base.ends_with(" const")
227}
228
229fn cpp_type_text_base(text: &str) -> &str {
230    text[..cpp_type_text_shape(text).0].trim()
231}
232
233pub fn cpp_split_top_level_commas(value: &str) -> impl Iterator<Item = &str> {
234    struct TopLevelCommaSplit<'a> {
235        value: &'a str,
236        start: usize,
237        angle: usize,
238        paren: usize,
239        brace: usize,
240        bracket: usize,
241    }
242
243    impl<'a> Iterator for TopLevelCommaSplit<'a> {
244        type Item = &'a str;
245
246        fn next(&mut self) -> Option<Self::Item> {
247            if self.start > self.value.len() {
248                return None;
249            }
250            for (offset, ch) in self.value[self.start..].char_indices() {
251                let absolute = self.start + offset;
252                match ch {
253                    '<' => self.angle += 1,
254                    '>' => self.angle = self.angle.saturating_sub(1),
255                    '(' => self.paren += 1,
256                    ')' => self.paren = self.paren.saturating_sub(1),
257                    '{' => self.brace += 1,
258                    '}' => self.brace = self.brace.saturating_sub(1),
259                    '[' => self.bracket += 1,
260                    ']' => self.bracket = self.bracket.saturating_sub(1),
261                    ',' if self.angle == 0
262                        && self.paren == 0
263                        && self.brace == 0
264                        && self.bracket == 0 =>
265                    {
266                        let item = self.value[self.start..absolute].trim();
267                        self.start = absolute + ch.len_utf8();
268                        return Some(item);
269                    }
270                    _ => {}
271                }
272            }
273            let item = self.value[self.start..].trim();
274            self.start = self.value.len() + 1;
275            Some(item)
276        }
277    }
278
279    TopLevelCommaSplit {
280        value,
281        start: 0,
282        angle: 0,
283        paren: 0,
284        brace: 0,
285        bracket: 0,
286    }
287    .filter(|item| !item.is_empty())
288}
289
290/// The byte offsets of a signature's outermost parameter-list parentheses.
291fn cpp_signature_parameter_span(signature: &str) -> Option<(usize, usize)> {
292    let open = signature.find('(')?;
293    let mut depth = 0i32;
294    for (offset, ch) in signature[open..].char_indices() {
295        match ch {
296            '(' => depth += 1,
297            ')' => {
298                depth -= 1;
299                if depth == 0 {
300                    return Some((open, open + offset));
301                }
302            }
303            _ => {}
304        }
305    }
306    None
307}
308
309fn cpp_signature_parameter_text(signature: &str) -> Option<&str> {
310    let (open, close) = cpp_signature_parameter_span(signature)?;
311    Some(signature[open + 1..close].trim())
312}
313
314/// What a signature carries after its parameter list: the trailing
315/// cv-/ref-qualifiers and `noexcept` that the signature identity records
316/// (#1827).
317///
318/// Two member declarations with the same parameter types but different
319/// trailing qualifiers are distinct declarations, so a caller deciding whether
320/// one declaration hides another has to compare this alongside the parameter
321/// types.
322pub fn cpp_signature_trailing_qualifiers(signature: &str) -> &str {
323    match cpp_signature_parameter_span(signature) {
324        Some((_, close)) => signature[close + 1..].trim(),
325        None => "",
326    }
327}
328
329fn cpp_parameter_name_token(token: &str) -> bool {
330    let token = token.trim_start_matches('*').trim_start_matches('&').trim();
331    token
332        .chars()
333        .next()
334        .is_some_and(|ch| ch == '_' || ch.is_ascii_lowercase())
335        && token
336            .chars()
337            .all(|ch| ch == '_' || ch.is_ascii_alphanumeric())
338}
339
340fn strip_tag_type_prefix(value: &str) -> &str {
341    let value = value.trim_start_matches("const ");
342    value
343        .strip_prefix("struct ")
344        .or_else(|| value.strip_prefix("class "))
345        .or_else(|| value.strip_prefix("enum "))
346        .unwrap_or(value)
347        .trim()
348}
349
350fn cpp_number_literal_is_float(text: &str) -> bool {
351    let text = text.trim();
352    text.contains('.') || text.contains('e') || text.contains('E') || text.ends_with(['f', 'F'])
353}
354
355#[cfg(test)]
356mod tests {
357    use super::*;
358    use brokk_bifrost_core::analyzer::ProjectFile;
359    use brokk_bifrost_core::analyzer::model::CodeUnitType;
360
361    fn test_file() -> ProjectFile {
362        ProjectFile::new(std::env::temp_dir(), "test.cpp")
363    }
364
365    fn function(name: &str, signature: &str) -> CodeUnit {
366        CodeUnit::with_signature(
367            test_file(),
368            CodeUnitType::Function,
369            "ns",
370            name,
371            Some(signature.to_string()),
372            false,
373        )
374    }
375
376    fn class(name: &str) -> CodeUnit {
377        CodeUnit::new(test_file(), CodeUnitType::Class, "ns", name)
378    }
379
380    #[test]
381    fn cpp_filter_candidates_matches_named_unindexed_types() {
382        let candidates = vec![
383            function("format", "std::string format(const std::string& value)"),
384            function("format", "std::string format(int value)"),
385        ];
386        let filtered = cpp_filter_candidates_by_args(
387            candidates,
388            &[Some(CppArgType {
389                name: "std::string".to_string(),
390                unit: None,
391                indirection: 0,
392                pointee_const: false,
393            })],
394            &|_| None,
395            &|_, _| false,
396        );
397        assert_eq!(1, filtered.len());
398        assert!(filtered[0].signature().unwrap().contains("std::string&"));
399    }
400
401    #[test]
402    fn cpp_filter_candidates_matches_assignable_units() {
403        let arg = class("Arg");
404        let param = class("Param");
405        let filtered = cpp_filter_candidates_by_args(
406            vec![function("take", "void take(Param value)")],
407            &[Some(CppArgType {
408                name: "Arg".to_string(),
409                unit: Some(arg.clone()),
410                indirection: 0,
411                pointee_const: false,
412            })],
413            &|name| (name == "Param").then(|| param.clone()),
414            &|from, to| from == &arg && to == &param,
415        );
416        assert_eq!(1, filtered.len());
417    }
418
419    #[test]
420    fn cpp_filter_candidates_rejects_pointer_depth_mismatch() {
421        let candidates = vec![
422            function("take", "void take(int* value)"),
423            function("take", "void take(int value)"),
424        ];
425        let filtered = cpp_filter_candidates_by_args(
426            candidates,
427            &[Some(CppArgType {
428                name: "int".to_string(),
429                unit: None,
430                indirection: 0,
431                pointee_const: false,
432            })],
433            &|_| None,
434            &|_, _| false,
435        );
436        assert_eq!(1, filtered.len());
437        assert_eq!("void take(int value)", filtered[0].signature().unwrap());
438    }
439
440    #[test]
441    fn cpp_filter_candidates_uses_const_string_literal_pointer_evidence() {
442        let literal = Some(CppArgType {
443            name: "char".to_string(),
444            unit: None,
445            indirection: 1,
446            pointee_const: true,
447        });
448        let direct = cpp_filter_candidates_by_args(
449            vec![
450                function("select", "int select(int value)"),
451                function("select", "int select(const char* value)"),
452            ],
453            std::slice::from_ref(&literal),
454            &|_| None,
455            &|_, _| false,
456        );
457        assert_eq!(1, direct.len());
458        assert_eq!(
459            "int select(const char* value)",
460            direct[0].signature().unwrap()
461        );
462
463        for candidates in [
464            vec![
465                function("select", "int select(int value)"),
466                function("select", "int select(char* value)"),
467            ],
468            vec![
469                function("format", "int format(int value)"),
470                function("format", "int format(std::string value)"),
471            ],
472        ] {
473            let filtered = cpp_filter_candidates_by_args(
474                candidates.clone(),
475                std::slice::from_ref(&literal),
476                &|_| None,
477                &|_, _| false,
478            );
479            assert_eq!(
480                candidates, filtered,
481                "unmodeled or invalid conversions must remain conservative"
482            );
483        }
484    }
485
486    #[test]
487    fn cpp_parameter_type_keeps_pointer_const_distinct_from_pointee_const() {
488        assert_eq!("char*", cpp_parameter_type_text("char * const value"));
489        assert_eq!(
490            "const char*",
491            cpp_parameter_type_text("const char * const value")
492        );
493        assert_eq!("char", normalize_cpp_type_name("char * const"));
494    }
495
496    #[test]
497    fn cpp_filter_candidates_keeps_all_for_unknown_arguments() {
498        let candidates = vec![
499            function("format", "void format(std::string value)"),
500            function("format", "void format(int value)"),
501        ];
502        let filtered =
503            cpp_filter_candidates_by_args(candidates.clone(), &[None], &|_| None, &|_, _| false);
504        assert_eq!(candidates, filtered);
505    }
506
507    #[test]
508    fn cpp_filter_candidates_keeps_all_when_no_candidate_matches() {
509        let candidates = vec![
510            function("format", "void format(std::string value)"),
511            function("format", "void format(int value)"),
512        ];
513        let filtered = cpp_filter_candidates_by_args(
514            candidates.clone(),
515            &[Some(CppArgType {
516                name: "double".to_string(),
517                unit: None,
518                indirection: 0,
519                pointee_const: false,
520            })],
521            &|_| None,
522            &|_, _| false,
523        );
524        assert_eq!(candidates, filtered);
525    }
526
527    #[test]
528    fn cpp_filter_candidates_keeps_templates_when_only_type_shape_is_unknown() {
529        let candidates = vec![
530            function("take", "void take(Vec256<float> value)"),
531            function("take", "<typename T>(Vec256<T>)"),
532        ];
533        let filtered = cpp_filter_candidates_by_args(
534            candidates.clone(),
535            &[Some(CppArgType {
536                name: "Vec256<int>".to_string(),
537                unit: Some(class("Vec256")),
538                indirection: 0,
539                pointee_const: false,
540            })],
541            &|_| None,
542            &|_, _| false,
543        );
544        assert_eq!(filtered, candidates);
545    }
546}