fix(cpp): an explicit specialization, like protobuf's WireFormatLite::ReadPrimitive<int32_t, WireFormatLite::TYPE_INT32>, is named after its template (#2609)

An explicit specialization of a function or member function template kept
its template arguments in its name: protobuf's `template <> bool
WireFormatLite::ReadPrimitive<int32_t, WireFormatLite::TYPE_INT32>(…)` was
named `TYPE_INT32>` and qualified `…::ReadPrimitive::TYPE_INT32>`, the
qualified id split at the `::` inside its arguments; rocksdb's `template <>
int64_t Deserialize<int64_t>(…)` was `Deserialize<int64_t>`. A call writes
the bare name, so nothing linked to them.

Both extractors (wasm and the native kernel) now name a specialization
after its template, from the template-id's own name, and take its receiver
from the text before the declarator's name. That text must end in a real
`::`: tree-sitter recovers Metal's `vertex VertexOut vertexShader(…)` with
a missing, zero-width one.

A specialization now meets its template among a name's candidates, so the
resolver chooses between them wherever it picks among C++ candidates (a
class's methods, the implicit-this walk and its bases, exact, qualified,
global, fuzzy and macro-namespace matching):
 - a call that writes template arguments reaches the specialization
   written with the same ones (spacing aside, or qualified further on one
   side), else the template itself or an ordinary overload, else nothing:
   fmt's `num_bits<unsigned short>()` stays on `num_bits<T>()` instead of
   the nearer `num_bits<uint128>()`; protobuf's `Arena::Create<T>` keeps
   calling `DefaultConstruct<T>`, not the tests' `DefaultConstruct<
   DispatcherTestProto>` hook; a dependent `ReadPrimitive<CType,
   DeclaredType>(…)` links nowhere rather than to the first specialization.
 - a call that writes none, or `<>`, reaches a specialization only when
   nothing else has the name.
 - a class's own member template, specialized but none for these
   arguments, ends the lookup there instead of falling to a base's
   namesake.
The `cpp-override` pass leaves specializations out on both sides: a member
template is never virtual.

Validated on the cgstd kit against main (f7951a13): leveldb and redis
byte-identical; fmt: 8 specializations renamed, every call target as on
main; rocksdb: the cassandra `Serialize<T>`/`Deserialize<T>` calls (25 in
format.cc, 75 in its tests, which main sent to `ColumnBase` methods) each
reach the specialization their argument names (100 of 100 agree);
protobuf: `MergeFrom<MessageLite>` (3 calls), `ReadPrimitive<int,
TYPE_ENUM>` (2) and a test's `ReadPackedPrimitive<uint32_t, TYPE_FIXED32>`
reach their specializations; 5 dependent calls like
`GetRepeatedFieldInternal<T>`, and one `MutableRepeatedFieldInternal<int>`
(the macro writes `<int32_t>`), no longer link to the first specialization
a macro defines. Kernel and wasm dumps identical (but for protobuf's known
Swift line). Full suite: 7179 passed, 0 failed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-16 02:40:57 +00:00
committed by GitHub
parent f7951a13db
commit 627018eb25
6 changed files with 724 additions and 30 deletions
+69 -6
View File
@@ -659,6 +659,9 @@ impl<'t> Walker<'t> {
if let Some(mut parts) = self.cpp_written_class_parts(node) {
return parts.pop().unwrap_or_default();
}
if let Some(name) = self.cpp_specialized_function_name(node) {
return name;
}
}
if let Some(name_node) = node.child_by_field_name("declarator") {
let mut resolved = name_node;
@@ -764,18 +767,52 @@ impl<'t> Walker<'t> {
Some(self.text(identifier).to_string())
}
/// extractCppQualifiedMethodName (languages/c-cpp.ts:75).
/// extractCppQualifiedMethodName (languages/c-cpp.ts): the name the
/// declarator's qualified id ends in, an explicit specialization's without
/// its template arguments (cppDefinedName).
fn extract_cpp_qualified_method_name(&self, node: Node) -> Option<String> {
if let Some(n) = self.recover_cpp_macro_defined_name(node) {
return Some(n);
}
let declarator = node.child_by_field_name("declarator")?;
let qid = find_declarator_qualified_id(declarator)?;
if let Some(name) = cpp_qualified_id_name(qid) {
return Some(self.cpp_defined_name(name));
}
let text = self.text(qid).trim();
let parts: Vec<&str> = text.split("::").filter(|p| !p.is_empty()).collect();
parts.last().map(|s| s.to_string())
}
/// cppDefinedName (languages/c-cpp.ts): a definition's name node as
/// named, a `template_function`'s without its template arguments
/// (`ReadPrimitive<int32_t, WireFormatLite::TYPE_INT32>` is `ReadPrimitive`).
fn cpp_defined_name(&self, name: Node) -> String {
let own = if name.kind() == "template_function" { name.child_by_field_name("name") } else { None };
self.text(own.unwrap_or(name)).trim().to_string()
}
/// cppSpecializedFunctionName (languages/c-cpp.ts): the name of a
/// function an explicit specialization defines, without its template
/// arguments (`Swap` for `template <> void Swap<Foo>(Foo& a, Foo& b)`).
fn cpp_specialized_function_name(&self, node: Node) -> Option<String> {
if node.kind() != "function_definition" {
return None;
}
let mut declarator = node.child_by_field_name("declarator")?;
while matches!(declarator.kind(), "pointer_declarator" | "reference_declarator") {
declarator = declarator.child_by_field_name("declarator").or_else(|| declarator.named_child(0))?;
}
if declarator.kind() != "function_declarator" {
return None;
}
let inner = declarator.child_by_field_name("declarator")?;
if inner.kind() != "template_function" {
return None;
}
Some(self.cpp_defined_name(inner))
}
/// recoverSingleArgMacroDefinedName: only a local macro definition proves
/// that its argument is the function name, in either parser shape (#1373).
fn recover_single_arg_macro_defined_name(&self, node: Node) -> Option<String> {
@@ -899,16 +936,32 @@ impl<'t> Walker<'t> {
Some(name.to_string())
}
/// extractCppReceiverType (languages/c-cpp.ts:86).
/// extractCppReceiverType (languages/c-cpp.ts): the scopes are what the
/// qualified id writes before its name, so a specialization's arguments
/// (`ReadPrimitive<int32_t, WireFormatLite::TYPE_INT32>`) are no scope.
fn receiver_type_of(&self, node: Node) -> Option<String> {
let declarator = node.child_by_field_name("declarator")?;
let qid = find_declarator_qualified_id(declarator)?;
let text = self.text(qid).trim();
let parts: Vec<&str> = text.split("::").filter(|p| !p.is_empty()).collect();
if parts.len() <= 1 {
let scopes = match cpp_qualified_id_name(qid) {
Some(name) => {
let written = self.src[qid.start_byte()..name.start_byte()].trim_end();
// A `::` the parser supplied in recovery writes nothing: Metal's
// `vertex VertexOut vertexShader(…)` reads as `VertexOut::vertexShader`.
if !written.ends_with("::") {
return None;
}
written.to_string()
}
None => {
let parts: Vec<&str> = self.text(qid).trim().split("::").filter(|p| !p.is_empty()).collect();
parts[..parts.len().saturating_sub(1)].join("::")
}
};
let parts: Vec<&str> = scopes.split("::").filter(|p| !p.is_empty()).collect();
if parts.is_empty() {
return None;
}
let receiver = strip_cpp_template_args(&parts[..parts.len() - 1].join("::"));
let receiver = strip_cpp_template_args(&parts.join("::"));
if receiver.is_empty() {
None
} else {
@@ -2391,6 +2444,16 @@ impl<'t> Walker<'t> {
// --- free helpers ------------------------------------------------------------
/// cppQualifiedIdName (languages/c-cpp.ts): the name a `qualified_identifier`
/// ends in, past its scopes. None when the tree lacks one.
fn cpp_qualified_id_name(qid: Node) -> Option<Node> {
let mut name = qid;
while name.kind() == "qualified_identifier" {
name = name.child_by_field_name("name")?;
}
Some(name)
}
/// findDeclaratorQualifiedId (languages/c-cpp.ts:13): BFS for the declarator's
/// `qualified_identifier`, skipping parameter_list + trailing_return_type so a
/// qualified PARAMETER type can't be mistaken for the method name.