Commit Graph
1736 Commits
Author SHA1 Message Date
Christoph Wille 519ed6e98c Skip the StringBuilder in EscapeIdentifier when nothing needs escaping
EscapeIdentifier runs for every identifier token emitted, yet it built
a StringBuilder plus a fresh result string even for the overwhelmingly
common case of an identifier with no escapable characters. A pre-scan
now returns the original instance untouched in that case, and the
surrogate-pair copy appends the two chars directly instead of cutting
a two-char substring. New unit tests pin the escaping behavior and the
identity fast path.

Assisted-by: Claude:claude-fable-5:Claude Code
2026-10-10 18:30:53 +02:00
Christoph Wille 9125b196d4 Fuse the reflection-name and arity parse to cut each string once
SplitTypeParameterCountFromReflectionName allocated the digits after
the backtick only to feed int.TryParse and discard them, and the
TopLevelTypeName constructor cut the name part twice for generic
types (once at the dot, again at the backtick), dropping the first
cut. Both run for every generic reflection name parsed, e.g. for
typeof-valued attribute arguments and string-switch metadata. The
arity is now parsed in place with a digit loop (netstandard2.0 has
no span int.TryParse) and each final string is cut exactly once.

The digit loop only accepts plain ASCII digits, so suffixes like
`+1 that int.TryParse tolerated are now rejected; such names are not
legal reflection names. New unit tests pin the parse edge cases.

Assisted-by: Claude:claude-fable-5:Claude Code
2026-10-10 18:30:53 +02:00
Siegfried Pammer c3da4a8358 Merge pull request #4225 from icsharpcode/fix/continue-leave-in-catch
Fix #2829: Emit continue for leave through try-finally
2026-10-10 14:59:21 +02:00
Siegfried Pammer 803d44e07f Make continue-in-catch test match issue shape
The regression only needs the nested catch inside an enclosing handler from #2829. Removing the explicit try/finally keeps the fixture focused on the failing control-flow shape instead of compiler-generated cleanup details.

Assisted-by: OpenCode:openai/gpt-5.5:OpenCode
2026-10-10 14:31:14 +02:00
Siegfried Pammer 04a0b6f1d0 Fix #2829: Emit continue for leave through try-finally
A catch inside an enclosing EH region can leave to a try body that immediately branches to the loop continuation. Treat that leave as a continue so decompilation does not expose the synthetic end label for the catch path.

Assisted-by: OpenCode:openai/gpt-5.5:OpenCode
2026-10-08 23:07:27 +02:00
Siegfried Pammer df79031e93 Merge pull request #4223 from icsharpcode/fix/2744-resx-resource-writer
Fix ResXResourceWriter empty output and type names
2026-10-08 20:57:31 +02:00
Siegfried Pammer aa18f6c075 Merge pull request #4222 from icsharpcode/fix/2199-vb-closure
Fix/2199 vb closure
2026-10-08 20:55:56 +02:00
Siegfried Pammer 73ec67e2af Allow configuring ResX assembly names
Resource export sometimes needs framework-specific ResX type names. Exposing the WinForms and mscorlib assembly suffixes keeps the writer default-compatible while letting callers choose older resource type names when needed.

Assisted-by: OpenCode:openai/gpt-5.5:OpenCode
2026-10-08 19:28:07 +02:00
Siegfried Pammer 6b43671564 Write empty ResX files on dispose
ResX export should still produce a valid file when no resources were added and the writer is finalized through Close or Dispose. Calling Generate from Close preserves creation timing while matching the empty-file behavior.

Assisted-by: OpenCode:openai/gpt-5.5:OpenCode
2026-10-08 19:26:44 +02:00
Siegfried Pammer 565e1a3442 Handle legacy VB delegate wrappers
VB can lower event-handler closures through generated wrapper methods, closure copy constructors, and immediately invoked VB anonymous delegate helpers. Recognizing and unwrapping those shapes leaves the expected lambda form and lets captured locals be scalar-replaced instead of exposing generated closure artifacts.

Assisted-by: OpenCode:openai/gpt-5.5:OpenCode
2026-10-08 17:52:13 +02:00
Siegfried Pammer accbd3eda0 Merge pull request #4220 from icsharpcode/fix/3180-nullprop-extension-method
Fix #3180: avoid invalid null-conditional extension calls
2026-10-07 20:15:07 +02:00
Siegfried Pammer 8f66608d82 Improve dynamic reference tooltips
Dynamic member tooltips should describe the visible static target, while punctuation should stay reserved for useful call targets such as constructors. This keeps dynamic object creation tooltips on the constructor parentheses but avoids noisy synthetic member references on dynamic invocation parentheses.

Assisted-by: OpenAI:openai/gpt-5.5:OpenCode
2026-10-06 07:35:36 +02:00
Christoph Wille c2d51a80b9 Remove DiasymReader from restore entirely (update other NuGet packages as well) 2026-10-05 08:01:59 +02:00
Siegfried Pammer 98b010ebe3 Merge pull request #4202 from icsharpcode/fix/1374-mixed-array-initializer
Fix mixed array initializers
2026-10-04 07:09:17 +02:00
Siegfried Pammer 2a204533ca Handle mixed array initializers
Roslyn can lower mostly constant arrays with a static data blob and leave non-constant elements as explicit stores. Merge those trailing stores into the decoded initializer so the decompiler keeps a single array initializer instead of splitting the expression.

Assisted-by: OpenCode:openai/gpt-5.5:opencode
2026-10-03 21:36:55 +02:00
Siegfried Pammer ddcac25c2b Repair private virtual member output
Metadata can contain private virtual members that C# cannot declare directly. Adjusting the emitted accessibility in the decompiler keeps such members compilable without pushing the policy into the general AST builder.

Assisted-by: OpenCode:openai/gpt-5.5:OpenCode
2026-10-03 14:52:35 +02:00
Siegfried Pammer e528a358ed Preserve VB WithEvents backing fields
AccessedThroughProperty-backed controls need to remain addressable as fields so WinForms initialization can use compiler-generated backing storage without hiding the setter that maintains WithEvents subscriptions.

Assisted-by: OpenCode:openai/gpt-5.5:OpenCode
2026-10-03 14:52:07 +02:00
Siegfried Pammer 3c608b7699 Reserve captured local names in declaring scope
Captured locals can first be named while visiting a nested local function even though their declaring ILFunction is the parent. Reserving such generated names only in the nested scope lets sibling captured locals reuse the same name and later collide when declarations are hoisted.

Assisted-by: OpenCode:openai/gpt-5.5:OpenCode
2026-10-03 11:24:48 +02:00
Siegfried Pammer 7236fd3495 Avoid invalid extension call in null propagation
Null propagation must not render an extension-method conditional access when normal member lookup would bind to a non-method member, because that produces invalid C# for ambiguous Count-style cases.

Assisted-by: OpenCode:openai/gpt-5.5:OpenCode
2026-10-02 07:24:33 +02:00
Siegfried Pammer 8b8af1e4f6 Fix UTF-8 literal span initializers
Recognize Roslyn's null-terminated backing-store shape for u8 literals so escaped control bytes decompile back to UTF-8 string literal syntax, fix LdStrUtf8 to infer ReadOnlySpan<byte> in both the template and generated code, and cover overload-resolution cases where the explicit span conversion must be preserved.

Assisted-by: OpenCode:openai/gpt-5.5:OpenCode
2026-10-01 07:35:23 +02:00
Siegfried Pammer f386a148b0 Merge pull request #4160 from icsharpcode/refactor/extension-method-syntax-in-callbuilder
Decide extension method syntax in CallBuilder
2026-09-29 07:45:01 +02:00
Siegfried Pammer 22ce4c4420 Handle required members on record copy constructors
Roslyn annotates record copy constructors with SetsRequiredMembersAttribute when records have required members. Treat that attribute as allowed only on constructor-owned synthesized record members so copy constructors are recognized and required init auto-properties do not leak backing-field accesses.

Assisted-by: OpenCode:openai/gpt-5.5:OpenCode
2026-09-27 14:22:32 +02:00
Siegfried Pammer d0987646bd Handle null expression-tree call arguments
Fix #4174 by bailing out of expression-tree call reconstruction when a converted argument cannot be built, and allow conditional operands that unify through an implicit reference conversion.

Assisted-by: OpenAI:gpt-5.5:OpenCode
2026-09-27 07:55:07 +02:00
Siegfried Pammer 8ea15eed4f Merge pull request #4182 from icsharpcode/fix/4180-negated-negative-constant
Fix #4180: Parenthesize negated negative constants
2026-09-26 21:27:42 +02:00
Siegfried Pammer d99de57d33 Fix #4180: Parenthesize negated negative constants
Unary minus over a negative literal printed as adjacent minus tokens, which C# parses as pre-decrement instead of numeric negation. Treat this literal shape as requiring parentheses so the output remains legal and preserves the IL semantics.

Assisted-by: OpenCode:openai/gpt-5.5:OpenCode
2026-09-26 20:11:56 +02:00
Siegfried Pammer a4324edef0 Merge pull request #4179 from icsharpcode/fix/decompilation-error-member-context
Include member context in decompilation error comments
2026-09-26 11:23:26 +02:00
Siegfried Pammer 453028f50f Include member context in decompilation error comments
The command-line summary already names the failing token and member, but the generated code only asked users to report a generic exception. Put the same member context into the emitted comment so copied snippets identify the failing method.

Assisted-by: OpenAI:gpt-5.5:OpenCode
2026-09-25 20:30:55 +02:00
Siegfried Pammer 8b183ab200 Recognize metadata property backing fields
Some assemblies preserve the C# backing-field naming pattern and accessor shape but omit CompilerGeneratedAttribute from the field. Use the existing property/backing-field metadata lookup so those properties can still collapse to field-backed or automatic property output instead of leaking <Name>k__BackingField.

Assisted-by: OpenCode:openai/gpt-5.5:OpenCode
2026-09-24 07:06:35 +02:00
Daniel Grunwald f7a1fb070e Merge pull request #4158 from icsharpcode/ldmembertoken
Invent a syntax for `ldtoken`.
2026-09-21 23:41:49 +02:00
Christoph Wille f226592bd8 Rename CLAUDE.md to AGENTS.md
AGENTS.md is the vendor-neutral name other coding agents already read,
and Claude Code now loads it through its built-in agents-md plugin
whenever a project has no CLAUDE.md of its own. One set of instructions
then serves every agent instead of only Claude. The plugin stands down
as soon as any CLAUDE.md remains in the project, so both files move
together rather than one at a time.

Assisted-by: Claude:claude-fable-5-1:Claude Code
2026-09-21 18:40:40 +02:00
Siegfried Pammer ffdbce6e35 Pin extension method call syntax with a Pretty fixture
Extension call syntax had no fixture of its own; it was covered only
incidentally by QualifierTests, Issue1080, RefLocalsAndReturns and a few
others. The cases that decide how the call is spelled - null receiver,
ref/in receiver, a named argument after the receiver, type arguments
inference cannot reach, params expansion, and an instance method that
beats the extension so the call has to stay static - now have one place
that fails if any of them moves.

Assisted-by: Claude:claude-opus-5:Claude Code
2026-09-21 16:57:39 +02:00
Siegfried Pammer 9b759705a9 Do not count the receiver as an argument of a method reference
An extension method reference binds its receiver as the target, so the
delegate it is converted to has one parameter fewer than the method.
PerformOverloadResolution prepends the target itself when extension
methods are allowed, so handing it the receiver's parameter as well left
it with one argument too many: every candidate came back
TooManyPositionalArguments, the reference was reported ambiguous, and the
escalation ran to the end and wrote out type arguments that inference
could have supplied.

Two fixtures pinned that output. Both spellings compile to byte-identical
IL - Use2(ints.Select) binds Enumerable.Select<int32, int32> and the same
Use2 overload as Use2(ints.Select<int, int>), and .Any is the same
Enumerable.Any<int32> as .Any<int> inside the expression tree - so the
shorter one is what the escalation is supposed to arrive at.

Assisted-by: Claude:claude-opus-5:Claude Code
2026-09-21 16:57:39 +02:00
Daniel Grunwald a5d6a38df5 Invent a syntax for ldtoken. Still not valid C# code, but at least it's now possible to see which member is referenced. 2026-09-20 15:57:36 +02:00
Siegfried Pammer ec901f3829 Merge pull request #4043 from icsharpcode/fix/3282-indexer-optional-arguments
Fix #3282: leave out trailing optional arguments in indexer accesses
2026-09-20 07:08:32 +02:00
Siegfried Pammer c9010c7121 Keep an index initializer's index, and only reorder nameable arguments
An index initializer is written as an element access even for a
parameterized property, which has no access syntax of its own, so
BuildDictionaryInitializerExpression routes one through the accessor
setter path regardless of symbol kind. The guard that stops an access
from dropping every argument only recognised indexers, so an index whose
argument repeats its declared default was left out, the element access
collapsed to a member assignment, and the placeholder target leaked into
the output. VB emits exactly this shape.

Which parameters carry the names, and which names can be written, were
decided in two places: NamedArgumentTransform admitted a reordering when
the accessor's parameters all had some name, while CallBuilder wrote the
names from the indexer's parameters and only when they were valid
identifiers. Where the two disagreed the reordering was kept and the
names dropped, leaving the arguments positional in the reordered order -
a silent change of meaning. Both now read one list under one predicate,
and a call whose names cannot be written is not reordered at all. The
value parameter is no longer consulted, since it is never named; COM
interop declares indexers that leave it unnamed.

The argument-list narrowing in Apply(CastArguments) is dropped again:
every step list runs NoOptionalArgumentAllowed first, so nothing is ever
left out by the time the casts go on.

Assisted-by: Claude:claude-opus-5[1m]:Claude Code
2026-09-19 19:28:32 +02:00
Siegfried Pammer fb780dcbcb Keep an argument an override redeclares a default for
An argument that repeats its parameter's default value may be left out,
but the value was only ever compared against the method the call
instruction names - for a virtual call the base declaration, since that
is the slot the compiler emits. The shortened form binds against the
receiver's static type, where an override is free to declare a different
default, and the recompiled code then passes that one instead. Calls have
had this since optional arguments were introduced; opening indexer
accesses to omission brought it to element accesses too.

Assisted-by: Claude:claude-opus-5[1m]:Claude Code
2026-09-19 17:00:05 +02:00
Siegfried Pammer e674361fbf Write named arguments for indexer accesses
C# allows named arguments in an element access, but NamedArgumentTransform
refused to introduce one for any accessor, so an access whose arguments the
compiler reordered came out as a temporary. Only indexers gain this: a
property access has no argument list, an operator cannot take names, and a
setter's value stays unnamed on the right-hand side, which also keeps it last
in argument order for everything that identifies it by position.

Introducing a name replaces the call with a block, so it is refused where the
surrounding instruction requires the call itself - a call-inline-assign block,
or the target of a compound assignment.

A name in an element access names a parameter of the indexer, which the type
system takes from the getter; the accessor being called may name the same
parameters differently. C# cannot declare that, but other languages can, so
the names are read off the indexer. The names also have to stop where the
written arguments do, which is now done once where they are built rather than
again inside the ambiguity check.

Assisted-by: Claude:claude-opus-5[1m]:Claude Code
2026-09-19 16:59:46 +02:00
Siegfried Pammer d31bea2106 Let an indexer access leave out trailing optional arguments
HandleAccessorCall had no way to express an omitted argument, so CallBuilder
asserted that none had been detected before it got there: any assembly
indexing through an indexer with an optional parameter hit that assert in a
Debug build, and Release wrote the defaults back out. An access now renders
through the same ArgumentList helpers as an ordinary call, and the steps that
disambiguate it can write the omitted arguments out again.

Two things had to reach that. The assigned value of a setter is the last
argument of the accessor call but not an argument of the access - the standard
adds it only for the invocation (12.6.2.1) - so it neither ends the run of
optional arguments nor is written out with them, and it is taken out of the
list before anything counts or casts the arguments. An indexer must also keep
one argument: dropping them all would turn the access into a property access,
which names a different member.

Readability names are turned off for an access. Rendering through
GetArgumentExpressions() would otherwise let them through, where master never
called it here, and `d[true]` would come out as `d[flag: true]` or, against an
override that renames the parameter, as `((Base)d)[flag: true]`.

Not covered: params indexers, [Optional] without a constant,
[DateTimeConstant]-style defaults, default(T) at a value-type instantiation,
and omitting a middle optional argument - the last of which plain calls do not
do either.

Assisted-by: Claude:claude-opus-5[1m]:Claude Code
2026-09-19 16:58:49 +02:00
Siegfried Pammer 1b2d77a07a Add discarded method-group cache test cases
Cover the method body shapes from the PR review and remove the extra field-load guard so discarded conversions are kept as allocations without preserving the cache check.

Assisted-by: OpenCode:openai/gpt-5.5:OpenCode
2026-09-19 13:11:51 +02:00
DualFroz 259e513829 Keep a discarded method-group conversion out of the delegate cache
CachedDelegateInitializationWithField only fired when the instruction
after the caching `if` read the cache field exactly once. A discarded
conversion -- `_ = (Action<int>)M;` -- reads it zero times: Roslyn emits
the null check and the store, and nothing else. The transform bailed out,
so the `<>O` cache class and its `<0>__M` field survived into the output
under names no C# compiler will accept.

Zero usages is now handled: the `if` is replaced by the delegate
construction it guarded, which is what the source expressed, and the
caching disappears with the field. The rest of the method is checked for
loads of the same field first, since only then is this `if` the sole
initialization.

Closes #3965
2026-09-19 11:14:31 +02:00
Siegfried Pammer 8faa1848d5 Close the test fixtures that hold an assembly open
A PEFile keeps its assembly mapped until it is disposed, and three fixtures
opened one and never did: DuplicateAssemblyReferenceTests both directly and
for every reference its resolver hands back, TypeForwarderResolutionTests for
everything the resolver probes, and LocationsInAstTests for the module behind
each decompiler. Their teardowns then deleted the directory those files live
in, which cannot work while the mapping is open, so a Windows run reported a
failed teardown for a run that otherwise passed. They now dispose what they
opened, the way WpfProjectExportTests already did.

Deleting is cleanup either way, so it goes through RepeatOnIOError rather than
straight at the file system, and that helper no longer rethrows: its last of
five attempts was deliberately unguarded, and four retries ten milliseconds
apart is a short window to wait for a handle that a virus scanner or the
indexer still holds. It now backs off to about a second and a half and, failing
that, reports and leaves the file behind, which costs nothing.

Assisted-by: Claude:claude-opus-5[1m]:Claude Code
2026-09-19 08:43:07 +02:00
Siegfried Pammer 16196731d3 Decide every qualifier with one rule
Whether a member reference has to name its target was decided in three places:
RequiresQualifier, and a copy each in the accessor and call paths. They
disagreed in two ways that both look like oversights.

Only the method group path failed to consult AlwaysQualifyMemberReferences.
That setting is on for the WinForms InitializeComponent method, which is also
decompiled with implicit method group conversion off, so every event handler
there came out as new EventHandler(Foo) while everything around it was
qualified.

A base target was the other, and the copies answered it three ways: never for
fields, always for accessors and method groups, and a virtual-only test for
calls. Dropping "base." leaves the reference dispatching virtually, which
reaches the same member unless the member can be overridden and the IL did not
dispatch virtually - so IsOverridable decides it, not the kind of reference.
Never is right only because a field access does not dispatch; always keeps
qualifiers that change nothing; and virtual-only is too narrow, since an
abstract or overriding member is dispatched virtually without carrying the
keyword. That last one drops the qualifier on base.OverridingMember, which
reaches this type's member instead of the base one the IL named, and the
escalation cannot catch it: with nothing overridden here the shorter spelling
binds to the same member and looks correct.

QualifierTests already covered base references, but only where the calling type
shadows or overrides the member, so the qualifier was always required and
over-removal could not show up. It now also covers a member overridden further
up the hierarchy. The spellings that change instead of round-tripping are
Ugly test cases.

Assisted-by: Claude:claude-opus-5[1m]:Claude Code
2026-09-19 08:42:52 +02:00
Siegfried Pammer 72dbe6f41d Merge pull request #4134 from dualfroz/fix-vb-exception-filter-type-test
Recognize a VB exception filter's type test
2026-09-15 20:56:26 +02:00
Siegfried Pammer 483a8661f6 Add VB tests for On Error and Try/Catch/Finally
VB lowers error handling differently from C# and no fixture covered it:
every Catch brackets its body with SetProjectError/ClearProjectError,
Catch...When moves that call into the filter, and On Error becomes one
try block with a dispatch switch driven by line and handler state. The
expected output is the current decompilation, rough spots included
(delegate-invoked filter blocks; gotos into the dispatch switch that
leave unreachable code, which keeps VBOnError.cs out of the
warnings-as-errors project), so improvements show up as diffs. IL offset
labels differ per compiler and optimization level, hence the per-method
#if variants. Legacy vbc output was verified on Windows.

The correctness test covers what a pretty test cannot: catching object
made the decompiled On Error code fail to recompile with CS0155, and the
round trip shows the rewritten dispatch code still runs the same. Like
the mcs configurations, the legacy and Roslyn 2.10/.NET Core 2.2 ones
recompile the decompiled VB code with the latest Roslyn: C# 5 cannot
express the exception filters VB error handling compiles to, and the
.NET Core 2.2 Microsoft.VisualBasic.dll lacks Information.Err and
ProjectData.CreateProjectError.

Assisted-by: Claude:claude-opus-5:Claude Code
2026-09-15 20:11:13 +02:00
DualFroz d5ff770959 Recognize a VB exception filter's type test
vbc builds an exception filter as a single non-short-circuiting
expression: `isinst`, then the user's `when` conditions, combined with
`and`. DetectCatchWhenConditionBlocks only knew the branch chain csc
emits, so the type test stayed inside the filter and the handler kept the
`object` variable it has in IL:

    catch (object obj) when ((obj is Exception) & (num2 != 0) & (num == 0))
    {
        ProjectData.SetProjectError((Exception)obj);

A catch type has to derive from Exception, so that does not compile.

The conjunction form is matched too now, lifting the test to the catch
type as the block form already does. The other conjuncts stop running for
a non-matching exception once the test moves, so the filter has to be
pure for this to be invisible; PropagateExceptionVariable then drops the
castclass in the handler:

    catch (Exception ex) when ((num2 != 0) & (num == 0))
    {
        ProjectData.SetProjectError(ex);

Closes #3659
2026-09-15 19:16:29 +02:00
Siegfried Pammer 85198271c9 Fix #3320: detect dynamic await in pre-Roslyn state machines
The C# 5 compiler loads the awaiter into a fresh local before every
dynamic call site and before the ICriticalNotifyCompletion type test, and
the early non-aggressive inlining refuses to fold those copies. Every
await matcher identifies an await by the identity of its awaiter
variable, so each copy hid the awaiter from the matcher and the whole
await stayed undetected.

Each such copy has one store and one load, and after the dynamic call
sites are collapsed that load sits in the next instruction, which is the
case ILInlining.InlineOne already handles, including the check that the
source is not overwritten first. Folding is restricted to the IsCompleted
and GetResult call sites and the completion-interface type tests, so
dynamic calls in user code keep their locals: a blanket fold retypes
unrelated locals of the surrounding method (an enum local decompiled as
int plus a cast).

Two smaller divergences from the Roslyn shape sat behind that one: the
merge block of the AwaitOnCompleted/AwaitUnsafeOnCompleted diamond
clears doFinallyBodies before its leave, and the awaiter is restored
from its object-typed field with unbox.any rather than castclass.

The ILPretty fixture is the state machine from the issue's assembly with
its external types stubbed out, so the fix stays guarded where the legacy
compiler is unavailable. The Pretty fixture for dynamic await now runs
the pre-Roslyn configurations as well, which is the end-to-end guard on
Windows; its expected output records that the compiler copies an awaited
dynamic value into a local first and emits no debug name for it. The
dead stores an optimized build leaves before a try block containing an
await are not specific to dynamic - a plain await on a Task produces the
same three - so they sit behind an EXPECTED_OUTPUT-only block.

Checked against real legacy csc output (/o- and /o+): dynamic await in
plain code, loops, try/catch, try/finally and using.

Assisted-by: Claude:claude-opus-5[1m]:Claude Code
2026-09-15 08:18:59 +02:00
Siegfried Pammer c1abc29765 Merge pull request #4091 from icsharpcode/split-obj-vt
Split StackType.O into StackType.Obj+StackType.VT
2026-09-13 21:45:49 +02:00
Daniel Grunwald e678321e00 Avoid unnecessary cast when comparing enum with 0 2026-09-13 12:57:19 +02:00
Daniel Grunwald c760a1d622 Fix "Remove redundant comp(... != 0)" to avoid changing the value
When an IfInstruction was created with resultType=bool, because only
values 0 or 1 are possible, then we need to preserve this property in
transforms -- otherwise the added correctness test would fail.
2026-09-13 11:57:46 +02:00
Siegfried Pammer d797d4abb4 TransformExpressionTrees: compare reference-constrained type parameters
Declining every type-parameter operand was too broad. The reason a type
parameter has no lambda spelling is that `v == other` is CS0019 and boxing
both operands compares box identity where the tree compares values - and both
only apply while the parameter may be a value type. A parameter constrained to
a reference type compares as a reference, which is what the tree asks for and
what `t == null` spells, so it converts like any other reference comparison.

IsReferenceType is the distinction the type system already makes here:
TypeUtils.GetStackType maps a type parameter to Obj or VT by the same
question. An unconstrained parameter answers null and keeps declining.

Assisted-by: Claude:claude-opus-5:Claude Code
2026-09-10 20:53:43 +02:00