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