The C#/Roslyn-form ID of a member whose C++/CLI form differs can equal
the only key of a same-named sibling overload (char* vs signed char*).
Assemblies containing such overloads cannot come from the C# compiler,
so their xml files use the C++/CLI dialect, where that key documents
the sibling: falling back to the Roslyn form would show the sibling's
documentation for an undocumented member. GetIdStringCandidates now
omits the Roslyn form when a same-named sibling's C++/CLI form owns it,
so a lookup miss stays a miss.
Assisted-by: Claude:claude-fable-5:Claude Code
The recursive-descent parser for the full ID string grammar existed only
to feed the type-system-based FindEntity, while signatures were already
matched by regenerating candidate IDs; with two dialects a parser would
have to implement both grammars and stay in sync with the generator.
FindEntity now only decodes the structural skeleton - the declaring type
name and the member name - resolves the type, and narrows the members by
metadata name before the generate-and-compare step, falling back to an
unfiltered scan for keys whose name does not equal the metadata name
(a C++/CLI 'default' indexer). ParseTypeName, ParseMemberIdString and
their reference types (IdStringMemberReference,
GetPotentiallyNestedClassTypeReference) are removed.
Assisted-by: Claude:claude-fable-5:Claude Code
Member lookup compares regenerated ID strings, and with two dialects a
single mixed pass can resolve to the wrong member: the stripped
C#/Roslyn form of one member can equal the C++/CLI-dialect key of a
different member, e.g. C++ overloads differing only in a custom
modifier such as char* vs signed char*. FindMemberInType therefore
runs two passes over the whole member list, the more specific C++/CLI
form first, so an MSVC-written cref resolves to the member it names
instead of the first member whose stripped form happens to collide.
Assisted-by: Claude:claude-fable-5:Claude Code
MSVC documents C++/CLI members with ECMA-372-style ID strings that
differ from Roslyn's in signatures: custom modifiers are rendered after
the modified type (a 'const int' parameter becomes
System.Int32!System.Runtime.CompilerServices.IsConst), arity markers
stay on generic instantiations (List`1{System.Int32}.Enumerator), and
the default indexed property is called 'default'. Roslyn ignores
modifiers entirely, so one generated string cannot match both
compilers' xml files: GetIdString keeps producing the C#/Roslyn form,
and the new GetIdStringCandidates additionally yields the C++/CLI form,
most specific first, for lookup code to try in order.
The dialect is pinned by IdStringProbe.il/.xml, the trimmed disassembly
of an MSVC-compiled probe assembly together with the unmodified xml MSVC
generated for it. Notable observed deviations from MSVC's documented
format: modreq is generated, but only modreq(IsVolatile) uses the
documented '|'; modreq(IsByValue) on conversion operator operands is
rendered with '!'.
Assisted-by: Claude:claude-fable-5:Claude Code
Resolve an ID string to a (module, handle) pair by scanning metadata
directly: namespace/type-name splits are tried at every dot (the format
does not mark the boundary), nested types and type forwarders are
walked, and members are matched by regenerating each candidate's ID
string instead of parsing the signature portion, which keeps resolution
in sync with generation by construction. This gives navigation a
resolution path that needs no type system and works on assemblies
exactly as their metadata spells them. Inherent format limitations
(metadata names containing ID string special characters; function
pointer types rendering empty, so such overloads share an ID) are
documented on the method.
Assisted-by: Claude:claude-fable-5:Claude Code
Generate ID strings directly from metadata instead of the type system,
so callers holding only a MetadataFile and an EntityHandle do not need
to materialize a compilation first, and raw metadata names survive
verbatim. GetIdString(IEntity) stays as a thin wrapper over the new
implementation, keeping the published entity-based entry point intact.
The implementation is validated by a differential suite comparing every
symbol of a test corpus against Roslyn's GetDocumentationCommentId.
Corners pinned by those tests: generic arguments distribute to their
nesting level (Outer{A}.Inner{B}), op_CheckedExplicit carries the
~ReturnType suffix like the other conversion operators, and custom
modifiers are ignored like Roslyn ignores them (a virtual method's 'in'
parameter is modreq(InAttribute) but documented as T@). Array shapes
use the spec's lowerbound:size notation, covered by a hand-built
module, since C# cannot express non-default array bounds in signatures.
Assisted-by: Claude:claude-fable-5:Claude Code
While support for multi-module assemblies isn't fully working yet; it is clear at this point that we want
to treat each module in a multi-module assembly separately for the purposes of the type system.