Roslyn passes a display class into a local function by ref, and a local
function that only forwards that parameter to a sibling has no closure
variable of its own. The closure analysis therefore found nothing to
anchor it and fell back to the root method body, which put it out of
reach of the callees it forwards to; CallBuilder then hit the assert
guarding a local function reference it cannot resolve and emitted the
raw metadata name of the target instead.
The constructor path also mixed use-site containers into a scope the
closure analysis had already determined; when the use-sites live in
separate function bodies there is no common container, and resetting to
the constructor body threw that scope away.
Assisted-by: Claude:claude-opus-5:Claude Code
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`.
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
The C# 10 grammar only allows attributes on a lambda or its parameters
when the parameter list is parenthesized, but LambdaNeedsParenthesis
predates attribute support and only considered the single parameter's
type and modifiers. An attributed lambda whose parameter type is erased
for being anonymous therefore printed as '[My] a => a.X', which does not
parse. Latent since attributed-lambda decompilation was added: every
other attributed lambda has explicitly typed parameters, which already
force the parenthesized form.
Assisted-by: Claude:claude-fable-5:Claude Code
Assigning through a ref-conditional, (cond ? ref a : ref b) = value, was
emitted without the parentheses, so it re-parsed as
cond ? ref a : (ref b = value) and failed to compile (CS8156 / CS0201).
The target was only parenthesized above assignment precedence, but a
conditional binds tighter than assignment, so the check let it through.
Require the assignment target to have precedence above the conditional
operator. Ordinary lvalues (locals, fields, indexers) are primary
expressions and are unaffected; the postfix ++ form already parenthesized
correctly via unary precedence. Covers plain and compound assignments alike.
Assisted-by: Claude:claude-opus-4-8:Claude Code
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
A local function nested in a lambda can capture closures at two depths:
csc emits it as an instance method on the enclosing method's display
class that takes the lambda's display class as a parameter. Combining
those two capture scopes with FindCommonAncestorInstruction picked the
enclosing method, moving the function out of the lambda that owns the
deeper closure; the variables captured there were then unreachable and
the display-class parameter survived into the output as an undeclared
identifier. Nested capture scopes resolve to the innermost instead,
which a local function can always see - it reaches the outer closure
through the display class it is declared on.
Assisted-by: Claude:claude-fable-5:Claude Code
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
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
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
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
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
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