HandleConditionalOperator collapses `if (c) a = x; else a = y;` into a
conditional operator, innermost first, and keeps going for as long as the
chain does. A source else-if ladder therefore comes back as one expression,
however long it was: the sample in #2027 decompiles to a single
2095-character statement, and one NLog method to 2230 characters nested 29
brackets deep.
ExpandNestedConditionals undoes that past one level, so a statement keeps at
most a single conditional operator.
It runs at the end of the pipeline rather than inside ExpressionTransforms,
because every transform that needs its input to be a single expression has
to see the collapsed form first: object and collection initializers, `with`,
switch expressions, interpolated string handlers, and the query lambdas the
C# stage later rewrites into clauses. Cutting the chain earlier leaves an
if-else between the statements they pattern-match on and they silently stop
matching - an object initializer assigning an init-only member then does not
even compile. The same reasoning ReduceNestingTransform gives for walking
back ConditionDetection's aggressive else-inlining once the structure is
settled.
A chain already stored to a variable is expanded into that variable, so
nothing has to be decided: the variable carries its own type. A chain in any
other position - an argument, a return value, a field store - has nothing to
expand into, and ILExtraction can give it one. The temporary ILExtraction
creates is typed from the stack type though, where `I4` is `int`, `bool`,
`char` and every enum at once, so extracting on that basis turned a bool
into `int num` with `if (num == 0)` and an enum into
`dbType = (IsFixedLength ? 22 : 0)`.
InferExpectedType is the counterpart to InferType that answers this: where
InferType asks what a value is, it asks what the position the value flows
into says it should be - a parameter, a return type, a field, all of which
carry their type in metadata. Extraction is done only where that question
has an answer, and the temporary is typed from it. The receiver of a call
then reads `XPathNavigator xPathNavigator`, not `object obj` with a cast
back, and a field store keeps its enum's member names.
The Pretty fixture covers what must NOT change: an array initializer, a
query lambda, a ref local, a switch expression, an object initializer with
an init-only member, a `with` expression, a catch-when filter and both
constructor-initializer forms. The positive case is an ILPretty test,
because a Pretty fixture is its own input and expected output, and a chain
that round-trips through collapse and expansion has no fixed point there.
The PdbGen test records the cost in breakpoints: the compiler's single
sequence point for the collapsed statement becomes one per expanded
statement, which is inherent to splitting a statement in two.
#2027
Assisted-by: Claude:claude-opus-5:Claude Code
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
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
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.
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.
Twenty shapes that no fixture covered: nesting at depth three and at position
zero, inner discards on either side, property and no-conversion targets, and
deconstruction inside try, switch, if/else and while. All but one already
decompile correctly - they are checked in so a future change to
DeconstructionTransform cannot silently drop them.
The one that does not is left commented out with a pointer to #4059 rather than
as a red test: two back-to-back deconstructions share their out-slot
temporaries, and neither is recognized.
#4059
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
Making a conversion implicit by unwrapping it hands the operand to a
different target type, and a default literal takes its value from that
type: "S? x = new S?(default)" holds a value, while "S? x = default" is
null. Unwrapping the nullable constructor around a shortened literal
therefore turned "S? x = default(S)" into a null nullable. The literal is
spelled out again whenever unwrapping moves it to a type other than the one
it was shortened from.
Converting a using resource to the declared variable type is unconditional
now (except when the declaration says "var", which supplies no type): the
declaration always spells the type out, so any conversion to it may stay
implicit, which is also what shortens default(T) there.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
Shortening default(T) is the same problem as removing the redundant cast
around a lambda whose delegate type the context already fixes, so it uses
the same mechanism: ConvertTo makes the explicit type implicit when the
conversion is an identity conversion and the caller allows an implicit
one. The literal keeps the type it was shortened from, so any later
conversion to a different type - or any context that requires an explicit
type, such as an overload resolution recheck falling back to CastArguments
- can spell default(T) out again. That keeps the value intact where the
bare literal would change it, e.g. "object o = default(SomeStruct)", which
boxes a non-null struct while "default" would be null.
Because the shortened literal resolves to DefaultLiteralResolveResult,
CallBuilder's existing overload resolution recheck sees a real default
literal and rejects ambiguous calls on its own; no separate bookkeeping
about which arguments may stay untyped is needed. Only the contexts that
supply no target type at all restore the explicit form: an awaited
expression, and arguments of operator methods, which later become operator
or cast syntax rather than calls.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
There's an additional local variable when decompiling the non-optimized code; and explicitly putting that variable
into the test case just makes it fail due to yet another additional variable.
They were split out only because they were failing; there is no reason to keep
a second fixture now that they pass. Folding them in also widens their coverage
from roslyn4OrNewer to every defaultOptions config -- legacy csc, Roslyn 1.3.2
onwards and the net40 targets -- with the 'in'-receiver extension gated on CS72
because that one needs C# 7.2. IAwaitable and ClassAwaitable were declared
identically in both files and collapse into one declaration.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
The fixture was written as a spec of nine await shapes that decompiled to code
that does not compile. Six no longer do. Of the rest, default(Task) was never a
defect -- it compiles to the same ldnull as (Task)null, so the two are
indistinguishable in IL and the cast is a correct decompilation. The three real
ones are unrelated to the await conversion and have no correct output to pin
yet, so they move to #4017, #4018 and #4019; what stays behind is a regression
test for the shapes where the cast in front of the operand is load-bearing.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
The await surface had almost no fixture coverage beyond Task/ValueTask: every
GetAwaiter in the corpus was an instance method on the awaited type itself, so
the conversion VisitAwait applies to the operand was never exercised for an
inherited, interface-typed or extension-method awaiter. Probing that surface
turned up eight defects, all of which produce C# that does not compile.
AsyncAwaitPatterns pins the shapes that do round-trip, along the three axes the
translation actually depends on: the GetAwaiter receiver, the operand
expression, and the context the await sits in. Its Correctness twin pins what
Pretty cannot see - copy semantics of struct awaitables and the evaluation
order around the suspension point.
AsyncAwaitPatternsBugs is the spec for the defects, written as the C# that
ought to come out, with the current wrong output named per member. It fails
today; that is the point, and fixing a defect is meant to delete a comment
rather than edit an expectation.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
The preferred-scale lookup reaches well past the byte-normalization cases it was
added for: any value that is exactly n/2^k for a k the old denominator limit
could not reach now prints as a fraction, so constants that used to be short
exact decimals changed shape. That is the widest-reaching part of the change and
nothing pinned it.
The added constants are those values, including the unreduced 126 / 1024 that a
lowest-terms rewrite would turn into 63 / 512, plus two that must keep their
decimal form so the length gate stays covered from both sides.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
I think this isn't reliable enough yet to actually omit parameter types for lambdas (it only protects against switching to the wrong overload; not against type inference failures); so for now it's only used in the query expression transform.
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 <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 86d2918e-5a24-48b4-9a86-41d331ec3720
A negative constant operand is usually the two's-complement rendering
of a bit mask or a high unsigned value (an enum member, a sentinel);
the IL view now appends the hexadecimal form as a comment, e.g.
'ldc.i4 -501 // 0xfffffe0b' (#1142). The short forms stay bare: their
operand range is readable as-is. The disassembler round-trip comparer
strips comments, so the new NegativeConstants case pins the rendering
with explicit content asserts.
Assisted-by: Claude:claude-fable-5:Claude Code
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
The remaining parameter-modifier dimension of the C# 14 rules: an expanded
params call prefers the params ReadOnlySpan overload over params array (the
C# 13 better-params-collection rule) while the normal form keeps the exact
array overload; and a span conversion never binds a ref or out parameter,
so a keyword-less argument picks the by-value overload and the ref/out
keyword must survive decompilation to keep binding the by-ref one. All
expectations come from compiling probes (the negative directions are
CS1503) and everything was green as written - these are lock-downs, not
fixes.
Part of #829.
Assisted-by: Claude:claude-fable-5:Claude Code
Compiling probes with the C# 14 compiler establishes the matrix: a value
argument binds to an 'in ReadOnlySpan<T>' parameter with and without the
span conversion (a temporary is created), while an explicit 'in' argument
demands the parameter's own type (CS1503); and for a by-value/'in' overload
pair, a call without 'in' picks the by-value overload - also through the
span conversion - while 'in' at the call site makes the in-overload the
only candidate (CS1615 with a conversion).
The fixture pins the decompiler side of the same matrix: 'in' must survive
decompilation where it disambiguates the overload pair, and the folded
span-conversion argument must re-resolve to the by-value winner. The
resolver unit tests pin applicability and betterness directly. All of these
were green as written - they fence the implicit-in cast stripping and the
recheck ladder against regressions rather than fixing a defect.
Part of #829.
Assisted-by: Claude:claude-fable-5:Claude Code
The C# 14 compiler lowers implicit span conversions to calls -
MemoryExtensions.AsSpan(string), ReadOnlySpan<T>.CastUp, and the span
op_Implicit operators - so decompiled code showed the lowered form even
though the conversion and betterness layers already implement the C# 14
rules. CallBuilder now folds those helper calls back into conversions,
riding the existing mechanism: the conversion is built as an explicit
cast, consumption sites make it implicit where the context allows, and
the overload-resolution recheck re-adds a cast when the bare argument
would bind to a different overload (which canonicalizes deliberate
AsSpan disambiguations to the equivalent explicit span cast).
Span conversions compose, so CastCanBeMadeImplicit lets a direct
input-to-target span conversion replace a chained pair; and an rvalue
bound to an in parameter gets the same chance to shed the cast as a
by-value argument, since ChangeDirectionExpressionTo bypasses the
by-value strip.
Part of #829.
Assisted-by: Claude:claude-fable-5:Claude Code
The green FirstClassSpanTypes fixture pins overload-resolution behavior
the decompiler already gets right under the C# 14 implicit span
conversions: calls picking the new betterness winners (ReadOnlySpan
over Span/object/IEnumerable, ReadOnlySpan<string> over object[] and
ReadOnlySpan<object>, MemoryExtensions.Contains over
Enumerable.Contains) round-trip as plain calls, while calls picking the
losing overload keep their disambiguating casts and Enumerable.Contains
stays in static call form. Extension methods on span-convertible
receivers, generic inference through span conversions, params
betterness, and array-to-span returns are covered too. All winners were
verified by executing probes compiled at LangVersion 13 vs 14.
The FirstClassSpanConversions fixture is Assert.Ignore'd (#829): it
specs the desired folding of compiler-emitted span-conversion helpers
back into implicit conversions - MemoryExtensions.AsSpan(string),
ReadOnlySpan<T>.CastUp for span variance, and covariant-array/in-arg
conversions - which the decompiler currently renders as explicit helper
calls or casts (recompilable and semantics-preserving, just not
minimal). Both roslyn-latest configs compile the fixture and fail only
at the output comparison.
Assisted-by: Claude:claude-fable-5:Claude Code
Below C# 7 ref locals are unavailable, so CopyPropagation is allowed to copy
LdFlda/LdElema. When such a copy lands in a StObj target slot whose value is
impure, it violates the invariant checked by StObj.CheckTargetSlot: C# computes
the value to be stored before dereferencing the target, so the exception moves.
ILInlining resolves the same conflict by marking the address as delayed rather
than falling back to a ref local; copy propagation now does the same, which
keeps the generated code unchanged and only repairs the IL.
Unlike inlining, copy propagation has no third arm to fall back to: by the time
DoPropagate runs, the defining store is about to disappear, so every load has to
be replaced and refusing the copy is no longer an option. That decision can only
be made up front, which is what CanPerformCopyPropagation does when ref locals
are requested. The assertion covers the remaining hole, the public Propagate()
entry point, which bypasses that check -- AsyncAwaitDecompiler copies an ldflda
of the builder field through it irrespective of the setting.
Propagating an address also un-inlines its arguments into fresh stack slots, and
those stores are copy-propagation candidates in their own right. They are
inserted before the store being replaced, so the block scan used to step right
past them: a slot loaded more than once could never be inlined back and survived
into the output as a ref local -- for `s.ShortField >>>= 5` the copied ldflda
left behind `stloc C_0(ldloca s)`, printed as `ref CustomStruct reference = ref
s`. Rewinding the scan to the first of those stores lets them propagate too.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
Structs whose assembly is missing decompile through the unresolved-type
path, which is not covered anywhere: the fixture pins the constructor
shapes that path has to recognize, and the reference-type cases that must
keep falling through to a plain call.
Assisted-by: Claude:claude-opus-5:Claude Code
A writable ref-struct argument can receive narrower values through regular ref/out calls, and a ref-return can expose the same storage for field mutation. Treat those paths like receiver captures so inferred declarations remain compilable.
Assisted-by: Copilot:gpt-5.6-sol:GitHub Copilot CLI
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5d30b7a7-983d-4efa-8d99-fbface5828dc
Comments used to be child nodes flushed by InsertSpecialsDecorator when the
next node started printing, which put the marker of an init-only setter right
after the keyword. In the slot AST comments are leading/trailing trivia, so the
accessor's trailing trivia moved the marker behind the accessor body. The
placement cannot go back to trivia on the body either: the auto-property
transform drops the body, and the marker with it. The accessor now carries the
init-only fact itself and the printer writes the marker next to the keyword.
Assisted-by: Claude:claude-opus-5:Claude Code
Local scopedness is erased from IL and PDBs, so it can only be recovered
from the body. Compare each declaration initializer with later assignments,
field stores, and receiver captures using the C# 11 ref/value escape rules,
and emit scoped only when a later operation is strictly narrower.
Assisted-by: Copilot:gpt-5.6-sol:GitHub Copilot CLI
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 86d2918e-5a24-48b4-9a86-41d331ec3720
Overload resolution reports no error for an empty candidate set - there is
no best candidate to attach one to - so the null result passed for success
and was dereferenced while checking the call target. Decompiling
FSharp.DataFrame from nuget.org crashes that way: F# compiles its comparison
members to instance methods carrying operator metadata names, and the
operator candidate search looks at the operand types rather than at the
receiver type the member belongs to.
The new fixture pins that such an assembly decompiles at all. It still
renders those instance methods as operators and drops the receiver at the
call sites, which is the misclassification behind the empty candidate set
and is handled separately; this is the guard that keeps an empty candidate
set from being read as a resolved call.
Assisted-by: Claude:claude-opus-5:Claude Code
Two paths generate a local named "field" inside an accessor - a local
typed after a class named Field, and one named after the GetField method
it is assigned from - and C# 14 rejects that identifier there (CS9273).
Worse than the compile error, the local shadows the keyword, so a
backing-field read silently becomes a read of the local; both paths now
have a fixture, verified to regress without the name reservation.
A static member named "field" cannot be disambiguated with "this.", so
the accessor has to name its declaring type; that path had no coverage
either.
Assisted-by: Claude:claude-opus-5:Claude Code
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
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
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
Element 8+ of a long tuple is read through the Rest field, which Roslyn
loads by value; ILSpy turned that into an addressof over the loaded
copy, hiding the tuple field chain from every downstream matcher. Elide
the copy when the enclosing expression only reads through it - the read
then goes directly through the original address and folds into the
usual Item_N chain. The deconstruction transform's use-shape guard also
learns to walk that chain; whether each read really is a consumable
element access remains the job of MatchTupleElementRead and the escape
check.
Assisted-by: Claude:claude-fable-5:Claude Code
Deconstruction assignment copies the right-hand side into a temporary
before calling Deconstruct. When the RHS is a call, inlining folds that
temporary away, but for a local or parameter it survived into the
output as a separate assignment statement. Consume the copy into the
deconstruction pattern; rendering the copied value as the RHS
recompiles to the identical temporary. Because blocks are processed
back to front, the call-position match defers to the attempt starting
at the copy, mirroring the existing nested-deconstruction defer guard.
The new fixture also covers deconstruction assignment to locals
captured by a lambda in an async method (issue #3037's crash shape,
already fixed earlier).
Assisted-by: Claude:claude-fable-5:Claude Code
Issue #3275 reported an ArgumentOutOfRangeException in
ExpressionBuilder.ConstructTuple for exactly this shape; the crash was
fixed by the nested-deconstruction rework, but no fixture pinned the
record-struct variant, whose Deconstruct methods are compiler-generated.
Assisted-by: Claude:claude-fable-5:Claude Code
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
A moved field initializer cannot read another instance member. Reject primary-constructor conversion for that case and preserve the original constructor.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 0dd407b6-9410-48df-add5-761ca4a8dec0
Tuple element names and nullability are not part of a type's identity, so an
interface resolved through one of its members carries neither. Naming an
explicit implementation from that type produced `void I<(int, int)>.M()` on a
type declared as `I<(int A, int B)>`, which the C# compiler rejects outright
with CS0540 - the decompiled source did not build. The nullable case was
already recorded as a TODO in the NullableRefTypes fixture, where the mismatch
costs a CS8643 warning rather than an error.
The implementing type's base-type list is the only place those annotations are
recorded, so the qualifier is looked up there. Three call sites derived it
independently - the AST builder for all five member kinds, the ambience used
for tooltips and tree labels, and the forwarders synthesized for MethodImpls -
so they now share one helper rather than repeating the rule twice more.
Matching while ignoring tuple names and nullability cannot be ambiguous:
implementing two interfaces that differ only in those is itself an error
(CS8140, CS8645).
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
A query source can be reached through an indexer as well as through a member
access or a call: `holder?[0].Where(...).Select(...)` puts an IndexerExpression
between the LINQ call and the `?.`. The receiver walk stopped there, so query
syntax was still introduced over a source the conditional access had lifted to
a nullable value type, and the output failed to compile with CS1936 - the same
way as the case that was reported, one node kind further along.
IndexerExpression.Target is nullable where MemberReferenceExpression's and
InvocationExpression's are not, so only that arm needs to match on the target.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
Query syntax cannot preserve a null-conditional receiver that lifts a value type. Detect null conditionals through the LINQ receiver chain before introducing query syntax.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 0dd407b6-9410-48df-add5-761ca4a8dec0
C# has no ref-returning switch expression. Skip the transform for StackType.Ref and cover the statement form in RefLocalsAndReturns.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 0dd407b6-9410-48df-add5-761ca4a8dec0
A nested designation whose temporary is still read elsewhere is retried with
that variable demoted to a designator leaf. The check that the first tuple
element must be assigned ran before that retry, and every leaf of a wrongly
nested first element precedes the assigned ones, so the pattern looked like it
started mid-way and was rejected before the retry could restore it. The flat
deconstruction was lost for a shape that has one.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
Deferring an inner deconstruction to its enclosing one used to be decided by
matching the enclosing pattern in full, once per inner statement of the same
pattern, discarding everything but the end position.
The same decisions are available without it. A nested Deconstruct call can only
be consumed by an enclosing one that is the immediately preceding statement,
looking through the defensive copy of a struct element; anything else in between
is a barrier that stops the enclosing from reaching this position, so it matches
here instead. That leaves the case where the enclosing call is adjacent but
cannot match anyway, which is decided by the constraint MatchDeconstructionCall
already places on its out-parameters.
The tuple-designation branch no longer needs the position the enclosing run
starts at, so the backward walk that searched for it is gone with it. The added
fixtures pin reconstruction across adjacent deconstructions, whose element
stores that walk used to step through.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
Only defer to an enclosing designation that can reach this position
The temporaries and element reads of a nested tuple designation are stored back
to back, so a statement of any other kind between the temporary and a read of it
stops the enclosing pattern from consuming that read. Deferring anyway lost the
deconstruction entirely: the enclosing attempt fails and the back-to-front walk
does not return to the position that stepped aside for it, so the reads were left
as the plain element accesses they came from, which master reconstructs.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
A nested designation over tuples, var (x, (a, b)) = t;, is lowered to one
temporary per nested designation - parents before children - followed by the
element reads in depth-first leaf order, and decompiled as a flat
deconstruction plus separate element statements.
The temporaries are now consumed into a tree of tuple nodes before the
conversions and assignments are matched, and the leaves get the same flat
depth-first indices the Deconstruct-call chain hands out, so conversion and
assignment matching runs unchanged. Two properties of the lowered IL shape
the matcher to it: earlier transforms rewrite non-escaping element reads
from ldloca to ldloc, and the temporaries are stack slots whose type is
imprecise, so the container's element type is authoritative and the match
variable is retyped to keep the tuple pattern's invariant.
An element that escapes the deconstruction - used after the statement, so
the pattern cannot consume all its reads - demotes back to a designator leaf
and the match is retried, which restores the flat deconstruction the
escaping read needs. The guard against consuming a pattern piecemeal extends
to the new shape: an element read whose container is stored by an earlier
element read defers to the match starting at that store.
Assisted-by: Claude:claude-opus-5:Claude Code
Element index resolution serves both pattern roots: a registered result of a
Deconstruct call, or an element read of a tuple, which it discovers on first
sight and then owns. In an attempt rooted in a Deconstruct call the tuple
branch must not engage - it overwrites the call's result bookkeeping and
rewires the element read to a fresh variable that the pattern never defines.
The shape that reaches it is a tuple whose element is custom-deconstructed
with discarded leaves, followed by an unrelated assignment: the tuple-rooted
attempt fails, the call-rooted one runs at the element's position, and, now
that an unrelated assignment ends a call pattern instead of rejecting it, the
mixed match is no longer rejected on the way out.
Assisted-by: Claude:claude-opus-5:Claude Code
A nested designation rebinds Deconstruct on the element's static type when
the output is recompiled, while the explicit call it replaces is bound at
the call site. Where a derived element type declares a Deconstruct of the
same arity as the called method, and the source deconstructs through a
base-typed view, the two bindings differ, so the sugared output calls the
wrong method - a divergence the runtime fixture demonstrates on optimized
builds, where copy propagation elides the view.
Nesting is therefore only applied when the method the call binds to is the
one a designation would rebind to; otherwise the call stays explicit, where
its receiver cast preserves the binding.
Assisted-by: Claude:claude-opus-5:Claude Code