Skip to main content

brokk_bifrost_cpp/
structural.rs

1//! C++ structural spec for `query_code`.
2
3use brokk_bifrost_core::analyzer::Language;
4use brokk_bifrost_core::analyzer::structural::adapter_helpers::{
5    attach_positional_argument_roles, attach_role_with_derived_name, attach_terminal_callee,
6    first_named_child,
7};
8use brokk_bifrost_core::analyzer::structural::callable::{
9    CallKind, CallShapeCoverage, CallSiteContext, CallSiteFacts,
10};
11use brokk_bifrost_core::analyzer::structural::edges::{
12    INVERSE_REFERENCE_EDGE_SUPPORT, ReferenceEdgeSupport,
13};
14use brokk_bifrost_core::analyzer::structural::facts::Span;
15use brokk_bifrost_core::analyzer::structural::kinds::{NormalizedKind, Role};
16use brokk_bifrost_core::analyzer::structural::materialization::{
17    CPP_MATERIALIZATION_SUPPORT, DeclarationMaterializationSupport,
18};
19use brokk_bifrost_core::analyzer::structural::occurrences::{
20    OccurrenceRole, OccurrenceRoleSupport,
21};
22use brokk_bifrost_core::analyzer::structural::resolution::{
23    CALLABLE_APPLICABILITY_ONLY_SUPPORT, LexicalEnvironmentSupport,
24};
25use brokk_bifrost_core::analyzer::structural::routes::{
26    IdentityAxis, IdentityRouteSupport, RouteHopKind,
27};
28use brokk_bifrost_core::analyzer::structural::spec::{RoleSink, StructuralSpec};
29use brokk_bifrost_core::hash::HashSet;
30use tree_sitter::Node;
31
32#[derive(Debug, Default)]
33pub struct CppStructuralSpec;
34
35pub static CPP_STRUCTURAL_SPEC: CppStructuralSpec = CppStructuralSpec;
36
37pub const CPP_KIND_TABLE: &[(&str, NormalizedKind)] = &[
38    ("call_expression", NormalizedKind::Call),
39    ("new_expression", NormalizedKind::Call),
40    ("field_expression", NormalizedKind::FieldAccess),
41    ("function_definition", NormalizedKind::Function),
42    ("lambda_expression", NormalizedKind::Lambda),
43    ("class_specifier", NormalizedKind::Class),
44    ("struct_specifier", NormalizedKind::Class),
45    ("union_specifier", NormalizedKind::Class),
46    ("alias_declaration", NormalizedKind::Declaration),
47    ("assignment_expression", NormalizedKind::Assignment),
48    ("init_declarator", NormalizedKind::Assignment),
49    ("preproc_include", NormalizedKind::Import),
50    ("identifier", NormalizedKind::Identifier),
51    ("field_identifier", NormalizedKind::Identifier),
52    ("namespace_identifier", NormalizedKind::Identifier),
53    ("qualified_identifier", NormalizedKind::Identifier),
54    ("type_identifier", NormalizedKind::Identifier),
55    ("template_function", NormalizedKind::Identifier),
56    ("template_method", NormalizedKind::Identifier),
57    ("template_type", NormalizedKind::Identifier),
58    ("dependent_name", NormalizedKind::Identifier),
59    ("destructor_name", NormalizedKind::Identifier),
60    ("operator_name", NormalizedKind::Identifier),
61    ("primitive_type", NormalizedKind::Identifier),
62    ("char_literal", NormalizedKind::StringLiteral),
63    ("string_literal", NormalizedKind::StringLiteral),
64    ("raw_string_literal", NormalizedKind::StringLiteral),
65    ("number_literal", NormalizedKind::NumericLiteral),
66    ("true", NormalizedKind::BooleanLiteral),
67    ("false", NormalizedKind::BooleanLiteral),
68    ("null", NormalizedKind::NullLiteral),
69    ("return_statement", NormalizedKind::Return),
70    ("throw_statement", NormalizedKind::Throw),
71    ("catch_clause", NormalizedKind::Catch),
72    ("if_statement", NormalizedKind::If),
73    ("for_statement", NormalizedKind::Loop),
74    ("while_statement", NormalizedKind::WhileLoop),
75    ("do_statement", NormalizedKind::WhileLoop),
76];
77
78/// Whether tree-sitter recovered `.field = value` as a declaration child pair:
79/// an `ERROR` containing only the anonymous dot immediately followed by an
80/// `init_declarator` whose declarator is a real identifier.
81///
82/// This is intentionally narrower than rejecting declarators below `ERROR` in
83/// general: macro-decorated declarations and ordinary comma-separated initialized
84/// declarations also use recovery nodes and remain real declarations.
85pub fn is_recovered_designator_init_declarator(node: Node<'_>) -> bool {
86    if node.kind() != "init_declarator" {
87        return false;
88    }
89    let Some(identifier) = node.child_by_field_name("declarator") else {
90        return false;
91    };
92    if identifier.kind() != "identifier" || identifier.is_missing() {
93        return false;
94    }
95    let Some(previous) = node.prev_named_sibling() else {
96        return false;
97    };
98    if previous.kind() != "ERROR"
99        || previous.named_child_count() != 0
100        || previous.child_count() != 1
101        || previous.end_byte() != node.start_byte()
102    {
103        return false;
104    }
105    previous.child(0).is_some_and(|child| {
106        child.kind() == "."
107            && !child.is_named()
108            && child.start_byte() == previous.start_byte()
109            && child.end_byte() == previous.end_byte()
110    })
111}
112
113fn last_named_field_child<'tree>(node: Node<'tree>, field: &str) -> Option<Node<'tree>> {
114    let mut cursor = node.walk();
115    node.children_by_field_name(field, &mut cursor)
116        .filter(|child| child.is_named())
117        .last()
118}
119
120fn declarator_name_node<'tree>(declarator: Node<'tree>) -> Option<Node<'tree>> {
121    let mut current = declarator;
122    loop {
123        match current.kind() {
124            "identifier"
125            | "field_identifier"
126            | "namespace_identifier"
127            | "type_identifier"
128            | "destructor_name"
129            | "operator_name"
130            | "primitive_type" => return Some(current),
131            "qualified_identifier" => current = last_named_field_child(current, "name")?,
132            "dependent_name" | "template_function" | "template_method" | "template_type" => {
133                current = current.child_by_field_name("name")?;
134            }
135            "function_declarator"
136            | "pointer_declarator"
137            | "array_declarator"
138            | "init_declarator" => current = current.child_by_field_name("declarator")?,
139            "reference_declarator" | "parenthesized_declarator" => {
140                current = first_named_child(current)?;
141            }
142            _ => return None,
143        }
144    }
145}
146
147fn expression_name_node<'tree>(expression: Node<'tree>) -> Option<Node<'tree>> {
148    let mut current = expression;
149    loop {
150        match current.kind() {
151            "identifier"
152            | "field_identifier"
153            | "namespace_identifier"
154            | "type_identifier"
155            | "destructor_name"
156            | "operator_name"
157            | "primitive_type"
158            | "this" => return Some(current),
159            "qualified_identifier" => current = last_named_field_child(current, "name")?,
160            "dependent_name" | "template_function" | "template_method" | "template_type" => {
161                current = current.child_by_field_name("name")?;
162            }
163            "field_expression" => current = current.child_by_field_name("field")?,
164            "call_expression" => current = current.child_by_field_name("function")?,
165            "new_expression" => current = current.child_by_field_name("type")?,
166            "parenthesized_expression" => current = first_named_child(current)?,
167            _ => return declarator_name_node(current),
168        }
169    }
170}
171
172fn attach_qualified_scope_receiver(sink: &mut RoleSink<'_>, function: Node<'_>) {
173    if function.kind() != "qualified_identifier" {
174        return;
175    }
176    if let Some(scope) = function.child_by_field_name("scope") {
177        attach_role_with_derived_name(sink, Role::Receiver, scope, expression_name_node);
178    }
179}
180
181fn qualified_declarator_node(mut node: Node<'_>) -> Option<Node<'_>> {
182    loop {
183        if node.kind() == "qualified_identifier" {
184            return Some(node);
185        }
186        node = node
187            .child_by_field_name("declarator")
188            .or_else(|| node.child_by_field_name("name"))
189            .or_else(|| first_named_child(node))?;
190    }
191}
192
193fn node_text<'source>(node: Node<'_>, source: &'source str) -> Option<&'source str> {
194    node.utf8_text(source.as_bytes()).ok()
195}
196
197fn scoped_function_definition(node: Node<'_>) -> Option<Node<'_>> {
198    node.child_by_field_name("declarator")
199        .and_then(qualified_declarator_node)
200        .and_then(|qualified| qualified.child_by_field_name("scope"))
201}
202
203fn is_constructor_definition(node: Node<'_>, source: &str) -> bool {
204    node.child_by_field_name("declarator")
205        .and_then(qualified_declarator_node)
206        .and_then(|qualified| {
207            Some((
208                expression_name_node(qualified.child_by_field_name("scope")?)?,
209                expression_name_node(last_named_field_child(qualified, "name")?)?,
210            ))
211        })
212        .is_some_and(|(scope, name)| node_text(scope, source) == node_text(name, source))
213}
214
215fn unquoted_include_span(node: Node<'_>) -> Option<Span> {
216    if !matches!(node.kind(), "string_literal" | "system_lib_string") {
217        return None;
218    }
219    let start = node.start_byte().checked_add(1)?;
220    let end = node.end_byte().checked_sub(1)?;
221    (start <= end).then_some(Span {
222        start_byte: start,
223        end_byte: end,
224    })
225}
226
227/// The names of every function-like macro this translation unit defines, read
228/// from `preproc_function_def` name fields.
229///
230/// The walk is iterative and descends only through preprocessor conditional
231/// blocks, which is where a definition can nest. A `#define` written inside a
232/// function body is not collected; that is a known and stated boundary, not an
233/// approximation of one.
234fn function_like_macro_names(root: Node<'_>, source: &str) -> HashSet<String> {
235    let mut names = HashSet::default();
236    let mut stack = vec![root];
237    while let Some(node) = stack.pop() {
238        for index in 0..node.named_child_count() {
239            let Some(child) = node.named_child(index) else {
240                continue;
241            };
242            match child.kind() {
243                "preproc_function_def" => {
244                    if let Some(name) = child.child_by_field_name("name") {
245                        names.insert(source[name.start_byte()..name.end_byte()].to_owned());
246                    }
247                }
248                "preproc_if" | "preproc_ifdef" | "preproc_else" | "preproc_elif"
249                | "preproc_elifdef" => stack.push(child),
250                _ => {}
251            }
252        }
253    }
254    names
255}
256
257fn cpp_member_terminal_name<'tree>(field: Node<'tree>) -> Option<Node<'tree>> {
258    let field = if field.kind() == "dependent_name" {
259        first_named_child(field)?
260    } else {
261        field
262    };
263    expression_name_node(field)
264}
265
266/// Return the one identifier-bearing node that spells a member in a C++
267/// `field_expression`.
268///
269/// The grammar permits the `field` child to be a qualified name, a dependent
270/// name, a template method, or a destructor name. `expression_name_node`
271/// already follows those wrappers to their terminal name. Walking upward from
272/// the candidate and comparing that terminal node keeps receiver identifiers,
273/// declaration names, field-designator labels, and path prefixes out of the
274/// member role without looking at source text.
275fn cpp_member_position(node: Node<'_>) -> Option<OccurrenceRole> {
276    if !matches!(
277        node.kind(),
278        "identifier" | "field_identifier" | "type_identifier" | "operator_name" | "destructor_name"
279    ) {
280        return None;
281    }
282
283    let mut current = node;
284    while let Some(parent) = current.parent() {
285        if parent.kind() == "field_expression" {
286            let field = parent.child_by_field_name("field")?;
287            return cpp_member_terminal_name(field)
288                .filter(|name| name.id() == node.id())
289                .map(|_| OccurrenceRole::MemberPosition);
290        }
291
292        // These are the only named wrappers that can occur between the
293        // terminal field name and its owning field expression.
294        if matches!(
295            parent.kind(),
296            "qualified_identifier" | "dependent_name" | "template_method" | "destructor_name"
297        ) {
298            current = parent;
299        } else {
300            return None;
301        }
302    }
303    None
304}
305
306impl StructuralSpec for CppStructuralSpec {
307    fn language(&self) -> Language {
308        Language::Cpp
309    }
310
311    fn supports_boolean_literal_value(&self) -> bool {
312        true
313    }
314
315    fn kind_table(&self) -> &'static [(&'static str, NormalizedKind)] {
316        CPP_KIND_TABLE
317    }
318
319    fn refine_kind(
320        &self,
321        node: Node<'_>,
322        kind: NormalizedKind,
323        enclosing: Option<NormalizedKind>,
324        source: &str,
325        _context: &CallSiteContext,
326    ) -> NormalizedKind {
327        if kind == NormalizedKind::Function
328            && (enclosing == Some(NormalizedKind::Class)
329                || scoped_function_definition(node).is_some())
330        {
331            if is_constructor_definition(node, source) {
332                NormalizedKind::Constructor
333            } else {
334                NormalizedKind::Method
335            }
336        } else {
337            kind
338        }
339    }
340
341    fn supports_kind(&self, kind: NormalizedKind) -> bool {
342        matches!(kind, NormalizedKind::Method | NormalizedKind::Constructor)
343            || self
344                .kind_table()
345                .iter()
346                .any(|(_, fact_kind)| fact_kind.satisfies(kind))
347    }
348
349    fn supports_role(&self, role: Role) -> bool {
350        !matches!(
351            role,
352            Role::Kwarg | Role::Decorator | Role::Iterable | Role::Element
353        )
354    }
355
356    fn call_site_context(&self, root: Node<'_>, source: &str) -> CallSiteContext {
357        CallSiteContext::with_macro_derived_callees(function_like_macro_names(root, source))
358    }
359
360    /// A `new_expression` is a constructor call, and a call whose callee names
361    /// a function-like macro of this translation unit has an argument list
362    /// that belongs to the macro, not to the callable that finally runs
363    /// (#1478). `CALL_TWICE(2)` parses as an ordinary call of two source
364    /// arguments even when the expansion calls a three-parameter callable, so
365    /// the honest answer is unknown coverage and no argument rows at all —
366    /// never a fabricated list an exact-arity assertion could read as clean.
367    ///
368    /// The boundary is deliberate: only macros defined in this file are
369    /// known here, because that is what this file's parse tree contains. A
370    /// macro defined in an included header leaves the site exact, which is
371    /// the same answer the analyzer gives today.
372    fn call_site_facts(
373        &self,
374        node: Node<'_>,
375        source: &str,
376        context: &CallSiteContext,
377    ) -> Option<CallSiteFacts> {
378        if node.kind() == "new_expression" {
379            return Some(CallSiteFacts::of_kind(CallKind::Constructor));
380        }
381        let callee = node.child_by_field_name("function")?;
382        (callee.kind() == "identifier"
383            && context.is_macro_derived_callee(&source[callee.start_byte()..callee.end_byte()]))
384        .then(|| CallSiteFacts::of_coverage(CallShapeCoverage::UnknownMacroDerived))
385    }
386
387    fn occurrence_role_support(&self) -> &OccurrenceRoleSupport {
388        static SUPPORT: OccurrenceRoleSupport =
389            OccurrenceRoleSupport::NONE.supported(OccurrenceRole::MemberPosition);
390        &SUPPORT
391    }
392
393    fn lexical_environment_support(&self) -> &LexicalEnvironmentSupport {
394        // C and C++ classify no scopes, binding intervals, import binders or
395        // package clause, but the call seams report per-candidate callable
396        // applicability (#1478 M3). The per-axis table states exactly that.
397        &CALLABLE_APPLICABILITY_ONLY_SUPPORT
398    }
399
400    fn materialization_support(&self) -> &DeclarationMaterializationSupport {
401        &CPP_MATERIALIZATION_SUPPORT
402    }
403
404    fn reference_edge_support(&self) -> &ReferenceEdgeSupport {
405        &INVERSE_REFERENCE_EDGE_SUPPORT
406    }
407
408    fn identity_route_support(&self) -> &IdentityRouteSupport {
409        // C++'s occurrence adapter is shallow, so it claims no path axes. Its
410        // declaration layer keeps prototype/body occurrences distinct, which
411        // the physical-grouping axis carries; the declaration-definition peer
412        // *relation* stays unclaimed until a producer emits typed peer rows
413        // rather than merged ranges (see the #1475 ExecPlan Decision Log, M6,
414        // and its follow-up issue).
415        static SUPPORT: IdentityRouteSupport = IdentityRouteSupport::NONE
416            .supported_axis(IdentityAxis::CanonicalIdentity)
417            .supported_axis(IdentityAxis::PhysicalGrouping)
418            .supported_relation(RouteHopKind::NestedOwner);
419        &SUPPORT
420    }
421
422    fn extract(&self, node: Node<'_>, kind: NormalizedKind, sink: &mut RoleSink<'_>) {
423        if let Some(role) = cpp_member_position(node) {
424            sink.occurrence_role(node, role);
425        }
426
427        match kind {
428            NormalizedKind::Call => {
429                let function_field = if node.kind() == "new_expression" {
430                    "type"
431                } else {
432                    "function"
433                };
434                if let Some(function) = node.child_by_field_name(function_field) {
435                    attach_terminal_callee(sink, function, expression_name_node(function));
436                    if function.kind() == "field_expression"
437                        && let Some(argument) = function.child_by_field_name("argument")
438                    {
439                        attach_role_with_derived_name(
440                            sink,
441                            Role::Receiver,
442                            argument,
443                            expression_name_node,
444                        );
445                    }
446                    attach_qualified_scope_receiver(sink, function);
447                }
448                if let Some(arguments) = node.child_by_field_name("arguments") {
449                    attach_positional_argument_roles(sink, arguments, expression_name_node);
450                }
451            }
452            NormalizedKind::FieldAccess => {
453                if let Some(field) = node.child_by_field_name("field") {
454                    attach_role_with_derived_name(sink, Role::Field, field, expression_name_node);
455                    if let Some(name) = expression_name_node(field) {
456                        sink.set_name(name);
457                    }
458                }
459                if let Some(argument) = node.child_by_field_name("argument") {
460                    attach_role_with_derived_name(
461                        sink,
462                        Role::Object,
463                        argument,
464                        expression_name_node,
465                    );
466                }
467            }
468            NormalizedKind::Function | NormalizedKind::Method | NormalizedKind::Constructor => {
469                if let Some(name) = node
470                    .child_by_field_name("declarator")
471                    .and_then(declarator_name_node)
472                {
473                    sink.set_name(name);
474                }
475            }
476            NormalizedKind::Class | NormalizedKind::Declaration => {
477                if let Some(name) = node
478                    .child_by_field_name("name")
479                    .and_then(declarator_name_node)
480                    .or_else(|| node.child_by_field_name("name"))
481                {
482                    sink.set_name(name);
483                }
484            }
485            NormalizedKind::Assignment => match node.kind() {
486                "init_declarator" => {
487                    if let Some(declarator) = node.child_by_field_name("declarator") {
488                        attach_role_with_derived_name(
489                            sink,
490                            Role::Left,
491                            declarator,
492                            declarator_name_node,
493                        );
494                        if let Some(name) = declarator_name_node(declarator) {
495                            sink.set_name(name);
496                        }
497                    }
498                    if let Some(value) = node.child_by_field_name("value") {
499                        attach_role_with_derived_name(
500                            sink,
501                            Role::Right,
502                            value,
503                            expression_name_node,
504                        );
505                    }
506                }
507                "assignment_expression" => {
508                    if let Some(left) = node.child_by_field_name("left") {
509                        attach_role_with_derived_name(sink, Role::Left, left, expression_name_node);
510                    }
511                    if let Some(right) = node.child_by_field_name("right") {
512                        attach_role_with_derived_name(
513                            sink,
514                            Role::Right,
515                            right,
516                            expression_name_node,
517                        );
518                    }
519                }
520                _ => {}
521            },
522            NormalizedKind::Import => {
523                if let Some(path) = node.child_by_field_name("path") {
524                    if let Some(name) = unquoted_include_span(path) {
525                        sink.role_named_span(Role::Module, path, name);
526                    } else {
527                        attach_role_with_derived_name(
528                            sink,
529                            Role::Module,
530                            path,
531                            expression_name_node,
532                        );
533                    }
534                }
535            }
536            NormalizedKind::Identifier => match expression_name_node(node) {
537                Some(name) => sink.set_name(name),
538                None => sink.set_name(node),
539            },
540            _ => {}
541        }
542    }
543}
544
545#[cfg(test)]
546mod tests {
547    use super::cpp_member_position;
548    use brokk_bifrost_core::analyzer::structural::occurrences::OccurrenceRole;
549    use brokk_bifrost_core::analyzer::structural::spec::StructuralSpec;
550    use tree_sitter::Parser;
551
552    fn member_occurrences(source: &str) -> Vec<(usize, &str, OccurrenceRole)> {
553        let mut parser = Parser::new();
554        parser
555            .set_language(&tree_sitter_cpp::LANGUAGE.into())
556            .expect("C++ grammar");
557        let tree = parser.parse(source, None).expect("C++ parse");
558        assert!(
559            !tree.root_node().has_error(),
560            "{}",
561            tree.root_node().to_sexp()
562        );
563        let mut found = Vec::new();
564        let mut pending = vec![tree.root_node()];
565        while let Some(node) = pending.pop() {
566            if let Some(role) = cpp_member_position(node) {
567                found.push((
568                    node.start_byte(),
569                    &source[node.start_byte()..node.end_byte()],
570                    role,
571                ));
572            }
573            for index in (0..node.named_child_count()).rev() {
574                if let Some(child) = node.named_child(index) {
575                    pending.push(child);
576                }
577            }
578        }
579        found
580    }
581
582    #[test]
583    fn cpp_member_position_is_limited_to_member_access_and_calls() {
584        let source = concat!(
585            "struct Widget {\n",
586            "    int value;\n",
587            "    int method(int label) {\n",
588            "        return this->value + label.value + label.method()\n",
589            "            + label.template convert<int>() + label.ns::member;\n",
590            "    }\n",
591            "};\n",
592            "int build(Widget widget) {\n",
593            "    Widget result{.value = widget.value};\n",
594            "    return widget.method(result.value);\n",
595            "}\n",
596        );
597        let found = member_occurrences(source);
598        let member_texts = found
599            .iter()
600            .map(|(_, text, role)| (*text, *role))
601            .collect::<Vec<_>>();
602
603        assert_eq!(
604            member_texts,
605            vec![
606                ("value", OccurrenceRole::MemberPosition),
607                ("value", OccurrenceRole::MemberPosition),
608                ("method", OccurrenceRole::MemberPosition),
609                ("convert", OccurrenceRole::MemberPosition),
610                ("member", OccurrenceRole::MemberPosition),
611                ("value", OccurrenceRole::MemberPosition),
612                ("method", OccurrenceRole::MemberPosition),
613                ("value", OccurrenceRole::MemberPosition),
614            ]
615        );
616
617        let at = |needle: &str| source.find(needle).expect("fixture token");
618        for receiver in ["this->", "label.value", "widget.value", "result.value"] {
619            let receiver_start = at(receiver);
620            assert!(
621                found.iter().all(|(offset, _, _)| *offset != receiver_start),
622                "receiver was classified: {receiver:?}"
623            );
624        }
625        let non_members = [
626            at("Widget {"),
627            at("int value") + "int ".len(),
628            at("int method") + "int ".len(),
629            at(".value =") + 1,
630        ];
631        for unrelated_start in non_members {
632            assert!(
633                found
634                    .iter()
635                    .all(|(offset, _, _)| *offset != unrelated_start),
636                "non-member identifier at byte {unrelated_start} was classified"
637            );
638        }
639    }
640
641    #[test]
642    fn cpp_member_position_support_declares_only_member_position() {
643        let support = super::CPP_STRUCTURAL_SPEC.occurrence_role_support();
644        assert!(support.is_supported(OccurrenceRole::MemberPosition));
645        for role in [
646            OccurrenceRole::ReceiverPosition,
647            OccurrenceRole::LabelOrKey,
648            OccurrenceRole::DeclarationName,
649            OccurrenceRole::ValueReference,
650        ] {
651            assert!(
652                !support.is_supported(role),
653                "unexpected C++ support for {role:?}"
654            );
655        }
656    }
657}