Commit Graph
597 Commits
Author SHA1 Message Date
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
Daniel Grunwald f7a1fb070e Merge pull request #4158 from icsharpcode/ldmembertoken
Invent a syntax for `ldtoken`.
2026-09-21 23:41:49 +02:00
Siegfried Pammer e60b6fc372 Look for extension methods innermost namespace first
C# resolves an extension method one namespace at a time, starting at the
namespace the call is written in and working outwards through the
enclosing ones before it considers any import. A library that defines
GetCustomAttributes(this MemberInfo) next to its callers therefore beats
System.Reflection's identically shaped one, and the call compiles.

ExpressionBuilder's resolver had a flat scope: the namespace superset was
one undifferentiated list of imports, so the two competed as equals and
the call looked ambiguous. The deleted AST transform did not have this
problem because it nested its scope into the current type's namespace
before resolving anything.

The scope is memoized per namespace on the DecompileRun, because the set
of extension methods a scope can reach is computed once per scope and an
ExpressionBuilder exists per function.

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 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 a7f2a98f8b Share one escalation loop across the member-reference builders
Emitting a member reference starts from its shortest spelling, which in the
output's name-lookup context may bind to a different member than the one the IL
referenced. Six places re-resolved what they were about to write and, while it
did not bind back, escalated to a more explicit spelling and tried again: field
accesses, calls, constructor calls, accessors, and the two method group forms.
Each kept its own retry state, and each held targetResolveResult in sync with
requireTarget by hand.

Disambiguator owns that loop. A For* factory per form of reference builds one,
escalates and hands it back finished, naming the steps that form prefers and
whether it is written with invocation syntax - the one thing an IMethod does
not say, since a method group is not, a property access is not even though it
is a call in IL, and a parameterized property's accessor is despite being an
accessor.

Everything that answers "does this spelling still reach the member" moves with
the loop, including the two checks that are asked standalone, and the decision
to write type arguments up front for a method whose arguments cannot infer
them, which belongs next to the step that takes that guess back. What is left
in CallBuilder builds call syntax. CheckSimpleCall now takes its compilation
from the resolver rather than the type system; they are the same one.

The escalations are already named by ReferenceTransformation, which is also the
record of what has been applied, so the allowed-set gating becomes one test and
clearing a bit to stop a step repeating becomes unnecessary: one use spends a
step, which is also what ends the loop. Spent is tracked apart from what is in
effect for the single escalation that is not monotone, where a protected member
of a base type cannot be reached through a cast target and the qualifier has to
be dropped again.

Assisted-by: Claude:claude-opus-5[1m]:Claude Code
2026-09-19 08:42:37 +02:00
Daniel Grunwald 42d70f74e2 Do not transforms pattern further as if they were real instructions. 2026-09-13 19:52:36 +02:00
Daniel Grunwald e678321e00 Avoid unnecessary cast when comparing enum with 0 2026-09-13 12:57:19 +02:00
Daniel Grunwald fb9ff796da Fix stackalloc char[4] turning to stackalloc short[4]. 2026-09-06 20:13:54 +02:00
Siegfried Pammer 2100d53faa Take the span type from the array conversion's return type
MatchArrayInitializerFinal selects the operator by its return type, so
the block's result type is that return type. The declaring type only
happens to be the same one for the two array-to-span operators; Span<T>
also declares the conversion to ReadOnlySpan<T>.

Assisted-by: Claude:claude-opus-5:Claude Code
2026-09-06 10:51:07 +02:00
Siegfried Pammer 7112ef9521 Restore the params form for a span parameter
An array initializer standing in for a Span<T>/ReadOnlySpan<T> reaches the
call builder as an implicit span conversion over the array creation, a shape
the params expansion did not know, so a params span argument came out as the
array the compiler had built rather than as the argument list that was
written.

Assisted-by: Claude:claude-opus-5[1m]:Claude Code
2026-09-05 22:11:12 +02:00
Siegfried Pammer 5347b03ade Keep the localloc pointer in a stackalloc initializer block
The block addresses its allocation through the pointer localloc returns, and
only its result is a Span<T>. Retyping the initializer variable to the span
made every element store ask for a pointer it no longer had, which was papered
over with a conv from the span; once Obj and VT became distinct stack types
that conv had no conversion kind left and the whole method failed to decompile.

The span constructor becomes the block's final instruction instead, so the
element stores keep the pointer they were written against and the block still
evaluates to the span.

Assisted-by: Claude:claude-opus-5[1m]:Claude Code
2026-09-05 22:11:12 +02:00
Siegfried Pammer 3a3a79e88f Let an array initializer block evaluate to a Span<T>
The Span<T>/ReadOnlySpan<T> initializer patterns replaced their call with an
array initializer block, so an array stood where a span was expected: the
enclosing leave and any call taking the result saw StackType.Obj against the
StackType.VT the span type demands. The block now ends in the implicit
conversion the C# compiler applies, which is the one shape besides a bare
ldloc that an array initializer may take; the expression builder keeps the
conversion out of the output but not out of the expression's type.

Naming that operator wants a method looked up by signature rather than by a
predicate over the type's members, so MetadataModule grows a ResolveMethod
overload for it, sharing its signature matching with the metadata path.

Assisted-by: Claude:claude-opus-5[1m]:Claude Code
2026-09-05 22:11:12 +02:00
Daniel Grunwald 30552b01f7 Replace a couple of inst.ResultType -> IType conversions with InferType() calls. 2026-09-05 22:11:12 +02:00
Daniel Grunwald 790f4f60ee Second round of fixes. 2026-09-05 22:11:12 +02:00
Daniel Grunwald 3f76bd97fa Remove StackType.O; use actually different values for StackType.Obj and StackType.VT. 2026-09-05 22:11:12 +02:00
Daniel Grunwald 437d8fe2d2 Remove the value type special case from the expression builder's variable-type-replacement.
With the recent improvements, value type variables are guaranteed to already have the correct type in the ILAst, so we no longer need these hacks.
2026-09-05 22:11:12 +02:00
Daniel Grunwald c4e181505c Prepare for a split of StackType.O into StackType.Obj and StackType.VT: the easy cases 2026-09-05 22:11:12 +02:00
Daniel Grunwald 9377648cba Merge branch 'master' into split/unresolved-value-type-call-targets 2026-09-03 20:29:06 +02:00
Daniel Grunwald 5526400b17 Extend InferType() to every ILInstruction
Change InferType() to an abstract method and implement for every ILInstruction.
With this change, we now always have enough information to create a variable of an appropriate type to store the result of evaluating the instruction.
This previously was not the case for instructions producing "other value type", for which the stacktype-based fallback incorrectly produced `object`.
2026-08-30 21:25:45 +02:00
Siegfried Pammer 3e45005ebd Emit params and default values on lambda parameter lists
C# 12 allows both on the explicitly typed parameter list of a lambda, and
nowhere else: an anonymous method cannot declare either, and neither can a
lambda whose parameter list is about to be dropped. Guarded by a setting so
the output stays valid for earlier language versions.

Only what the anonymous function's own metadata declares is written. A lambda
may state a default the delegate does not have, a different one, or none where
the delegate has one, and reflection over the lambda's method reports what the
lambda declared - so filling either in from the delegate's Invoke would make
the recompiled assembly describe itself differently from the original. Call
sites are unaffected either way, because they bind against the delegate, which
still declares both.

Roslyn writes ParamArrayAttribute on the anonymous function's own method only
from version 5 on; before that it stands on the delegate type alone, where it
is not the lambda's to restate, so the fixture guards those cases on ROSLYN5.
The correctness test reads the metadata back through reflection, which is the
only place the difference is observable.

Assisted-by: Claude:claude-opus-5[1m]:Claude Code
2026-08-30 13:49:33 +02:00
sonyps5201314 d730ec0a20 Handle unresolved value-type call targets
A missing value-type definition makes ILReader insert a Ref-to-Unknown conversion before instance calls. Preserve the managed-reference receiver so C# output does not fall back to invalid ref casts or unsafe pointers.

Assisted-by: Codex:gpt-5.6-sol:Codex
2026-08-30 19:02:06 +08:00
Siegfried Pammer 19e702cc70 Merge pull request #4075 from icsharpcode/fix/3704-ldlen-infertype
Infer the C# type of ldlen
2026-08-30 12:21:40 +02:00
Siegfried Pammer 40b90766a1 Infer the C# type of ldlen
NullPropagationTransform only rewrites "x != null ? x.Chain : fallback"
into "x?.Chain ?? fallback" when the chain's inferred type is a
non-nullable value type, and InferType had no case for ldlen. Array length
therefore came back as UnknownType, so "arr?.Length ?? 0" was left as a
ternary.

The inferred type mirrors ExpressionBuilder.VisitLdLen, which decides
between Array.Length and Array.LongLength from the result type alone.

Found while investigating #3704, where the surviving ternary also keeps the
tested array in a stack slot and strands the typeof of a dynamic call's
static target. That issue is fixed separately in #4072, whose DynamicTests
cases pinned the ternary as expected output; those blocks round-trip
exactly now, so they are gone.

Also carries a review follow-up that missed #4072: the static-target test
in VisitDynamicInvokeMemberInstruction is a plain null check, the way
DynamicInvokeMemberInstruction itself tests the field, rather than a
pattern match binding a name it does not need.

Assisted-by: Claude:claude-opus-5[1m]:Claude Code
2026-08-30 11:55:42 +02:00
Siegfried Pammer e423cfb9ca Decide backing-field references by one verdict, not three
Three places decided independently whether a backing field would still be
declared, and they disagreed. ReplaceBackingFieldUsage rewrote a constructor
store into a property assignment whenever the property looked collapsible,
without asking whether PatternStatementTransform would actually remove the
declaration; ConvertField printed the "field" keyword on the FieldKeyword
setting alone, ignoring the GetterOnlyAutomaticProperties veto that
MemberIsHidden applies to the same field.

Two consequences, both silent. A setter-less property under
GetterOnlyAutomaticProperties = false kept its declaration and got "field" in
the getter anyway, so the keyword bound to a second synthesized field and the
declared one went unwritten. A settable property under AutomaticProperties =
false had its initializing store turned into a property assignment that
TransformFieldAndConstructorInitializers could no longer lift, leaving the
constructor in the output with an unconverted base-constructor call.

BackingFieldWillBeRemoved is now the single verdict every branch consults, and
it mirrors the transform's own entry gate. A property that keeps explicit
accessors is no longer addressed by name: the store stays a field reference and
becomes the property initializer, which is what field-backed storage means. The
one exception is a setter-less property's constructor store, which C# allows to
be written as an assignment and which has no other expressible form.

IsBackingFieldOfAutomaticProperty now answers through TryGetBackingField instead
of its own name check, so the two directions of "is this field that property's
storage" cannot diverge on staticness or field type. ReplaceBackingFieldUsage
dispatches on the resolve result rather than the identifier's spelling; after
ConvertField the same field appears both as "field" and under its metadata name
carrying the same annotation, so matching the name was matching the wrong thing.

The keyword's own precondition moves into a CanUseFieldKeyword local function.
Seven clauses with comment blocks wedged between them had to be read as one
expression; as one early return per rule the list reads in order.

Assisted-by: Claude:claude-opus-5[1m]:Claude Code
2026-08-30 11:48:44 +02:00
Siegfried Pammer 8ab3f93f5e Fix #3704: keep the static type of a dynamic call target
A dynamic call site that names a type passes typeof(T) as its target. When
a later argument contains control flow, the compiler stores that typeof in
a temporary. ILInlining does not undo this: it inlines a store only into
the immediately following instruction, so an intervening statement leaves
the typeof behind, and the expression builder printed the temporary instead
of the type. Substituting the typeof back into the target slot is not an
option either, because a call there has side effects and blocks inlining of
the remaining arguments.

The type is therefore stored on the instruction. Object creation already
did this, but kept the field private, so the expression builder re-derived
the type from the target argument and failed on the spilled form. Member
invocation gets the same field. Both drop the target from Arguments, so the
dead-argument handling removes the store; ArgumentInfo keeps its entry for
the target, because the invocation symbol is built from it.

The type of an object creation is not nullable: the transform returns
before constructing the instruction when it cannot match the typeof, and
substitutes the unknown type otherwise. An unresolvable type therefore
reaches the expression builder as the unresolved type it is, and prints as
`?` like any other, rather than being indistinguishable from a missing one.

Those two are also the only binder method kinds that ever see
CSharpArgumentInfoFlags.IsStaticType. Every other way of naming a type as
the receiver binds statically and leaves only a conversion of the dynamic
operand behind.

Assisted-by: Claude:claude-opus-5[1m]:Claude Code
2026-08-29 22:41:05 +02:00
Daniel Grunwald 5670bfc132 Stack slots holding value types previously nevertheless often had type object
This was due to StackType.O doing double-duty as `object` and `other`.
While ExpressionBuilder would often improve the type of such locals, the `object` nevertheless ended up used in a couple of places, e.g. via the `typeHint`. This could result in value types being boxed even though the original IL didn't contain any `box` instruction.

This is an attempt to use better types for stack slot variables created by ILReader. The idea is: there aren't many IL instructions that produce "other" value types, and `InferType()` already handles pretty much all of them, so we can use that to assign types to our stack slots.

It's a bit more tricky if the stack is pushed to on multiple branches that join together before the value is used: here the variable type must be suitable for both assignments. In this case, we go back to the previously-used stacktype.
2026-08-29 15:21:08 +02:00
Daniel Grunwald 5d9cdce94c We need to properly compare the C# type, not just the StackType, otherwise our implicit conversions might end up converting to the wrong type.
This also matters in the `1 => DateTime.Now, 2 => null` case -- BestCommonType infers `DateTime` here, but we need `DateTime?` instead. But both had `StackType.O` so this went wrong prior to this commit.
2026-08-28 21:15:15 +02:00
Daniel Grunwald 1a784e2fa9 Fix throw switch-expr; by specifying the typeHint for throw. 2026-08-28 09:19:11 +02:00
Daniel Grunwald ca24b8f3af Fix #3683: insert casts in switch expression to ensure a best common type exists. 2026-08-27 23:14:42 +02:00
Christoph Wille f2b80df148 Look through the box when checking an await's GetAwaiter
An operand boxed for the GetAwaiter call is typed 'object', so the member
lookup that decides whether the await needs a cast finds nothing and a
redundant cast to the receiver type reaches the output. C# inserts that boxing
conversion implicitly, so the box may be dropped -- but only after the lookup
confirms the unboxed operand still binds the same GetAwaiter, and only via the
resolve result: UnwrapChild detaches the operand from the AST, so running it
speculatively leaves a cast with no child behind and decompilation of the whole
method falls back to the raw state machine.

Assisted-by: Claude:claude-opus-5[1m]:Claude Code
2026-08-18 21:36:46 +02:00
Daniel Grunwald e530b1c1b6 Avoid allowImplicitConversion: true for await expressions -- there's no target type that the C# compiler could convert to.
Instead, use `IsAppropriateCallTarget` to detect whether an explicit cast is necessary for calling the correct `GetAwaiter` method.
2026-08-18 21:36:45 +02:00
Sebastien Lebreton e7bed9c78e Use pattern expression instead 2026-08-16 08:38:07 +02:00
Sebastien LebretonandCopilot b1e5163873 Fix #3894: keep lambda parameter typing consistent
An anonymous parameter type cannot be named, so the original lambda must
have used implicit parameters throughout. Emit the whole parameter list
implicitly instead of mixing implicit and explicit declarations.

Assisted-by: Copilot:gpt-5.6-sol:GitHub Copilot CLI
Co-authored-by: Copilot <[email protected]>
Copilot-Session: 86d2918e-5a24-48b4-9a86-41d331ec3720
2026-08-16 08:38:07 +02:00
Siegfried Pammer 63d03f1b49 Fix #1142: recover enum members for out-of-range constant operands
An addition or subtraction on an enum whose constant operand's numeric
value does not fit the underlying type (an int constant standing for a
high uint member, e.g. -501 for 0xfffffe0b) failed to resolve as enum
arithmetic and fell back to integer arithmetic with casts, producing
'(uint)((int)value - -501)' and, for the same source expression in an
argument position, '(uint)value - 4294966795u'. Retry the failed
resolution once with constant operands reinterpreted in the enum type;
the reinterpretation is lossless whenever the constant's stack type
matches the enum's underlying stack type, because the IL constant is
the member's bit pattern. Valid non-enum resolutions like 'data - 1'
(enum minus underlying, yielding the enum) are unaffected because the
retry only runs when the plain resolution fails.

Assisted-by: Claude:claude-fable-5:Claude Code
2026-08-15 18:57:38 +02:00
Daniel Grunwald a0b30a6902 Merge pull request #4006 from icsharpcode/refactor/usingscope-to-typesystem
Tidy up the resolve-result hierarchy and document the tuple-cast rule
2026-08-15 16:28:50 +02:00
Christoph Wille e475e26443 Make ThisResolveResult an ILVariableResolveResult of the this parameter
'this' and 'base' both read the 'this' parameter of the function being
decompiled, but their resolve result did not say so: consumers that key on
ILVariableResolveResult (local-reference output, highlighting, hover) could
not connect the keyword to the variable, and the qualified/unqualified
spellings of the same access carried differently shaped annotations.

The resolver has no ILFunction and thus no variable to put into a
ThisResolveResult, so it stops synthesizing one: LookInCurrentType looks
the name up against the (self-parameterized) current type, which grants
the same protected access, and the annotation of an unqualified field
access is built from the translated target instead. ResolveThisReference
and ResolveBaseReference had no callers left and are removed.

Assisted-by: Claude:claude-fable-5:Claude Code
2026-08-15 13:17:49 +02:00
Siegfried Pammer 48fcf0313e Move UsingScope and CSharpTypeResolveContext into the type system
Both types are consumed well outside the C# output layer - DecompileRun
carries the using scope, and the IL transforms build a resolve context from
it - so living in ICSharpCode.Decompiler.CSharp.TypeSystem misrepresented
where they belong and forced a C#-specific namespace import on every
consumer.

Assisted-by: Claude:claude-opus-5[1m]:Claude Code
2026-08-15 11:03:10 +02:00
Siegfried Pammer 3da129632e Fix #3624: decompile semi-auto properties with the C# 14 field keyword
Backing-field references inside a property's own get/set/init accessors
are emitted as the `field` keyword at IL-to-AST translation time
(ExpressionBuilder.ConvertField), so arbitrary accessor bodies become
expressible and no separate rewrite pass is needed. Compiler-generated
trivial accessors then collapse individually to `get;`/`set;`, which
also handles mixed shapes like `{ get; set { ... field ... } }`; the
backing-field declaration is removed with its remaining attributes
re-hosted as `field:` sections, and constructor stores become property
initializers (or property assignments, for setter-less properties).
Implicit zero-stores that auto-default struct constructors emit for
unassigned backing fields are dropped rather than lifted.

Recognition stays AST/metadata-based rather than mirroring the ILAst
analysis used for automatic events: events must prove compiler-generated
bodies before discarding them, while the field keyword discards nothing,
so name association plus the accessor context is sufficient.

Below C# 14 (or with the new FieldKeyword setting off), the field
declaration survives under its metadata name, so the UI keeps showing
the truth; EscapeInvalidIdentifiers - the transform the compilable-output
flows (project export, VS, tests) already add - now maps
`<P>k__BackingField` to the readable `P__BackingField` instead of the
generic character escape. A genuine field literally named "field" is
qualified as `this.field` inside accessors, and locals are not named
"field" there, since C# 14 rebinds the bare identifier. Bodiless
accessors mixed into multi-line properties get their own line in the
output.

The fixture covering the feature surface lands with the implementation rather
than as a separate xfailed commit. It is excluded from the test-assembly
compilation because its nullable annotations would trip warnings-as-errors
there.

Assisted-by: Claude:claude-fable-5:Claude Code
2026-08-13 13:15:21 +02:00
Siegfried Pammer 60c08fcb74 Keep 'delegate {}' when a parameter type is inaccessible
The parameter-list-less anonymous method form is compatible with any
delegate signature, and C# code must rely on exactly that when a
delegate's parameter types cannot be named at the use site: IL, unlike
C#, permits a delegate signature to reference less accessible types.
Expanding such an anonymous method into a lambda would force the
unnameable type into a parameter list. Keep the delegate form, with its
parameter list dropped, when the parameters are unused and one of their
types is not accessible from the current context.

Assisted-by: Claude:claude-fable-5:Claude Code
2026-08-13 10:54:52 +02:00
Siegfried Pammer 3cc49044ec Extend UseLambdaSyntax to emit statement lambdas
Under UseLambdaSyntax, anonymous functions became lambdas only when an
expression body was possible; statement-bodied ones kept C# 2 delegate
syntax. Now every anonymous function whose parameter shape a lambda can
express uses lambda syntax; delegate syntax remains for ref/out/in and
params parameters and for pre-C# 3 language profiles.

Two latent issues surfaced by the wider lambda coverage: DeclareVariables
assumed an insertion point directly under a LambdaExpression is an
expression body it must convert to a block, which block-bodied lambdas
now violate; and anonymous methods declared without a parameter list
carry compiler-generated parameter names like '<p0>' that are not valid
identifiers, so the lambda's mandatory parameter list regenerates such
names from the parameter type: (object obj, EventArgs e) => ...
A side effect visible in fixtures: an explicit parameter list can make
a delegate-creation cast redundant that bare 'delegate' syntax needed
for overload resolution, e.g. new Thread((ThreadStart)delegate { })
becomes new Thread(() => { }).

Assisted-by: Claude:claude-fable-5:Claude Code
2026-08-13 10:54:52 +02:00
Siegfried Pammer 13c6cc6741 Give an explicit this the same resolve result as base
The guard added here reads the target of a member access to decide whether an
assignment may move into a field initializer, and it recognises the current
instance as a ThisResolveResult. Only one of the two spellings produces that.
An unqualified `A` is resolved through CSharpResolver.LookInCurrentType, which
synthesizes the target as a this-reference; an explicit `this.A` is built by
ExpressionBuilder, whose TranslateTarget hands back whatever ConvertVariable
produced - and `this` is a parameter like any other there, so the target is an
ILVariableResolveResult. The guard saw the first and missed the second.

Which spelling appears is decided by RequiresQualifier, for reasons unrelated
to the question being asked: a constructor parameter that shadows the field
forces the qualifier, and AlwaysQualifyMemberReferences forces it everywhere.
So the transform hoisted `b = this.value + 1` into a field initializer, where
naming the instance is CS0027 and the output does not compile.

TranslateTarget already builds a ThisResolveResult for `base`, one branch
above. Doing the same for `this` leaves the guard untouched and makes it see
both spellings, and spares every future consumer the same trap. The type is
carried over from the previous resolve result, so nothing downstream observes
a different one - the this/base keyword links read exactly this node.

Fixes #3984.

Assisted-by: Claude:claude-opus-5:Claude Code
2026-08-12 09:24:38 +02:00
Siegfried Pammer f8617a0c3d Print 0m instead of default(decimal) for zero decimal values
decimal has no IL literal: legacy csc compiles 0m to a Decimal.Zero
field load, which already decompiled to the literal, but Roslyn
compiles it to a zero-initialization, which decompiled to
default(decimal). The two forms are bit-identical for decimal, so the
literal is no less faithful to the IL and matches what a human writes;
it also removes the per-compiler split in the CompoundAssignmentTest
fixture.

Assisted-by: Claude:claude-fable-5:Claude Code
2026-08-03 23:17:13 +02:00
Siegfried Pammer 716d7f6a5b Fix #3803: Crash on nested deconstruction of custom structs
Optimized code stores no temporary for a deconstruction element that is
used only once after the deconstruction. MatchAssignments handled that
for trailing elements, but a nested deconstruction copies the inner
element to a temporary, so the elements preceding it are also left
without an assignment; their external load then violated the
DeconstructInstruction invariant that all pattern variable loads are
descendants of the instruction. The forwarding fixup now covers all
unassigned elements and inserts in pattern order, because the statement
and expression builders pair pattern variables with assignments
positionally. This also fixes the nested tuple deconstruction crash
reported in #3388.

Also unwrap the address of the tested operand in
VisitDeconstructInstruction: deconstructing a struct passes the
receiver by reference, which was emitted as an invalid cast,
'var (x, y) = (S)(ref s);', even without nesting.

Fixes #3388.

Assisted-by: Claude:claude-fable-5:Claude Code
2026-08-02 15:54:27 +02:00
Siegfried Pammer 753f1d2481 Fix #1924: don't undo the compiler's &&-to-& optimization
ExpressionBuilder printed bitwise & / | on booleans as && / || whenever
the right-hand side was pure, but Roslyn lowers && / || to & / | only
when the right operand is a bare local or parameter read
(LocalRewriter.MakeBinaryOperator, unchanged since 2014). Shapes like
(c == 'a') | (c == 'b') can therefore only originate from a bitwise
source operator, yet were shown as short-circuiting. Per the discussion
in #1545, show the operator the IL actually uses instead of guessing the
source form: the reversal is dropped entirely, so Roslyn-compiled
"a && b" now decompiles to "a & b", which recompiles to the same IL.

Assisted-by: Claude:claude-fable-5:Claude Code
2026-07-23 18:51:52 +02:00
Siegfried Pammer a926ae7e8b Show a hover for the indexer of a dynamic index access
A dynamic index access (a[b]) gave its IndexerExpression a
DynamicInvocationResolveResult with no symbol, so the brackets carried no
tooltip. Synthesize an indexer (FakeProperty, IsIndexer) on the target
type with the index parameters typed from the callsite delegate, and
attach it. Route it hover-only by detecting a DynamicInvocationResolveResult
directly on the node - which also covers an invoke-member's own
parentheses, so those stop producing a dead navigation link too.

Assisted-by: Claude:claude-fable-5:Claude Code
2026-07-23 06:32:02 +02:00
Siegfried Pammer 4f9014fd71 Show the type parameter list on generic dynamic member tooltips
A dynamic invocation with explicit type arguments (a.Method<int>())
synthesized a non-generic fake method, so the hover showed Method(...)
without the generic list. Give the fake a matching set of conventionally
named type parameters (T, T2, ...) and specialize it with the actual
arguments, as a real generic call produces a SpecializedMethod, so the
hover shows the method's generic parameters.

Assisted-by: Claude:claude-fable-5:Claude Code
2026-07-23 06:32:02 +02:00
Siegfried Pammer 325494558b Show a hover for the constructor of a dynamic new expression
A dynamic object creation (new T(b) where an argument is dynamic) gave
its ObjectCreateExpression only a plain ResolveResult(T), so neither the
type name nor the parentheses referenced a constructor - unlike a normal
new expression, whose CSharpInvocationResolveResult lets both hover the
ctor. Synthesize a constructor on the created type (parameters typed from
the callsite delegate) and attach it the same way. Since it has no
metadata, route it hover-only, like the other dynamic members, so the
type name's existing navigation is not replaced by a dead link.

Assisted-by: Claude:claude-fable-5:Claude Code
2026-07-23 06:32:02 +02:00
Siegfried Pammer 1c010a828e Give dynamic member accesses a symbol for hover tooltips
Dynamic member accesses and invocations carried only the member name
(DynamicMemberResolveResult / DynamicInvocationResolveResult), so
GetSymbol returned null and the editor emitted no reference or hover.

Synthesize a member on the target type - a dynamic field for a member
access, a dynamic-returning method for a member invocation - named after
the accessed member and typed from the callsite delegate: each argument
uses its recorded compile-time type when the binder set one (statically
typed or constant arguments), dynamic otherwise, and the declaring type
comes from the receiver's argument info. Route these through GetSymbol;
TextTokenWriter and the hover renderer already turn an IEntity into a
tooltip. The synthesized members have no metadata token, so they render
a signature on hover but are not navigation targets.

Assisted-by: Claude:claude-fable-5:Claude Code
2026-07-23 06:32:02 +02:00
Siegfried Pammer 461df88572 Replace automatic-event backing-field references during translation
PatternStatementTransform renamed backing-field identifiers to the event
after the fact, keyed on the metadata name association alone: references
belonging to an event that is not actually automatic were still renamed,
binding them to a custom event that is unusable as a value (the same
defect class as #3858), and the rename bypassed the resolver checks, so
qualifiers were computed for the hidden field instead of the printed
event. ExpressionBuilder.ConvertField now performs the substitution,
keyed on the AutoEventDecompiler verdict whose memo moves into
DecompileRun so that member hiding, the event declaration, and reference
translation all decide from one analysis. Checking the verdict's field
identity also keeps same-typed sibling events apart (#3575), and the
qualifier logic running against the event drops spurious this./type
qualifiers from raise sites.

mcs 2.x accesses a sibling automatic event's backing field directly
inside custom accessors instead of calling the accessor, so the fixture
expects the resulting Delegate.Combine form there.

Assisted-by: Claude:claude-fable-5:Claude Code
Claude-Session: https://claude.ai/code/session_01Btdypgm8utyxqt1Etn2BDi
2026-07-18 21:56:16 +02:00