Drop isReferenceType when erasing unknown types - #4070
Merged
Merged
Conversation
dgrunwald
reviewed
Aug 29, 2026
Whether an unresolvable type is a reference type is not a property of the type but of the metadata that mentioned it: a signature spelling it `valuetype T` yields false, a bare TypeRef yields null. UnknownType.Equals compares the flag, so the two spellings of one missing type compared unequal and EquivalentTypes reported false - the decompiler then emitted a cast between a type and itself. Erasing the flag in NormalizeTypeVisitor keeps the relaxation inside the comparisons that ask for erasure, next to the nullability, modopt and tuple erasure that are use-site spellings of the same kind. Dropping the term from UnknownType.Equals instead was measured and rejected: Equals also keys CSharpConversions' implicit-conversion cache, where merging the two spellings lets whichever conversion is computed first answer for both, adding 398 boxing casts across two real-world assemblies. Assisted-by: Claude:claude-opus-5[1m]:Claude Code
siegfriedpammer
force-pushed
the
fix/unknown-type-erasure
branch
from
August 29, 2026 16:26
3a60798 to
4bc51e6
Compare
1 task
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Whether an unresolvable type is a reference type is not a property of the type, but of the metadata that mentioned it: a signature spelling
valuetype TyieldsisReferenceType == false, a bareTypeRef(MemberRef parent,constrained.prefix) yieldsnull.UnknownType.Equalscompares that flag, so the two spellings of one and the same missing type compare unequal.NormalizeTypeVisitor.TypeErasure.EquivalentTypesthen reports false and the decompiler emits a cast between a type and itself, e.g. inILPretty/Issue3729:Fix
NormalizeTypeVisitorerases the flag before comparing. That keeps the relaxation inside the comparisons that ask for erasure, next to the nullability, modopt and tuple erasure it already performs — all use-site spellings of the same kind.Why not drop the term from
UnknownType.EqualsThat was the other candidate; it was built and measured, and it is worse.
Equalsis global, and among other things it keysCSharpConversions.implicitConversionCache(ConcurrentDictionary<(IType, IType), Conversion>).UnknownType.GetHashCodealready ignoresisReferenceType, so both spellings already share a hash bucket and onlyEqualskeeps them apart; merging them lets whichever conversion is computed first answer for both.Measured over 15 nuget assemblies decompiled with their dependencies absent:
(object)castsEqualschangeUnder the
Equalsvariant two sibling types in one method are treated differently —PushParent(val5)next toPushParent((object)val21), both unresolved reference types.Test
ILPretty/Issue3729's expectation carried the cast above and is updated to the correct output; it is the regression guard for this change.ICSharpCode.Decompiler.Tests: 3539 total, 3494 passed, 45 skipped, 0 failed.Written by Claude Code (
claude-opus-5) on behalf of @siegfriedpammer; measurements and test runs reproduced locally.