Review follow-up. A display-class field initialized from a non-this
parameter is now the only shape where propagation and a later mutation
coexist; it stays sound only because ResolveVariableToPropagate accepts
a parameter with LoadCount == 1, so the mutation can be redirected to
it. Nothing covered that, so Test12 pins it, and Test13 records the
neighbouring shape where the mutation happens inside a lambda - there
capturing the display class keeps it materialized and propagation never
arises. The guard predicate is renamed to say what it matches, since
'ReadOnly' reads like the C# keyword rather than 'a plain read'.
Assisted-by: Claude:claude-fable-5:Claude Code
A field that is propagated to the variable it was initialized from is
replaced by that variable, so re-emitting its initializer assigns the
variable to itself. Where the field was initialized from 'this' the
result does not even compile ('this = this'). The store is dropped
instead, which is what VisitStObj already does for initializer stores
that are not part of an object-initializer block; the insertion position
has to be tracked separately from the loop index, because skipping a
store would otherwise push the following ones past the end of the block.
Assisted-by: Claude:claude-fable-5:Claude Code
Aggressive scalar replacement propagated a display-class field to its
source variable even when the field is mutated after initialization,
aliasing two distinct source-level variables (Test9: thisField and
this). Propagation is now cancelled when the field sees a second store
or its address escapes, but only for propagation targets that cannot
absorb the store: 'this' and variables that are themselves
scalar-replaced display classes. Parameters continue to propagate,
because their remaining uses are already restricted by
ResolveVariableToPropagate and a captured parameter mutated inside a
lambda (DelegateConstruction's Bug951) must keep mapping to the
parameter. Checking CanPropagate first also keeps the guard away from
Mono state-machine fields, whose VariableToDeclare is pre-bound to a
state-machine variable that Propagate(null) would discard.
Re-enables Test9 and adds Test10 covering the escaping-address variant
(Interlocked.Exchange(ref displayClass.thisField, ...)).
Assisted-by: OpenCode:openai/gpt-5.5:OpenCode
Assisted-by: Claude:claude-fable-5:Claude Code
The .NET 10 BCL ships static [Extension] classes that contain ordinary
nested types (e.g. XDocumentExtensions.XDocumentNavigable). Decompiling
such a nested type's member in isolation resolved the enclosing
container's ExtensionInfo, and DecompileBody then dereferenced the
missing extension-member mapping. A container without any extension
blocks now reports no ExtensionInfo at all, and ResolveExtensionInfo
applies a container's info only to members that actually belong to one
of its extension blocks.
Assisted-by: Claude:claude-fable-5:Claude Code
Covers ref structs implementing interfaces (implicit, explicit, and
default-interface-method reimplementation, which CS9245 forces on every
ref struct implementer), the allows ref struct anti-constraint on
methods, classes, interfaces, delegates, local functions, iterators,
async methods, and capturing local functions, constraint combinations
(interface, IDisposable with using, unmanaged, struct, new()),
interface members invoked through a constrained T (instance, static
abstract factory, and default interface methods, which are callable
through T because implementers must always override them), scoped/ref/
in/out parameters of T, and call sites instantiating with Span/
ReadOnlySpan type arguments.
The test is green in all four Roslyn 4.14/latest debug/opt configs: the
decompiler already round-trips the gpAcceptByRefLike flag into
"allows ref struct" clauses and re-emits ref struct interface
implementations correctly. One cosmetic quirk is pinned as-is: calls
whose type arguments are ref structs are printed with explicit type
arguments and a declaring-type qualifier even within the declaring
class, because type-argument inference validation does not accept ref
struct type arguments; the output remains compilable and semantically
identical.
Assisted-by: Claude:claude-fable-5:Claude Code
The chain cases in the fixture route T : TOuter through a class-level
type parameter; the variant where the dependency target is a sibling
method type parameter (M<T, U> with T : U) was uncovered. It pins the
same alignment from a different angle: csc rejects 'class' with CS8665
when U is merely class-constrained and requires 'default', but accepts
'class' when U carries a class-type constraint, matching what the
tri-state IsReferenceType derives. Both directions were verified
against csc before adding the expected output.
Assisted-by: Claude:claude-fable-5:Claude Code
Corrupt or hand-written metadata can contain a property with no
MethodSemantics rows at all. MetadataTypeDefinition.Properties already
skips properties with neither a visible getter nor setter, so neither
the parameterized-property path nor the ordinary one ever sees them;
the accessor-method emission also tolerates a missing accessor by
construction. Pin that with an ILPretty case containing both a
parameterized and an ordinary accessor-less property.
Assisted-by: Claude:claude-fable-5:Claude Code
The renamed-Implements case exposed a gap: the accessor-method
declarations did not include the explicit-interface-implementation
forwarders generated from .override directives, so decompiled types did
not implement their interfaces and same-name implementations lost their
interface mapping. DecompileParameterizedProperty now emits the same
forwarder stubs as the ordinary method path.
Assisted-by: Claude:claude-fable-5:Claude Code
C# cannot declare a named property with parameters: only the type's
default member gets indexer syntax, and reusing it via [IndexerName]
collapses for types with several differently-named indexed properties,
static properties, or explicit interface implementations. Emitting the
accessors as ordinary methods is the only fully general compilable
form, matches how C# consumes such properties (Roslyn exposes the
accessors of properties it cannot bind as regular methods, the same
pattern C# 14 made user-facing for extension-member disambiguation),
and round-trips call sites to identical IL. Call sites already lower
to direct accessor calls.
The property-level attributes are kept on the first accessor under the
'property:' attribute target: it is not valid on methods, so csc emits
nothing for it (CS0657 warning) and recompilation neither loses the
attributes from the source nor misapplies them to the accessor. A
comment on the first accessor documents the deliberate deviation.
Visual Studio's metadata-as-source view drops such properties'
attributes entirely.
The assembly tree and tooltips are unaffected: they keep rendering the
property node with its parameter list. Known limitation, inherent to
any C# projection: recompiling the output produces plain methods, so
VB.NET consumers of the recompiled assembly lose property syntax.
Assisted-by: Claude:claude-fable-5:Claude Code
C# has no syntax for named properties with parameters, so the expected
output declares the accessors as ordinary methods, keeps the
property-level attributes under the inert 'property:' attribute target
(ignored by csc with CS0657), and consumes the properties through
direct accessor calls, which Roslyn permits exactly for properties
whose shape C# cannot bind. Red against the current decompiler, which
emits a parameterless property whose body references the vanished
parameters.
Assisted-by: Claude:claude-fable-5:Claude Code
The foreach pattern was only matched against using instructions, but
the compiler emits no using/try-finally at all when the enumerator's
static type can never require disposal: a struct or a sealed class
that does not implement IDisposable (SerializationInfoEnumerator in
the issue's example). Such loops stayed while loops.
Recognize the bare 'enumerator = x.GetEnumerator(); while
(enumerator.MoveNext())' shape during statement building and reuse the
existing foreach transformation core for it. The transformation is
restricted to exactly the cases where recompilation would produce the
same IL: the enumerator type rules above (ref structs are excluded
because of pattern-based disposal), a single-store enumerator variable
unused outside the loop, and synchronous enumeration only, since async
enumerators are always IAsyncDisposable.
Assisted-by: Claude:claude-fable-5:Claude Code
allows ref struct is inherited implicitly, so restating it on an override is
CS0460 even alongside a legal disambiguator. Roslyn still re-emits the byreflike
flag on the override's own type parameter, and the general constraint printer
turns that flag back into source, so the disambiguator stays legal only as long
as it is built separately. Cover a C# 13 base whose annotated and plain methods
both allow ref structs.
Assisted-by: Copilot:claude-opus-5:GitHub Copilot CLI
A class-type constraint such as Stream or Delegate sets no
ReferenceTypeConstraint flag, so keying the disambiguator off that flag gave
those overrides the default constraint, which is CS8822, and the output still
did not recompile. The restated disambiguator leaves no metadata trace of its
own, so the choice has to follow from whether the inherited constraints make
the type parameter a reference type, a value type, or neither.
Matching the annotated type parameters by identity rather than by owner kind
and index also keeps a specialized signature from contributing a foreign type
parameter that happens to share an index.
Assisted-by: Copilot:claude-opus-5:GitHub Copilot CLI
Override constraints are normally inherited and omitted, but nullable type
parameters still require class or default to distinguish annotations from
Nullable<T>. Derive that legal discriminator from the method metadata.
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
Yield translation derived its target type only from synchronous enumerable
interfaces, leaving async iterator yields untyped and preserving compiler
boxing casts. Use the element type already recovered by the async decompiler.
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
ExpressionBuilder printed bitwise & / | on booleans as && / || whenever
the right-hand side was pure, but Roslyn lowers && / || to & / | only
when the right operand is a bare local or parameter read
(LocalRewriter.MakeBinaryOperator, unchanged since 2014). Shapes like
(c == 'a') | (c == 'b') can therefore only originate from a bitwise
source operator, yet were shown as short-circuiting. Per the discussion
in #1545, show the operator the IL actually uses instead of guessing the
source form: the reversal is dropped entirely, so Roslyn-compiled
"a && b" now decompiles to "a & b", which recompiles to the same IL.
Assisted-by: Claude:claude-fable-5:Claude Code
Obfuscators may rename the members of compiler-generated iterator
classes while keeping the MethodImpl (.override) rows intact. Since
b110d5c2d (first shipped in ILSpy 8.0), Dispose, get_Current and
GetEnumerator are identified by name or method impl; MoveNext was still
looked up by name only, so such a state machine was rejected with
"Method not found". The pre-Roslyn "yield break" handling likewise
matched the Dispose call by name; it now compares against the already
resolved Dispose handle. The new ILPretty fixture models the obfuscated
shape from the issue's assemblies, against which the fix was verified;
the rest of the issue was already resolved by b110d5c2d and 81e702f84.
Assisted-by: Claude:claude-fable-5:Claude Code
Enum members whose value duplicates an earlier member now reference it
(Item2B = Item2A), [Flags] members combine earlier single-bit members
(All = Item1 | Item2 | Item3) or their complement (NotItem1 = ~Item1)
instead of showing a bare number.
Several guardrails keep the output faithful to how such enums are
written by hand: only previously declared members are referenced (field
row order); a multi-bit value lying entirely within a larger, earlier
member is a field encoding inside that mask, not a flag union, and
stays numeric, as do zero-valued members of [Flags] enums, which
routinely have several unrelated zero members. The ~X form is
suppressed in byte/ushort enum declarations, where the initializer
constant folds in int and would not compile. Enums with unusual
underlying types (bool, native int) keep the plain constant conversion.
With these rules, decompiling System.Private.CoreLib reproduces the
hand-written declarations of TypeAttributes, MethodAttributes,
AttributeTargets and FileAttributes almost verbatim.
Assisted-by: Claude:claude-fable-5:Claude Code
Object creation can require an unsafe context solely because the selected
constructor has a pointer parameter. Apply the existing unsafe-signature
check to object creation nodes so the emitted declaration remains compilable.
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
With recognition on the ILAst and reference substitution during
translation, the four accessor-body patterns in
PatternStatementTransform were only reachable as a fallback, and any
divergence between them and the AutoEventDecompiler verdict produced
inconsistent output. A compiler shape the ILAst matchers do not know now
degrades to explicit accessors with the backing field kept in the
output, which stays compilable. Bodyless events (abstract, extern,
interface members) previously relied on the patterns' no-body clause to
become field-like; since C# cannot express bodyless custom accessors,
DoDecompile now chooses the field-like form for them directly. Also
deletes the orphaned IsEventBackingFieldName helper; the name
association lives in PropertyAndEventBackingFieldLookup.
Assisted-by: Claude:claude-fable-5:Claude Code
Claude-Session: https://claude.ai/code/session_01Btdypgm8utyxqt1Etn2BDi
PatternStatementTransform renamed backing-field identifiers to the event
after the fact, keyed on the metadata name association alone: references
belonging to an event that is not actually automatic were still renamed,
binding them to a custom event that is unusable as a value (the same
defect class as #3858), and the rename bypassed the resolver checks, so
qualifiers were computed for the hidden field instead of the printed
event. ExpressionBuilder.ConvertField now performs the substitution,
keyed on the AutoEventDecompiler verdict whose memo moves into
DecompileRun so that member hiding, the event declaration, and reference
translation all decide from one analysis. Checking the verdict's field
identity also keeps same-typed sibling events apart (#3575), and the
qualifier logic running against the event drops spurious this./type
qualifiers from raise sites.
mcs 2.x accesses a sibling automatic event's backing field directly
inside custom accessors instead of calling the accessor, so the fixture
expects the resulting Delegate.Combine form there.
Assisted-by: Claude:claude-fable-5:Claude Code
Claude-Session: https://claude.ai/code/session_01Btdypgm8utyxqt1Etn2BDi
MemberIsHidden decides field hiding from the metadata name association
alone, which over-approximates: an event whose accessors fail the ILAst
validation is decompiled with explicit accessors, and while a referenced
backing field is re-added through the work list, an unreferenced one was
silently dropped from the output. The type-definition member loop now
consults the same memoized verdict as the event declaration, so the two
decisions agree by construction.
Assisted-by: Claude:claude-fable-5:Claude Code
Claude-Session: https://claude.ai/code/session_01Btdypgm8utyxqt1Etn2BDi
The syntactic accessor-body patterns in PatternStatementTransform sit
downstream of every settings-dependent transform, so each new compiler
shape or settings combination silently broke recognition: with
AggressiveInlining enabled, static events inline the Delegate.Combine
call into CompareExchange positionally, which none of the four patterns
matched, while call sites were still rewritten to the event name from
metadata alone - producing uncompilable output (CS0079).
Recognition now happens in DoDecompile(IEvent) by structurally matching
the ILAst of the accessors, decompiled with a fixed set of settings the
same way RecordDecompiler analyzes method bodies. This makes detection
independent of the user-visible settings by construction. Events that
are not recognized fall back to the classic path unchanged, including
the existing AST patterns.
mcs 2.x compiles the accessors as a compound assignment, evaluating
'this' once via IL 'dup'; the simple-combine matcher accepts that
stack-slot alias.
Assisted-by: Claude:claude-fable-5:Claude Code
Roslyn caches a ReadOnlySpan<T> created from an array literal in a
<PrivateImplementationDetails> field on target frameworks without
RuntimeHelpers.CreateSpan (e.g. .NET Framework / netstandard2.0 + System.Memory):
object obj = <PrivateImplementationDetails>.cache;
if (obj == null) {
obj = new char[] { '\r', '\n' };
<PrivateImplementationDetails>.cache = (char[])obj;
}
... new ReadOnlySpan<char>((char[])obj) ...
The decompiled output referenced the compiler-synthesized
<PrivateImplementationDetails> type, whose escaped name is not expressible in C#
and is never declared, so the output failed to recompile (CS0400).
The modern RuntimeHelpers.CreateSpan form was already handled
(TransformRuntimeHelpersCreateSpanInitialization); this adds the analogous
handling for the legacy lazy-cache form, mirroring CachedDelegateInitialization
(which collapses the same lazy-static-field cache for anonymous-method delegates).
Once the cache is collapsed, the existing array-initializer transforms recover
the array literal, so the <PrivateImplementationDetails> reference disappears.
Test: ILPretty/CachedReadOnlySpanInitialization.
MatchSwitchOnCharBlock's case 2 and default paths guarded against a
negative character index, but case 1 (a bare switch on get_Chars) did
not. Crafted IL whose get_Chars/get_Item index is negative - a value no
compiler emits, but valid IL - therefore reached the pattern
reconstruction unchecked. For a length-1 group this silently miscompiled
the switch (it rebuilds the string switch from the char labels without
using the index), turning IL that reads s[-1] into `switch (s)`; for
longer strings it threw IndexOutOfRangeException and aborted the method.
Move the check into MatchGetChars so all three call sites reject a
negative index by construction, and drop the two now-redundant guards.
Same class of unvalidated-integer robustness issue as #3878, in a
different switch-on-string pattern.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Added Issue3877 test to PrettyTestRunner and new test case source to verify dictionary initialization with negative capacity. Updated SwitchOnStringTransform to skip processing when a negative dictionary capacity is detected.
When a null literal is the only argument a generic type argument could
be inferred from, and that type argument is an anonymous type, ILSpy
used to drop the type arguments entirely (they cannot be written
explicitly), producing 'Test(null)', which no longer compiles
(CS0411). A conditional expression whose never-taken branch creates an
instance of the anonymous type is the minimal C# expression that gives
a null value that type, so the argument now carries enough information
for type inference.
The instance expression is obtained by translating a synthesized
'newobj' with 'default.value' arguments through the existing pipeline
rather than assembling syntax by hand; the property values are typed
defaults, since the IL only contains ldnull. Anonymous types occurring
inside other constructed types (e.g. arrays) stay unexpressible and
keep the previous output.
Assisted-by: Claude:claude-fable-5:Claude Code
CS8196 also fires when the second reference sits inside a nested call
in another argument of the declaring call, e.g.
OutAndValue(out var a, UseAndReturn(out a)). The existing check already
covers this because it walks all descendants of the sibling arguments;
this fixture pins that behavior.
Assisted-by: Claude:claude-fable-5:Claude Code
Passing the same local as multiple out arguments of one call made
DeclareVariables turn the first use into an implicitly-typed declaration,
producing 'f(out var x, out x)'. Referencing an implicitly-typed out
variable in another argument of the declaring call is rejected by the
compiler (CS8196), because its type is only inferred once overload
resolution of that call has completed. The explicitly-typed form
'f(out int x, out x)' is valid, so fall back to the explicit type in
that case.
Assisted-by: Claude:claude-fable-5:Claude Code
Awaiting a dynamic value lowers GetAwaiter/IsCompleted/GetResult to
dynamic callsites, which async decompilation could not recognize:
await detection ran before DynamicCallSiteTransform, so the await was
emitted as a raw state machine (or crashed in AnalyzeAwaitBlock, #1388).
AnalyzeStateMachine now collapses the awaiter callsites per block, folds
the runtime ICriticalNotifyCompletion branch the compiler emits for an
awaiter not statically known to implement it into the canonical single
call, and re-joins the branch chains the collapse leaves so each dynamic
await sits in one block. DetectAwaitPattern matches the dynamic
GetAwaiter/IsCompleted/GetResult shape and emits `await expr`; a
synthesized dynamic GetResult method gives the await and its local the
dynamic type. DynamicCallSiteTransform also follows callsite targets
spilled into state-machine locals, so an awaited value flowing into a
dynamic callsite (e.g. d.Result = await ..., #1928) decompiles too.
Assisted-by: Claude:claude-fable-5:Claude Code
DynamicIsEventInstruction has no ExpressionBuilder visitor, so any is-event
diamond that survives the transform pipeline leaks as an "OpCode not supported"
comment. The collapse only fired for the plain statement form; every other
lowering the compiler emits for "d.Event += b" was missed:
- a copy-of-value temporary between the getmember cache and the diamond (modern
Roslyn emits it for the statement form),
- the result-used shape, where the diamond is a value-if nested inside the
consuming expression (a call argument, or a leave's value when returned),
- the result-returned shape as two leaves, "if (isevent) leave(add)" plus a
fall-through "leave(compound)" rather than an if/else, and
- the same two-leaves shape when the optimizer drops the getmember cache (legacy
csc and Roslyn <= 2.x): the is-event flag then feeds a single branch, inlining
folds it into the condition, and the compound leave is already collapsed, so
the cache-based entry never matched it and the branch leaked. These compilers
only run on Windows CI, so the gap was invisible on Linux.
The descendants walk handles the nested value-if once an over-strict
copy-of-value bounds guard (which never held for the compact return block) is
dropped. The two two-leaves shapes share a skeleton matcher and the add/remove
accessor-name check, and both validate the accessor name and arguments. Re-running
from the collapsed statement instead of the next one avoids indexing past the end
of a block that has shrunk to a single leave.
Assisted-by: Claude:claude-opus-4-8:Claude Code
A stackalloc initializer whose first element is written twice must not be
folded into a 'stackalloc int[] { ... }': doing so drops the earlier store,
which is observable when its value has side effects. HandleSequentialLocAllocInitializer
already rejects this (the extra store trips the LoadCount and elementCount
guards); the sequential-store offset matcher on its own would misclassify the
second offset-0 write. The IL is derived from a C# 'stackalloc int[] { a, b, c }'
disassembly with an extra offset-0 store injected, a shape no compiler emits.
Assisted-by: Claude:claude-fable-5:Claude Code
A second store to an already-written array element must prevent
HandleSimpleArrayInitializer from folding the stores into a collection
initializer: the earlier store would be dropped (a visible change when
its value has side effects), and without the nextMinimumIndex guard the
transform crashes on the colliding index. The nextMinimumIndex check
already enforces this; pin it with a Pretty fixture so the boundary
between a foldable initializer and a duplicate-write sequence stays
covered.
Assisted-by: Claude:claude-fable-5:Claude Code
The C# compiler lowers a foreach over an inline array into a for loop whose
body reads each element through <PrivateImplementationDetails>.InlineArrayElementRef(ref
buffer, i). That helper cannot be named in C#, so the decompiled for loop did
not compile.
Reconstruct the foreach at the AST level instead. Rewriting the unchecked
InlineArrayElementRef helper to the bounds-checked indexer buffer[i] would be
unsound for an out-of-bounds index, so the transform fires only when the loop
bound equals the inline array's length: that proves 0 <= i < length, matching
the exact shape the compiler emits and nothing else. A loop that does not match
keeps the faithful (if unnameable) helper call rather than silently gaining a
bounds check.
Assisted-by: Claude:claude-fable-5:Claude Code
A value-type constructor chains via 'this = new TSelf(...)', an ordinary
body statement, so a hoisted argument null-guard in front of it is legal
C# output as-is; folding it back only bought lifting the chain into a
this(...) initializer. That cosmetic gain does not justify the extra
stobj shape matching, so the guard now stays in the body for structs and
the gate reduces to the ChainedConstructorCallILOffset check.
Assisted-by: Claude:claude-fable-5:Claude Code
Per @siegfriedpammer: rather than rescue the hoisted for-initializer in
DeclareVariables, don't form the for-loop at all. TransformFor now bails when the
loop variable is a by-ref-like local used after the loop, leaving the while-loop
(matching source); the ref decl keeps its initializer, no CS8174. Reverts the
DeclareVariables change; test now expects the while form.
Assisted-by: Copilot:claude-opus-4.8:GitHub Copilot CLI
A ref local that is used after a for-loop has its declaration hoisted in front of
the loop, but its only initialization is the for-initializer ref-assignment. The
declaration was then emitted without an initializer (`ref T x;`), which does not
compile (CS8174).
When a by-ref-like local's matching assignment is the first for-initializer, move
the ref-assignment's value up into the declaration (`ref T x = ref expr;`) and
drop the for-initializer.
Assisted-by: Copilot:claude-opus-4.8:GitHub Copilot CLI
A type's leading field assignments are extracted to field declarations
only when every constructor that does not chain with this() agrees on
them. When they disagree, the analyzer gave up on the whole type, so the
remaining constructors' this()/base() calls were never lifted into
initializers -- a struct with two divergent constructors plus a chaining
one rendered the chain as `this = new TSelf(...)` instead of `: this(...)`.
Chain lifting does not depend on the shared-initializer extraction, so a
mismatch now just skips the extraction (the assignments stay in the
bodies) and the transform continues. Primary constructors still bail,
since their parameters drive the initializers that must be extracted.
Assisted-by: Claude:claude-opus-4-8:Claude Code
When a chained constructor-call argument contains a throwing null-check
(e.g. `value?.Length ?? throw ...`) and the argument is evaluated more
than once, the compiler hoists the null-check in front of the chained
call. The hoisted `if (value == null) throw ...;` then became the first
body statement, so MoveConstructorInitializer could not recognize the
chained call and left it as an illegal in-body `base..ctor(...)` /
`this..ctor(...)` (a parse error).
Fix it in the ILAst, where the `?? throw` shape already lives, rather
than re-deriving it on the C# AST: NullCoalescingTransform folds a guard
that directly precedes the chained call back into the first use of the
parameter as `if.notnull(ldloc param, throw)`. Nothing in a constructor
body can legally run before the chained call, so a statement preceding it
is necessarily compiler-hoisted; matching is by ILVariable identity, not
parameter name. The guard disappears before the AST transforms run, so
they need no change.
Reference types chain via a base/this..ctor CallInstruction; value types
chain via `this = new TSelf(...)`, i.e. stobj(ldthis, newobj TSelf(...)),
which ChainedConstructorCallILOffset does not report -- so the value-type
chain (including the case where this is spilled to a stack slot because
the guard sits between its load and the call) is matched directly.
Assisted-by: Claude:claude-opus-4-8:Claude Code
For an async iterator with an [EnumeratorCancellation] cancellation token, the
hoisted-local cleanup (stfld <>u__N(this, null)) can be emitted before the
combined CancellationTokenSource disposal in the set-result and catch blocks.
CheckSetResultReturnBlock and ValidateCatchBlock only consumed that cleanup after
the disposal, so the `pos + 2 == count` test missed the dispose pattern and the
analysis failed, leaving the raw state machine (catch (object), goto case, ...).
Allow the cleanup to appear before the combined-tokens disposal as well.
Assisted-by: Copilot:claude-opus-4.8:GitHub Copilot CLI
UnscopedRef only appeared on a params parameter anywhere in the
suite; a ref-returning method and property on a ref struct pin the
attribute round-trip.
Assisted-by: Claude:claude-fable-5:Claude Code
Static abstract interface members had no checked operator or
unsigned-right-shift coverage, and static abstract property getters
were never read as rvalues through the constraint.
Assisted-by: Claude:claude-fable-5:Claude Code
Compound assignments selecting between op_Addition and
op_CheckedAddition across a checked block boundary were not
covered.
Assisted-by: Claude:claude-fable-5:Claude Code
Generic attributes were only closed over simple types; constructed
generic, array, and enum type arguments exercise separate paths in
the custom-attribute blob decoder.
Assisted-by: Claude:claude-fable-5:Claude Code
SetsRequiredMembers had no coverage anywhere; required members
across a base/derived pair and in a generic abstract host were also
untested.
Assisted-by: Claude:claude-fable-5:Claude Code
readonly record struct had no coverage, and no record overrode a
synthesized member; user-declared ToString and PrintMembers pin the
synthesized-vs-user member detection.
Assisted-by: Claude:claude-fable-5:Claude Code
Only two plain ASCII u8 literals were covered; the empty literal and
a literal made of escape sequences pin the recovery heuristic
boundaries.
Assisted-by: Claude:claude-fable-5:Claude Code
The C# 6 index-initializer syntax was only covered through custom
indexers; a real Dictionary<,> initialized with [key] = value pins
the choice between the index form and the Add form.
Assisted-by: Claude:claude-fable-5:Claude Code
ConfigureAwait(false) inside an async iterator and await foreach
over ConfiguredCancelableAsyncEnumerable were not covered.
Assisted-by: Claude:claude-fable-5:Claude Code