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
A nested designation, var (x, (a, b)) = o;, is lowered to a chain of
Deconstruct calls - the inner call taking the outer call's out-argument as
its target, through a defensive copy where the element is a struct - and
decompiled as a flat deconstruction followed by an explicit Deconstruct
call. The IL pattern node, its invariants and the C# builders already
support nested patterns; only the transform never built them.
MatchDeconstruction now consumes the chain into a tree of match patterns.
The leaves get flat indices in depth-first order, which is the order in
which StatementBuilder and ExpressionBuilder pair pattern variables with
assignments, so the conversion and assignment matching runs unchanged on
top of a nested pattern.
Two matching rules follow from the chain being consumed: a call pattern no
longer needs a matched assignment, because single-use leaves are covered by
the forwarding fixup in MatchAssignments; and a pattern is not rooted on an
element of an enclosing deconstruction, because blocks are processed back to
front, so the inner call is visited first and would otherwise consume the
pattern piecemeal, starving the outer call. That guard runs the enclosing
match as a dry run, which is precise: a barrier statement between the calls
or an element with further uses makes it fail, and the inner deconstruction
is then still transformed on its own.
Assisted-by: Claude:claude-opus-5:Claude Code
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
The correctness DynamicTests only ran binary + - * / on a dynamic operand, so
no test recompiled and executed decompiled output for any unary operator. That
gap is why ~ on a dynamic value (issue #3820) shipped uncompilable output
undetected: a correctness case round-trips the decompilation through the
compiler, so it fails the moment the decompiler emits something that does not
recompile.
Add ~, -, +, and ! cases. They pin the runtime semantics of the dynamic unary
path and would have caught the OnesComplement regression directly.
Assisted-by: Claude:claude-opus-4-8:Claude Code
After inlining turns a runtime accumulator (e.g. a hand-written GetHashCode prime
chain `h = h * -1521134295 + ...`) into one expression, the leading `SEED * PRIME`
becomes a compile-time constant subexpression. C# always evaluates constant
subexpressions in a checked context, so emitting it bare fails to compile with
CS0220, even though the surrounding context is unchecked.
HandleBinaryNumeric now annotates such an overflowing constant binary operation
with ExplicitUncheckedAnnotation (instead of the implicit UncheckedAnnotation), so
AddCheckedBlocks wraps it in an explicit unchecked(...) - mirroring the existing
handling of overflowing constant n(u)int casts.
Assisted-by: Copilot:claude-opus-4.8:GitHub Copilot CLI
NullPropagationTransform rewrote `c != null ? c.AccessChain : default` to
`c?.AccessChain ?? default` whenever the access-chain result was a non-nullable
value type. For a by-ref-like type (a ref struct such as Span<T>) that form does
not compile: a ref struct cannot be wrapped in Nullable<T> (CS8978). Exclude
by-ref-like return types from the null-coalescing rewrite.
Assisted-by: Copilot:claude-opus-4.8:GitHub Copilot CLI
Compilation uses the .NET builds of the Roslyn toolsets (tasks/netcore*,
bincore csc.dll/vbc.dll launched through the dotnet host). ilasm/ildasm
options use the '-' prefix, which all platforms accept. The dotnet-hosted
compilers have no implicit references or SDK path: net40 compiles pass
mscorlib explicitly, and vbc gets -sdkpath, _MYTYPE=Empty and
-vbruntime:Microsoft.VisualBasic.Core.dll (the facade in the ref packs is
not followed for runtime helpers). The TestRunner gets a self-contained
build for the host platform.
Configurations depending on Windows-only tools or runtimes (legacy
csc/vbc, Roslyn 1.x/2.x, mcs, Force32Bit, executing net40 binaries) are
filtered from the matrix off-Windows via Tester.SupportedOnCurrentPlatform
or gated with [Platform("Win")]. PdbGen comparisons normalize document
name separators, and Correctness/Async uses Console.IsInputRedirected
instead of the Windows-only Console.CapsLock.
Assisted-by: Claude:claude-fable-5:Claude Code
This makes our logic more similar to that used by the dotnet runtime. This lets us infer correct stack types in edge cases such as #2401. It also improves support for obfuscated control flow such as #2878.
The C# translation of StObj will always apply delayed exceptions in these two cases, so putting an instruction with delayed exceptions in that slot would change program semantics.