A C# anonymous type is immutable and compares every member. VB's are neither
unless every property is declared 'Key': otherwise the properties are settable
and only the 'Key' ones take part in Equals and GetHashCode. Writing such a
type as 'new { ... }' silently gave it value equality and made any assignment
to one of its properties fail to compile, so only an anonymous type with no
settable property is treated as one; the rest keep their own declaration.
Those declarations carry the shape VB gave them, so the round-trip preserves
both mutability and 'Key' equality. Their names are the remaining obstacle,
since the VB compiler separates the parts with '$': the type, its backing
fields and any local named after it are renamed to use '_' instead, and a
comment on the declaration says why the type is spelled out.
Generated variable names are now rejected when they would not be legal C#
identifiers, which also stops a display class from lending its unspeakable
name to a local in the NoLocalFunctions output.
Assisted-by: Claude:claude-fable-5:Claude Code
Two predicates disagreed on what a generated name looks like. At the metadata
level a '$' in the name counts, so MemberIsHidden treated VB$AnonymousType_0
as an anonymous type and dropped its definition from the output. At the type
system level only '<' counted, so none of the anonymous-type translations in
CallBuilder and ExpressionBuilder fired. VB assemblies therefore lost the
definitions and kept the raw metadata names at every use site, which is not
valid C#.
Both levels now share one predicate and cannot drift apart again. It keeps the
metadata-level behaviour exactly: counting every name that merely contains
'<' would newly capture explicit implementations of generic interface members.
Assisted-by: Claude:claude-fable-5:Claude Code