1use crate::declarations::{
37 cpp_template_parameter_kind, cpp_template_parameter_name, cpp_template_term, node_text,
38 normalize_cpp_whitespace,
39};
40use crate::graph::resolver::{
41 cpp_name_component_nodes, cpp_substitute_template_term, cpp_template_terms_equal,
42};
43use brokk_bifrost_core::analyzer::model::{CppTemplateParameterKind, CppTemplateTerm};
44use brokk_bifrost_core::analyzer::tree_walk::ParentIndex;
45use brokk_bifrost_core::hash::{HashMap, HashSet};
46use tree_sitter::Node;
47
48#[derive(Debug, Clone, PartialEq, Eq)]
50pub enum CppTemplateParameterShape {
51 Type,
52 Value {
57 type_term: CppTemplateTerm,
58 declarator: Option<CppTemplateTerm>,
59 },
60 Template(Vec<(CppTemplateParameterKind, bool)>),
64}
65
66#[derive(Debug, Clone, PartialEq, Eq)]
67pub struct CppTemplateParameter {
68 pub name: String,
70 pub variadic: bool,
71 pub shape: CppTemplateParameterShape,
72}
73
74impl CppTemplateParameter {
75 fn injected_argument(&self) -> CppTemplateTerm {
79 let parameter = CppTemplateTerm::Parameter(self.name.clone());
80 if self.variadic {
81 CppTemplateTerm::Node {
82 kind: "parameter_pack_expansion".to_owned(),
83 children: vec![
84 parameter,
85 CppTemplateTerm::Atom {
86 kind: "...".to_owned(),
87 text: "...".to_owned(),
88 },
89 ],
90 }
91 } else {
92 parameter
93 }
94 }
95}
96
97#[derive(Debug, Clone, PartialEq, Eq)]
99pub struct CppTemplateHead {
100 pub parameters: Vec<CppTemplateParameter>,
101 pub requires: Option<CppTemplateTerm>,
103}
104
105#[derive(Debug, Clone, PartialEq, Eq)]
108pub struct CppTemplateLevel {
109 pub name: String,
111 pub head: Option<CppTemplateHead>,
115 pub arguments: Option<Vec<CppTemplateTerm>>,
122}
123
124impl CppTemplateLevel {
125 fn is_bare(&self) -> bool {
126 self.head.is_none() && self.arguments.is_none()
127 }
128
129 pub fn selects_specialization(&self) -> bool {
135 let Some(arguments) = &self.arguments else {
136 return false;
137 };
138 let Some(head) = &self.head else {
139 return true;
140 };
141 arguments.len() != head.parameters.len()
142 || !arguments
143 .iter()
144 .zip(&head.parameters)
145 .all(|(argument, parameter)| {
146 cpp_template_terms_equal(argument, ¶meter.injected_argument())
147 })
148 }
149
150 fn effective_arguments(&self) -> Option<Vec<CppTemplateTerm>> {
151 self.arguments.clone().or_else(|| {
152 self.head.as_ref().map(|head| {
153 head.parameters
154 .iter()
155 .map(CppTemplateParameter::injected_argument)
156 .collect()
157 })
158 })
159 }
160}
161
162#[derive(Debug, Clone, PartialEq, Eq)]
165pub struct CppTemplateContext {
166 pub levels: Vec<CppTemplateLevel>,
167}
168
169#[derive(Debug, Clone, Default, PartialEq, Eq)]
172pub struct CppTemplateBinding {
173 right_to_left: HashMap<String, CppTemplateTerm>,
174 left: HashSet<String>,
175}
176
177impl CppTemplateBinding {
178 pub fn is_empty(&self) -> bool {
179 self.left.is_empty()
180 }
181
182 pub fn is_left_parameter(&self, name: &str) -> bool {
183 self.left.contains(name)
184 }
185
186 pub fn is_right_parameter(&self, name: &str) -> bool {
187 self.right_to_left.contains_key(name)
188 }
189
190 pub fn binds(&self, right: &str, left: &str) -> bool {
192 matches!(
193 self.right_to_left.get(right),
194 Some(CppTemplateTerm::Parameter(bound)) if bound == left
195 )
196 }
197
198 pub fn rename(&self, term: &CppTemplateTerm) -> Option<CppTemplateTerm> {
201 cpp_substitute_template_term(term, &self.right_to_left)
202 }
203
204 pub fn terms_agree(&self, left: &CppTemplateTerm, right: &CppTemplateTerm) -> bool {
206 self.rename(right)
207 .is_some_and(|renamed| cpp_template_terms_equal(left, &renamed))
208 }
209
210 fn bind(&mut self, right: &str, left: &str) {
211 self.left.insert(left.to_owned());
212 self.right_to_left.insert(
213 right.to_owned(),
214 CppTemplateTerm::Parameter(left.to_owned()),
215 );
216 }
217}
218
219#[derive(Debug, Clone, PartialEq, Eq)]
221pub enum CppTemplateReconciliation {
222 Equivalent(CppTemplateBinding),
224 Distinct,
228 Unsupported,
231}
232
233impl CppTemplateContext {
234 pub fn is_templated(&self) -> bool {
236 self.levels.iter().any(|level| !level.is_bare())
237 }
238
239 pub fn reconcile(&self, other: &CppTemplateContext) -> CppTemplateReconciliation {
246 let shared = self.levels.len().min(other.levels.len());
247 let (left_extra, left) = self.levels.split_at(self.levels.len() - shared);
248 let (right_extra, right) = other.levels.split_at(other.levels.len() - shared);
249 if !left_extra
250 .iter()
251 .chain(right_extra)
252 .all(CppTemplateLevel::is_bare)
253 {
254 return CppTemplateReconciliation::Unsupported;
255 }
256 let mut binding = CppTemplateBinding::default();
257 for (left_level, right_level) in left.iter().zip(right) {
258 if left_level.name != right_level.name {
259 return CppTemplateReconciliation::Unsupported;
260 }
261 match (&left_level.head, &right_level.head) {
262 (None, None) => {}
263 (Some(left_head), Some(right_head)) => {
264 if !heads_agree(left_head, right_head, &mut binding) {
265 return CppTemplateReconciliation::Distinct;
266 }
267 }
268 _ => return CppTemplateReconciliation::Distinct,
269 }
270 let agree = match (
271 left_level.effective_arguments(),
272 right_level.effective_arguments(),
273 ) {
274 (None, None) => true,
275 (Some(left_arguments), Some(right_arguments)) => {
276 left_arguments.len() == right_arguments.len()
277 && left_arguments
278 .iter()
279 .zip(&right_arguments)
280 .all(|(left, right)| binding.terms_agree(left, right))
281 }
282 _ => false,
283 };
284 if !agree {
285 return CppTemplateReconciliation::Distinct;
286 }
287 }
288 CppTemplateReconciliation::Equivalent(binding)
289 }
290}
291
292fn heads_agree(
297 left: &CppTemplateHead,
298 right: &CppTemplateHead,
299 binding: &mut CppTemplateBinding,
300) -> bool {
301 if left.parameters.len() != right.parameters.len() {
302 return false;
303 }
304 for (left_parameter, right_parameter) in left.parameters.iter().zip(&right.parameters) {
305 if left_parameter.variadic != right_parameter.variadic {
306 return false;
307 }
308 binding.bind(&right_parameter.name, &left_parameter.name);
309 let agree = match (&left_parameter.shape, &right_parameter.shape) {
310 (CppTemplateParameterShape::Type, CppTemplateParameterShape::Type) => true,
311 (
312 CppTemplateParameterShape::Value {
313 type_term: left_type,
314 declarator: left_declarator,
315 },
316 CppTemplateParameterShape::Value {
317 type_term: right_type,
318 declarator: right_declarator,
319 },
320 ) => {
321 binding.terms_agree(left_type, right_type)
322 && match (left_declarator, right_declarator) {
323 (None, None) => true,
324 (Some(left), Some(right)) => binding.terms_agree(left, right),
325 _ => false,
326 }
327 }
328 (
329 CppTemplateParameterShape::Template(left_kinds),
330 CppTemplateParameterShape::Template(right_kinds),
331 ) => left_kinds == right_kinds,
332 _ => false,
333 };
334 if !agree {
335 return false;
336 }
337 }
338 match (&left.requires, &right.requires) {
339 (None, None) => true,
340 (Some(left), Some(right)) => binding.terms_agree(left, right),
341 _ => false,
342 }
343}
344
345pub fn cpp_declaration_template_context(
353 node: Node<'_>,
354 source: &str,
355) -> Option<CppTemplateContext> {
356 let statement = declaration_statement(node)?;
357 let (own_heads, top) = direct_template_heads(statement);
358 let qualified = declaration_qualifier(statement);
359 let mut enclosing = Vec::new();
360 let mut current = top;
361 while let Some(parent) = current.parent() {
362 match parent.kind() {
363 "field_declaration_list" => {
364 let class = parent.parent()?;
365 if !is_class_specifier(class) {
366 return None;
367 }
368 enclosing.push(class);
369 current = class;
370 }
371 "template_declaration"
372 | "declaration"
373 | "field_declaration"
374 | "declaration_list"
375 | "namespace_definition"
376 | "linkage_specification"
377 | "preproc_if"
378 | "preproc_ifdef"
379 | "preproc_else"
380 | "preproc_elif"
381 | "preproc_elifdef" => current = parent,
382 "translation_unit" => break,
383 _ => return None,
384 }
385 }
386 let mut builder = ContextBuilder {
387 source,
388 ancestry: ParentIndex::unindexed(),
389 names: Vec::new(),
390 levels: Vec::new(),
391 };
392 for class in enclosing.into_iter().rev() {
393 let (heads, _) = direct_template_heads(class);
394 match heads.as_slice() {
395 [] => builder.push_class(class, None)?,
396 [head] => builder.push_class(class, Some(*head))?,
397 _ => return None,
398 }
399 }
400 if is_class_specifier(statement) {
401 match own_heads.as_slice() {
402 [] => builder.push_class(statement, None)?,
403 [head] => builder.push_class(statement, Some(*head))?,
404 _ => return None,
405 }
406 return Some(builder.finish());
407 }
408 match qualified {
409 Some(_) if !builder.levels.is_empty() => return None,
412 Some(qualifier) => builder.push_out_of_line(qualifier, &own_heads)?,
413 None => {
414 let head = match own_heads.as_slice() {
415 [] => None,
416 [head] => builder.head(*head)?,
417 _ => return None,
418 };
419 builder.levels.push(CppTemplateLevel {
420 name: String::new(),
421 head,
422 arguments: None,
423 });
424 }
425 }
426 if let Some(arguments) = declaration_terminal(statement)
430 .filter(|terminal| terminal.kind() == "template_function")
431 .and_then(|terminal| terminal.child_by_field_name("arguments"))
432 {
433 let arguments = builder.arguments(arguments);
434 builder.levels.last_mut()?.arguments = Some(arguments);
435 }
436 Some(builder.finish())
437}
438
439pub fn cpp_out_of_line_owner_names(node: Node<'_>, source: &str) -> Option<Vec<String>> {
449 let context = cpp_declaration_template_context(node, source)?;
450 let mut components =
451 cpp_name_component_nodes(declaration_qualifier(declaration_statement(node)?)?)?;
452 components.pop()?;
453 debug_assert_eq!(context.levels.len(), components.len() + 1);
455 let names = components
456 .into_iter()
457 .zip(&context.levels)
458 .map(|(component, level)| {
459 debug_assert_eq!(level.name, node_text(component, source));
460 match component.parent() {
461 Some(template)
462 if level.selects_specialization()
463 && template.kind() == "template_type"
464 && template.child_by_field_name("name") == Some(component) =>
465 {
466 normalize_cpp_whitespace(node_text(template, source))
467 }
468 _ => node_text(component, source).to_owned(),
469 }
470 })
471 .collect();
472 Some(names)
473}
474
475struct ContextBuilder<'a, 'tree> {
476 source: &'a str,
477 ancestry: ParentIndex<'tree>,
478 names: Vec<String>,
480 levels: Vec<CppTemplateLevel>,
481}
482
483impl<'tree> ContextBuilder<'_, 'tree> {
484 fn finish(self) -> CppTemplateContext {
485 CppTemplateContext {
486 levels: self.levels,
487 }
488 }
489
490 fn push_class(&mut self, class: Node<'tree>, head: Option<Node<'tree>>) -> Option<()> {
491 let head = match head {
492 Some(head) => self.head(head)?,
493 None => None,
494 };
495 let (name, arguments) = match class.child_by_field_name("name") {
496 None => (String::new(), None),
497 Some(name) if name.kind() == "template_type" => (
498 node_text(name.child_by_field_name("name")?, self.source).to_owned(),
499 Some(self.arguments(name.child_by_field_name("arguments")?)),
500 ),
501 Some(name) if name.kind() == "type_identifier" => {
502 (node_text(name, self.source).to_owned(), None)
503 }
504 Some(_) => return None,
507 };
508 self.levels.push(CppTemplateLevel {
509 name,
510 head,
511 arguments,
512 });
513 Some(())
514 }
515
516 fn push_out_of_line(&mut self, qualifier: Node<'tree>, heads: &[Node<'tree>]) -> Option<()> {
525 let mut components = cpp_name_component_nodes(qualifier)?;
526 components.pop()?;
527 let mut heads = heads.iter().copied().peekable();
528 for component in components {
529 let arguments = component
530 .parent()
531 .filter(|parent| {
532 matches!(parent.kind(), "template_type" | "template_function")
533 && parent.child_by_field_name("name") == Some(component)
534 })
535 .map(|template| template.child_by_field_name("arguments"));
536 let name = node_text(component, self.source).to_owned();
537 let Some(arguments) = arguments else {
538 self.levels.push(CppTemplateLevel {
539 name,
540 head: None,
541 arguments: None,
542 });
543 continue;
544 };
545 let arguments = arguments?;
546 let scope = self.names.len();
547 let head = match heads.peek().copied() {
548 Some(head) => {
549 let parsed = self.head(head)?;
550 let consumed = match &parsed {
551 None => true,
552 Some(parsed) => {
553 let introduced = parsed
554 .parameters
555 .iter()
556 .map(|parameter| parameter.name.as_str())
557 .collect::<HashSet<_>>();
558 self.arguments(arguments)
559 .iter()
560 .any(|term| term_names_any(term, &introduced))
561 }
562 };
563 if consumed {
564 heads.next();
565 parsed
566 } else {
567 self.names.truncate(scope);
568 None
569 }
570 }
571 None => None,
572 };
573 let arguments = Some(self.arguments(arguments));
574 self.levels.push(CppTemplateLevel {
575 name,
576 head,
577 arguments,
578 });
579 }
580 let own = match heads.next() {
581 Some(head) => self.head(head)?,
582 None => None,
583 };
584 if heads.next().is_some() {
585 return None;
586 }
587 self.levels.push(CppTemplateLevel {
588 name: String::new(),
589 head: own,
590 arguments: None,
591 });
592 Some(())
593 }
594
595 fn head(&mut self, template: Node<'tree>) -> Option<Option<CppTemplateHead>> {
598 let list = template.child_by_field_name("parameters")?;
599 let mut parameters = Vec::new();
600 let mut cursor = list.walk();
601 for (index, parameter) in list
602 .named_children(&mut cursor)
603 .filter(|child| !child.is_extra())
604 .enumerate()
605 {
606 if !matches!(
607 parameter.kind(),
608 "type_parameter_declaration"
609 | "optional_type_parameter_declaration"
610 | "variadic_type_parameter_declaration"
611 | "template_template_parameter_declaration"
612 | "parameter_declaration"
613 | "optional_parameter_declaration"
614 | "variadic_parameter_declaration"
615 ) {
616 return None;
617 }
618 let name = cpp_template_parameter_name(parameter, self.source)
619 .unwrap_or_else(|| format!("<anonymous:{}:{index}>", self.names.len()));
620 self.names.push(name.clone());
621 let variadic = matches!(
622 parameter.kind(),
623 "variadic_type_parameter_declaration" | "variadic_parameter_declaration"
624 );
625 let shape = match cpp_template_parameter_kind(parameter) {
626 CppTemplateParameterKind::Type => CppTemplateParameterShape::Type,
627 CppTemplateParameterKind::Template => {
628 let nested = parameter.child_by_field_name("parameters")?;
629 let mut cursor = nested.walk();
630 CppTemplateParameterShape::Template(
631 nested
632 .named_children(&mut cursor)
633 .filter(|child| !child.is_extra())
634 .map(|child| {
635 (
636 cpp_template_parameter_kind(child),
637 matches!(
638 child.kind(),
639 "variadic_type_parameter_declaration"
640 | "variadic_parameter_declaration"
641 ),
642 )
643 })
644 .collect(),
645 )
646 }
647 CppTemplateParameterKind::Value => CppTemplateParameterShape::Value {
648 type_term: self.term(parameter.child_by_field_name("type")?),
649 declarator: parameter
650 .child_by_field_name("declarator")
651 .filter(|declarator| {
652 !matches!(declarator.kind(), "identifier" | "variadic_declarator")
653 })
654 .map(|declarator| self.term(declarator)),
655 },
656 };
657 parameters.push(CppTemplateParameter {
658 name,
659 variadic,
660 shape,
661 });
662 }
663 let mut cursor = template.walk();
664 let requires = template
665 .named_children(&mut cursor)
666 .find(|child| child.kind() == "requires_clause")
667 .map(|clause| self.term(clause));
668 Some((!parameters.is_empty()).then_some(CppTemplateHead {
669 parameters,
670 requires,
671 }))
672 }
673
674 fn arguments(&self, list: Node<'tree>) -> Vec<CppTemplateTerm> {
675 let mut cursor = list.walk();
676 list.named_children(&mut cursor)
677 .filter(|argument| !argument.is_extra() && argument.kind() != "comment")
678 .map(|argument| self.term(argument))
679 .collect()
680 }
681
682 fn term(&self, node: Node<'tree>) -> CppTemplateTerm {
683 cpp_template_term(node, self.source, &self.names, &self.ancestry)
684 }
685}
686
687fn term_names_any(term: &CppTemplateTerm, names: &HashSet<&str>) -> bool {
688 let mut work = vec![term];
689 while let Some(term) = work.pop() {
690 match term {
691 CppTemplateTerm::Parameter(name) if names.contains(name.as_str()) => return true,
692 CppTemplateTerm::Node { children, .. } => work.extend(children),
693 CppTemplateTerm::Parameter(_) | CppTemplateTerm::Atom { .. } => {}
694 }
695 }
696 false
697}
698
699fn is_class_specifier(node: Node<'_>) -> bool {
700 matches!(
701 node.kind(),
702 "class_specifier" | "struct_specifier" | "union_specifier"
703 )
704}
705
706fn declaration_statement(node: Node<'_>) -> Option<Node<'_>> {
709 let mut current = node;
710 loop {
711 match current.kind() {
712 "function_definition" | "declaration" | "field_declaration" => return Some(current),
713 _ if is_class_specifier(current) => return Some(current),
714 "function_declarator"
715 | "pointer_declarator"
716 | "reference_declarator"
717 | "parenthesized_declarator"
718 | "attributed_declarator"
719 | "init_declarator" => current = current.parent()?,
720 _ => return None,
721 }
722 }
723}
724
725fn class_statement(class: Node<'_>) -> Node<'_> {
728 class
729 .parent()
730 .filter(|parent| {
731 matches!(parent.kind(), "declaration" | "field_declaration")
732 && parent.child_by_field_name("type") == Some(class)
733 })
734 .unwrap_or(class)
735}
736
737fn direct_template_heads(statement: Node<'_>) -> (Vec<Node<'_>>, Node<'_>) {
740 let statement = if is_class_specifier(statement) {
741 class_statement(statement)
742 } else {
743 statement
744 };
745 let mut heads = Vec::new();
746 let mut current = statement;
747 while let Some(parent) = current.parent() {
748 if parent.kind() != "template_declaration"
749 || parent.child_by_field_name("parameters") == Some(current)
750 {
751 break;
752 }
753 heads.push(parent);
754 current = parent;
755 }
756 heads.reverse();
757 (heads, current)
758}
759
760fn declaration_qualifier(statement: Node<'_>) -> Option<Node<'_>> {
762 if is_class_specifier(statement) {
763 return None;
764 }
765 let mut current = statement.child_by_field_name("declarator")?;
766 loop {
767 match current.kind() {
768 "qualified_identifier" => return Some(current),
769 "identifier" | "field_identifier" | "operator_name" | "destructor_name"
770 | "template_function" => return None,
771 _ => {
772 current = current
773 .child_by_field_name("declarator")
774 .or_else(|| current.named_child(0))?;
775 }
776 }
777 }
778}
779
780fn declaration_terminal(statement: Node<'_>) -> Option<Node<'_>> {
783 if is_class_specifier(statement) {
784 return None;
785 }
786 let mut current = statement.child_by_field_name("declarator")?;
787 loop {
788 match current.kind() {
789 "qualified_identifier" => current = current.child_by_field_name("name")?,
790 "identifier" | "field_identifier" | "operator_name" | "destructor_name"
791 | "template_function" => return Some(current),
792 _ => {
793 current = current
794 .child_by_field_name("declarator")
795 .or_else(|| current.named_child(0))?;
796 }
797 }
798 }
799}
800
801#[cfg(test)]
802mod tests {
803 use super::*;
804
805 const DECLARATIONS: &str = r#"namespace n {
806template<class T> struct Box {
807 void put(T);
808 template<class U> void take(U);
809};
810template<class T> struct Box<T*> {
811 void put(T*);
812};
813template<> struct Box<int> {
814 void exact();
815};
816template<class T, class U> struct Pair {
817 void pair();
818};
819template<int N> struct Fixed {
820 void reset();
821};
822template<class... Ts> struct Pack {
823 void pack();
824};
825template<class T> requires Ready<T> struct Gate {
826 void open();
827};
828void make();
829}
830"#;
831
832 const DEFINITIONS: &str = r#"namespace n {
833template<typename U> void Box<U>::put(U) {}
834template<class T> template<class W> void Box<T>::take(W) {}
835template<class T> void Box<T*>::put(T*) {}
836void Box<int>::exact() {}
837template<class T> void Pair<T>::pair() {}
838template<long N> void Fixed<N>::reset() {}
839template<int M> void Fixed<M>::reset() {}
840template<class... Us> void Pack<Us...>::pack() {}
841template<class T> void Pack<T>::pack() {}
842template<class U> requires Ready<U> void Gate<U>::open() {}
843template<class U> void Gate<U>::open() {}
844template<> void make<int>() {}
845using Exact = Box<int>;
846void Exact::exact() {}
847}
848"#;
849
850 fn context_of(source: &str, needle: &str) -> CppTemplateContext {
851 let tree = crate::diagnostics::parse_cpp_tree(source);
852 assert!(!tree.root_node().has_error(), "fixture must parse cleanly");
853 let start = source.find(needle).expect("needle");
854 let mut node = tree
855 .root_node()
856 .descendant_for_byte_range(start, start + needle.len())
857 .expect("needle node");
858 while !matches!(
859 node.kind(),
860 "function_definition" | "field_declaration" | "declaration" | "template_declaration"
861 ) {
862 node = node.parent().expect("declaration ancestor");
863 }
864 while node.kind() == "template_declaration" {
865 node = node
866 .named_child(node.named_child_count() - 1)
867 .expect("templated declaration");
868 }
869 cpp_declaration_template_context(node, source)
870 .unwrap_or_else(|| panic!("no template context for {needle}"))
871 }
872
873 #[test]
881 fn reconcile_matches_heads_and_owner_arguments_structurally() {
882 use CppTemplateReconciliation::{Distinct, Unsupported};
883 let cases = [
884 ("put(T);", "Box<U>::put(U)", Some(true)),
885 ("take(U)", "Box<T>::take(W)", Some(true)),
886 ("put(T*)", "Box<T*>::put(T*)", Some(true)),
887 ("put(T);", "Box<T*>::put(T*)", Some(false)),
888 ("exact();", "Box<int>::exact()", Some(true)),
889 ("put(T);", "Box<int>::exact()", Some(false)),
890 ("pair();", "Pair<T>::pair()", Some(false)),
891 ("reset();", "Fixed<N>::reset()", Some(false)),
892 ("reset();", "Fixed<M>::reset()", Some(true)),
893 ("pack();", "Pack<Us...>::pack()", Some(true)),
894 ("pack();", "Pack<T>::pack()", Some(false)),
895 ("open();", "Ready<U> void Gate<U>::open()", Some(true)),
896 ("open();", "<class U> void Gate<U>::open()", Some(false)),
897 ("make();", "make<int>()", Some(false)),
898 ("exact();", "Exact::exact()", None),
899 ];
900 let mut mismatches = Vec::new();
901 for (declaration, definition, expected) in cases {
902 let left = context_of(DECLARATIONS, declaration);
903 let right = context_of(DEFINITIONS, definition);
904 let forward = left.reconcile(&right);
905 let inverse = right.reconcile(&left);
906 let verdict = |reconciliation: &CppTemplateReconciliation| match reconciliation {
907 CppTemplateReconciliation::Equivalent(_) => Some(true),
908 Distinct => Some(false),
909 Unsupported => None,
910 };
911 if verdict(&forward) != expected || verdict(&inverse) != expected {
912 mismatches.push((declaration, definition, forward, inverse));
913 }
914 }
915 assert!(mismatches.is_empty(), "{mismatches:#?}");
916 }
917}