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
ScopedKind is now the authoritative lifetime representation, so retaining
the preview-era boolean fields would duplicate state. Keep the current
ScopedRef compatibility property and group the new metadata attributes
with the other C# 11 attributes.
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
ScopedRefAttribute only records explicit syntax. Effective lifetime also
depends on UnscopedRefAttribute, params collections, out parameters, and
the defining module's RefSafetyRules version. Model those distinctions in
the type system without changing decompiler output.
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
VB emits an auto-property as a "_<PropertyName>" backing field plus accessors
it does not mark [CompilerGenerated]. The pre-C# 14 transform knows this: it
relaxes its accessor requirement whenever it finds such a field, which is how
a VB auto-property still prints as "{ get; set; }".
Routing every property through the field-backed path lost that. Collapsing an
accessor demanded [CompilerGenerated] unconditionally, so a VB auto-property
stopped collapsing and grew explicit "field" accessors instead - correct code,
but noise where every other compiler's equivalent stays a one-liner. Only the
legacy vbc configurations show it, and those run on Windows alone, so the
Linux and macOS jobs stayed green while both Windows ones failed on
VBPropertiesTest and Async.
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
In generic types, resolve results reference members specialized by the
type's own type parameters. The worklist dedupe and entityMap in
DoDecompile(ITypeDefinition) are keyed by definition, so a hidden member
re-added through the worklist (e.g. a property backing field referenced
from an accessor) was decompiled under a key the output pass never looks
up, silently dropping the declaration while keeping its uses.
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
Review of #3989 pointed out that guarding registration on the theme-aware
marker conflates "XSHD opts out", "already themed" and "already registered",
and leans on two non-contractual AvaloniaEdit details (the delay-load
wrapper's Properties forwarding and its materialize-on-touch behaviour).
Keying the pristine-colour snapshots by colour instance (ConditionalWeakTable)
instead of by definition makes the in-place theming idempotent no matter how
many definition identities expose the colours, so correctness no longer
depends on registration order or the marker; the guard remains only to honour
the XSHD opt-out and to skip redundant list entries. ApplyHighlightingColors
is now private so nothing can set the marker outside a registration.
Also from review: the new tests move to a uniquely named fixture (the old
name collided with Themes/ThemeAwareHighlightingColorizerTests), gain
coverage of the Light/Dark switch path after a dark startup, and the cache
characterization asserts the reconverted content instead of relying on inert
theme switches.
Assisted-by: Claude:claude-fable-5:Claude Code
With dark preselected, the first document of a session rendered with a
double-converted (washed-out) palette; resources showed it across every
token, C# only on tokens outside the hand-authored dark palette. The
same definition was registered with the theme manager under two
identities: HighlightingManager hands out a delay-loaded wrapper whose
members forward to the inner definition that HighlightingService.Load
registers during materialization. Registering the wrapper afterwards
snapshotted the shared colours AFTER the inner registration had already
darkened them, so the snapshot's "light originals" were dark values and
the rewrite darkened them a second time. In-session theme switches were
unaffected because the first touch happens in Light, where both
snapshots are pristine -- which is why the bug only appeared when dark
was already active at first touch.
Skip registration when the definition is already theme-aware: reading
the marker forces the wrapper to materialize, so the check observes the
inner registration. This also stops the remap from clobbering
definitions whose XSHD opts out via ILSpy.IsThemeAware.
Assisted-by: Claude:claude-fable-5:Claude Code
ThemeManager and ThemeAwareHighlightingColorizer split dark mode between
them: the manager darkens a registered definition's named colours in
place, the colorizer per-paint-remaps colours of unregistered
definitions. Running both on one definition converts every colour twice
and washes the palette out. The colorizer captured IsThemeAware once in
its constructor, so a definition registered after the colorizer was
created would be double-converted from then on. Today every colorizer
is created after registration (HighlightingService registers inside
GetByExtension/Load before returning), but that is a calling
convention, not an invariant; reading the flag per paint removes the
ordering dependency.
The colorizer's dark-conversion cache needs no matching flush: its keys
use HighlightingColor's content-based equality, so recolouring a source
colour in place changes its hash and the lookup misses instead of
serving a conversion of the old values. A characterization test pins
that, so an equality-semantics change in AvaloniaEdit shows up as a red
test rather than as stale colours.
Assisted-by: Claude:claude-fable-5:Claude Code
These explain the code by pointing at the front-end that used to implement it.
That front-end is no longer in the tree, so the referent a reader would go
looking for does not exist: "Mirrors WPF's RefreshDecompiledView() call" names
a method nobody can open. In almost every case the sentence beside it already
carried the reason, and the reference was an appendix.
Comments citing a live platform difference are left alone, because there the
comparison is the reason rather than a memory: Avalonia genuinely has no
global RequerySuggested signal, which is why SimpleCommand exists at all.
One had gone stale rather than merely redundant. DerivedTypesEntryNode
described consulting the active search term as a missing feature to reinstate,
but SearchTermMatches is deliberately a no-op so the assembly tree stays
independent of the search pane; the comment now says so.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
A comment that justifies behaviour by pointing at the WPF front-end means
nothing to someone reading the file cold: the reason is either already stated
beside it or is not stated anywhere. Each of these now names the constraint
itself - why navigation waits for pointer-release, why a signature block wraps,
why the tree filter ignores the search term.
Comments citing an external product's documented behaviour as the source of a
rule are left alone; there the reference is the reason, not a memory of how the
code arrived.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
Zoom was stored as the font size itself, so the zoom overlay had no way to
tell a Ctrl+Wheel zoom from a font size picked in the options dialog: any
size other than the hard-coded default made the overlay appear, and the
percentage was measured against that default rather than the user's font.
A separate multiplier restores the split the setting always implied - the
options dialog moves the base size, zoom scales it - so 100% means "the
font you configured", whatever that is.
Assisted-by: Claude:claude-opus-5[1m]: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
Five fixtures covered the "Use nested namespace structure" setting, four of
them running the same toggle at a different layer: the model shape, the same
toggle awaited live, and the same toggle again asserting it reached the
SharpTreeView's rows. Each paid its own boot for a scenario that is one story
end to end, and together they were the second-largest block of time in the
suite after the process-list scroll loops. One test now walks the whole path
once, carrying every assertion the four had, including the nesting depth only
the first checked.
The comparison view's model-is-bound test is dropped: the test after it
renders rows out of that model, which cannot happen unless it is bound, and
it opened two fixture assemblies to prove it.
The expander hitbox test asserted the toggle measures 13x16 and its glyph 9x9,
then clicked 14px down to prove the area below the glyph is live. The click
proves the geometry; the measurements only restate it, and would fail on a
font-metric change that broke nothing. Its layout-settling loop slept 200ms
unconditionally, which is a race that usually wins - it now waits for the
condition it needs.
Assisted-by: Claude:claude-opus-5:Claude Code
These fixtures were written while porting to Avalonia, as an author's own
verification step rather than as coverage: reflection asserting that a type
derives from its base and that a property has the type it is declared with;
literals (MinHeight 29, Padding 3, MaxWidth 900) copied out of the .axaml
beside them; a property override asserted only so pane descendants stay
reachable from tests. None of them can fail except when someone deliberately
edits the line they mirror, and then they fail as a chore.
StartupPerfTests keeps its two [Explicit] benchmarks, which print per-phase
timings worth reading. The third was a wall-clock assertion (8 CoreLib copies
must settle in under 15s) that ran in CI, where a shared runner decides the
verdict; as [Explicit] it would be strictly dominated by the 200-assembly
benchmark it was derived from, so it goes.
Two fixtures are trimmed rather than deleted, because their kernel is real:
XmlDocLoader's ref-pack fallback has no other test in the repo, and the
MenuIcon metadata rasterisation was dropped once during the port already.
Both now assert that without booting MainWindow to reach it.
This is worth about two seconds - it buys reviewers less to read, not CI
less to do.
Assisted-by: Claude:claude-opus-5:Claude Code
* Set v11 RTM
* Update features in README.md
* Remove the two 900-iteration process-list scroll tests
* Keep Svg.Controls.Skia.Avalonia at 12.0.0.13
* 10.0.11 and Roslyn for net11p7
* Fix module-scan test failing when PowerShell's NGen images are stale
* Opt Pack NuGets out of the MSBuild server to fix SBOM generation
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
An export's ITextOutput goes nowhere: ProjectExporter and SolutionWriter both
hand the language a throwaway PlainTextOutput and build their own status
report, so a language writing failures into that output is invisible. The
failures travel on DecompilationOptions instead and are rendered by the caller
that owns the report - each one with its full exception in a collapsed fold,
which is what makes a bug report actionable.
Drive-by: WriteExceptionDetails split the exception text without trimming, so
for exceptions rendering a trailing newline the fold reached one line past the
last frame and swallowed the line behind it; and the tab's own decompilation-
failure path had regressed to dumping a raw stack trace instead of using that
helper.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
One member the decompiler could not handle aborted the whole export, so a
single unsupported method in a large assembly left the user with nothing: no
sources, no .csproj, no way around it. Recovering silently would trade that
for a worse outcome - broken output nobody knows is broken - so every failure
is recorded, written where the content would have gone, and pointed at the
issue tracker.
The recovery has to hold for anything the export touches, not just method
bodies: a file that cannot be created, a resource that cannot be decoded, an
output visitor that throws mid-type. Each of those costs its own unit and
nothing else, and the units behind a failure are still produced - dropping
them would make the export look complete when it is not.
Consumers that relied on the exception keep their failure signal: ilspycmd
exits non-zero and lists the failures, the PowerShell cmdlets raise an error
record per failure, and the round-trip suite asserts the export reported none
- otherwise a crash on a method its own tests never call would ship green.
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
Structural generator mistakes surface at compile time via DSTG002-005
and partial-member matching, and emission regressions light up the
fixture suite - except one: dropping the reversed bucket scan in the
generated GetMinimumRequiredVersion compiles green and returns the
lowest enabled feature version instead of the highest, and the method's
only consumer is project-export LangVersion stamping, which default CI
runs barely exercise. Pin the highest-wins contract, including the
syntax-preference settings that now participate in the ladder.
Assisted-by: Claude:claude-fable-5:Claude Code
The 14 C# 1.0 settings each carried a handwritten
[Category("C# 1.0 / VS .NET")] literal, duplicating the per-version
display knowledge the generator's CategoryByVersion map single-sources.
Gating them on LanguageVersion.CSharp1 instead is observably identical:
CSharp1 is the smallest enum value, so the generated SetLanguageVersion
bucket can never fire, and the new GetMinimumRequiredVersion arm returns
the same CSharp1 the final fallback already does.
Assisted-by: Claude:claude-fable-5:Claude Code
The language version appears in two places that share a name but not a
concept, which repeatedly reads as one confused API: on
DecompilerSettings it is a construction shortcut (SetLanguageVersion
initializes the feature flags once and the version is not stored, so
the flags are the only state and the call is deliberately one-way),
while on WholeProjectDecompiler it is an export parameter (the
LangVersion stamped into the project file, defaulting to
GetMinimumRequiredVersion() and rejected below it as a safety net
against exporting uncompilable projects). Spell both roles out in the
XML docs so the distinction no longer has to be reverse-engineered.
Assisted-by: Claude:claude-fable-5:Claude Code
The LanguageVersion setter's InvalidOperationException is a safety net
against exporting a project whose LangVersion cannot compile the
emitted code, but it only fires at assignment time: Settings is mutable
and shared, so enabling a feature after assigning the version slipped
past the check. Re-validating at the start of DecompileProject closes
that gap while keeping the setter's immediate feedback.
Assisted-by: Claude:claude-fable-5:Claude Code
Four settings used the bare category string "Other" while the rest of
the group uses the "DecompilerSettings.Other" resource key. Both happen
to resolve to the same English text today, so the options UI shows one
group, but the two keys would split into separate groups the moment
their translations diverge.
Assisted-by: Claude:claude-fable-5:Claude Code
The setting carried the C# 11.0 display category but was missing from
both SetLanguageVersion and GetMinimumRequiredVersion, so decompiling
for an older target language version could still produce switches over
ReadOnlySpan<char> that the requested compiler cannot compile. Gating
it like the other C# 11.0 settings closes that gap; the category string
is now derived from the version like everywhere else.
Assisted-by: Claude:claude-fable-5:Claude Code
These three settings were disabled by SetLanguageVersion for older
targets but, unlike every comparable syntax-preference setting, never
raised GetMinimumRequiredVersion while enabled - an omission that had
gone unnoticed in the handwritten version bookkeeping. Drop the
AffectsMinimumRequiredVersion escape hatch that reproduced it.
Assisted-by: Claude:claude-fable-5:Claude Code
Every version-gated setting was bookkept in four places that had to stay
in sync by hand: the property boilerplate, SetLanguageVersion,
GetMinimumRequiredVersion, and the [Category] display string - and that
sync had already drifted in a handful of settings. A new source
generator in ICSharpCode.Decompiler.Generators now derives all four
from a single [DecompilerSetting] attribute on a partial property:
backing field, accessors with change notification, the version-derived
[Category], and both version methods. [Description] stays handwritten
because its resource keys are irregular and are grepped from the resx.
This commit is a 1:1 translation: the current inconsistencies are
reproduced exactly (AffectsMinimumRequiredVersion = false on
ExtensionMethods, UseLambdaSyntax and UseEnhancedUsing; no gate on
SwitchOnReadOnlySpanChar), verified against the old build by comparing
SetLanguageVersion and GetMinimumRequiredVersion behavior for every
setting at every language version, plus a reflection diff of the full
per-property attribute surface.
Assisted-by: Claude:claude-fable-5:Claude Code
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