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    if candidates.len() <= 1 || arg_types.iter().any(Option::is_none) {
135        return candidates;
136    }
137
138    let filtered: Vec<_> = candidates
139        .iter()
140        .filter(|candidate| {
141            cpp_signature_param_types(candidate.signature().unwrap_or_default()).is_some_and(
142                |params| {
143                    params.len() == arg_types.len()
144                        && params.iter().zip(arg_types.iter()).all(|(param, arg)| {
145                            cpp_param_matches_arg(param, arg, resolve_type, assignable)
146                        })
147                },
148            )
149        })
150        .cloned()
151        .collect();
152    if filtered.is_empty() {
153        candidates
154    } else {
155        filtered
156    }
157}
158
159fn cpp_param_matches_arg(
160    param: &str,
161    arg: &Option<CppArgType>,
162    resolve_type: &dyn Fn(&str) -> Option<CodeUnit>,
163    assignable: &dyn Fn(&CodeUnit, &CodeUnit) -> bool,
164) -> bool {
165    let Some(arg) = arg else {
166        return false;
167    };
168    if cpp_type_text_pointer_depth(param) != arg.indirection {
169        return false;
170    }
171    if arg.pointee_const && !cpp_type_text_pointee_is_const(param) {
172        return false;
173    }
174    let param_name = normalize_cpp_type_name(param);
175    match (resolve_type(&param_name), arg.unit.as_ref()) {
176        (Some(param_unit), Some(arg_unit)) => assignable(arg_unit, &param_unit),
177        _ => param_name == arg.name,
178    }
179}
180
181fn cpp_type_text_pointee_is_const(text: &str) -> bool {
182    let normalized = normalize_cpp_whitespace(text);
183    let base = cpp_type_text_base(&normalized).trim();
184    base.starts_with("const ") || base.ends_with(" const")
185}
186
187fn cpp_type_text_base(text: &str) -> &str {
188    text[..cpp_type_text_shape(text).0].trim()
189}
190
191pub fn cpp_split_top_level_commas(value: &str) -> impl Iterator<Item = &str> {
192    struct TopLevelCommaSplit<'a> {
193        value: &'a str,
194        start: usize,
195        angle: usize,
196        paren: usize,
197        brace: usize,
198        bracket: usize,
199    }
200
201    impl<'a> Iterator for TopLevelCommaSplit<'a> {
202        type Item = &'a str;
203
204        fn next(&mut self) -> Option<Self::Item> {
205            if self.start > self.value.len() {
206                return None;
207            }
208            for (offset, ch) in self.value[self.start..].char_indices() {
209                let absolute = self.start + offset;
210                match ch {
211                    '<' => self.angle += 1,
212                    '>' => self.angle = self.angle.saturating_sub(1),
213                    '(' => self.paren += 1,
214                    ')' => self.paren = self.paren.saturating_sub(1),
215                    '{' => self.brace += 1,
216                    '}' => self.brace = self.brace.saturating_sub(1),
217                    '[' => self.bracket += 1,
218                    ']' => self.bracket = self.bracket.saturating_sub(1),
219                    ',' if self.angle == 0
220                        && self.paren == 0
221                        && self.brace == 0
222                        && self.bracket == 0 =>
223                    {
224                        let item = self.value[self.start..absolute].trim();
225                        self.start = absolute + ch.len_utf8();
226                        return Some(item);
227                    }
228                    _ => {}
229                }
230            }
231            let item = self.value[self.start..].trim();
232            self.start = self.value.len() + 1;
233            Some(item)
234        }
235    }
236
237    TopLevelCommaSplit {
238        value,
239        start: 0,
240        angle: 0,
241        paren: 0,
242        brace: 0,
243        bracket: 0,
244    }
245    .filter(|item| !item.is_empty())
246}
247
248/// The byte offsets of a signature's outermost parameter-list parentheses.
249fn cpp_signature_parameter_span(signature: &str) -> Option<(usize, usize)> {
250    let open = signature.find('(')?;
251    let mut depth = 0i32;
252    for (offset, ch) in signature[open..].char_indices() {
253        match ch {
254            '(' => depth += 1,
255            ')' => {
256                depth -= 1;
257                if depth == 0 {
258                    return Some((open, open + offset));
259                }
260            }
261            _ => {}
262        }
263    }
264    None
265}
266
267fn cpp_signature_parameter_text(signature: &str) -> Option<&str> {
268    let (open, close) = cpp_signature_parameter_span(signature)?;
269    Some(signature[open + 1..close].trim())
270}
271
272/// What a signature carries after its parameter list: the trailing
273/// cv-/ref-qualifiers and `noexcept` that the signature identity records
274/// (#1827).
275///
276/// Two member declarations with the same parameter types but different
277/// trailing qualifiers are distinct declarations, so a caller deciding whether
278/// one declaration hides another has to compare this alongside the parameter
279/// types.
280pub fn cpp_signature_trailing_qualifiers(signature: &str) -> &str {
281    match cpp_signature_parameter_span(signature) {
282        Some((_, close)) => signature[close + 1..].trim(),
283        None => "",
284    }
285}
286
287fn cpp_parameter_name_token(token: &str) -> bool {
288    let token = token.trim_start_matches('*').trim_start_matches('&').trim();
289    token
290        .chars()
291        .next()
292        .is_some_and(|ch| ch == '_' || ch.is_ascii_lowercase())
293        && token
294            .chars()
295            .all(|ch| ch == '_' || ch.is_ascii_alphanumeric())
296}
297
298fn strip_tag_type_prefix(value: &str) -> &str {
299    let value = value.trim_start_matches("const ");
300    value
301        .strip_prefix("struct ")
302        .or_else(|| value.strip_prefix("class "))
303        .or_else(|| value.strip_prefix("enum "))
304        .unwrap_or(value)
305        .trim()
306}
307
308fn cpp_number_literal_is_float(text: &str) -> bool {
309    let text = text.trim();
310    text.contains('.') || text.contains('e') || text.contains('E') || text.ends_with(['f', 'F'])
311}
312
313#[cfg(test)]
314mod tests {
315    use super::*;
316    use brokk_bifrost_core::analyzer::ProjectFile;
317    use brokk_bifrost_core::analyzer::model::CodeUnitType;
318
319    fn test_file() -> ProjectFile {
320        ProjectFile::new(std::env::temp_dir(), "test.cpp")
321    }
322
323    fn function(name: &str, signature: &str) -> CodeUnit {
324        CodeUnit::with_signature(
325            test_file(),
326            CodeUnitType::Function,
327            "ns",
328            name,
329            Some(signature.to_string()),
330            false,
331        )
332    }
333
334    fn class(name: &str) -> CodeUnit {
335        CodeUnit::new(test_file(), CodeUnitType::Class, "ns", name)
336    }
337
338    #[test]
339    fn cpp_filter_candidates_matches_named_unindexed_types() {
340        let candidates = vec![
341            function("format", "std::string format(const std::string& value)"),
342            function("format", "std::string format(int value)"),
343        ];
344        let filtered = cpp_filter_candidates_by_args(
345            candidates,
346            &[Some(CppArgType {
347                name: "std::string".to_string(),
348                unit: None,
349                indirection: 0,
350                pointee_const: false,
351            })],
352            &|_| None,
353            &|_, _| false,
354        );
355        assert_eq!(1, filtered.len());
356        assert!(filtered[0].signature().unwrap().contains("std::string&"));
357    }
358
359    #[test]
360    fn cpp_filter_candidates_matches_assignable_units() {
361        let arg = class("Arg");
362        let param = class("Param");
363        let filtered = cpp_filter_candidates_by_args(
364            vec![function("take", "void take(Param value)")],
365            &[Some(CppArgType {
366                name: "Arg".to_string(),
367                unit: Some(arg.clone()),
368                indirection: 0,
369                pointee_const: false,
370            })],
371            &|name| (name == "Param").then(|| param.clone()),
372            &|from, to| from == &arg && to == &param,
373        );
374        assert_eq!(1, filtered.len());
375    }
376
377    #[test]
378    fn cpp_filter_candidates_rejects_pointer_depth_mismatch() {
379        let candidates = vec![
380            function("take", "void take(int* value)"),
381            function("take", "void take(int value)"),
382        ];
383        let filtered = cpp_filter_candidates_by_args(
384            candidates,
385            &[Some(CppArgType {
386                name: "int".to_string(),
387                unit: None,
388                indirection: 0,
389                pointee_const: false,
390            })],
391            &|_| None,
392            &|_, _| false,
393        );
394        assert_eq!(1, filtered.len());
395        assert_eq!("void take(int value)", filtered[0].signature().unwrap());
396    }
397
398    #[test]
399    fn cpp_filter_candidates_uses_const_string_literal_pointer_evidence() {
400        let literal = Some(CppArgType {
401            name: "char".to_string(),
402            unit: None,
403            indirection: 1,
404            pointee_const: true,
405        });
406        let direct = cpp_filter_candidates_by_args(
407            vec![
408                function("select", "int select(int value)"),
409                function("select", "int select(const char* value)"),
410            ],
411            std::slice::from_ref(&literal),
412            &|_| None,
413            &|_, _| false,
414        );
415        assert_eq!(1, direct.len());
416        assert_eq!(
417            "int select(const char* value)",
418            direct[0].signature().unwrap()
419        );
420
421        for candidates in [
422            vec![
423                function("select", "int select(int value)"),
424                function("select", "int select(char* value)"),
425            ],
426            vec![
427                function("format", "int format(int value)"),
428                function("format", "int format(std::string value)"),
429            ],
430        ] {
431            let filtered = cpp_filter_candidates_by_args(
432                candidates.clone(),
433                std::slice::from_ref(&literal),
434                &|_| None,
435                &|_, _| false,
436            );
437            assert_eq!(
438                candidates, filtered,
439                "unmodeled or invalid conversions must remain conservative"
440            );
441        }
442    }
443
444    #[test]
445    fn cpp_parameter_type_keeps_pointer_const_distinct_from_pointee_const() {
446        assert_eq!("char*", cpp_parameter_type_text("char * const value"));
447        assert_eq!(
448            "const char*",
449            cpp_parameter_type_text("const char * const value")
450        );
451        assert_eq!("char", normalize_cpp_type_name("char * const"));
452    }
453
454    #[test]
455    fn cpp_filter_candidates_keeps_all_for_unknown_arguments() {
456        let candidates = vec![
457            function("format", "void format(std::string value)"),
458            function("format", "void format(int value)"),
459        ];
460        let filtered =
461            cpp_filter_candidates_by_args(candidates.clone(), &[None], &|_| None, &|_, _| false);
462        assert_eq!(candidates, filtered);
463    }
464
465    #[test]
466    fn cpp_filter_candidates_keeps_all_when_no_candidate_matches() {
467        let candidates = vec![
468            function("format", "void format(std::string value)"),
469            function("format", "void format(int value)"),
470        ];
471        let filtered = cpp_filter_candidates_by_args(
472            candidates.clone(),
473            &[Some(CppArgType {
474                name: "double".to_string(),
475                unit: None,
476                indirection: 0,
477                pointee_const: false,
478            })],
479            &|_| None,
480            &|_, _| false,
481        );
482        assert_eq!(candidates, filtered);
483    }
484}