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
Only interface-based disposal was covered; a using declaration over
a ref struct with a pattern-based Dispose exercises the recognition
heuristic without IDisposable.
Assisted-by: Claude:claude-fable-5:Claude Code
protected and protected internal default interface members, static
properties and field-like events in interfaces, and a generic
interface with a constrained generic default method were not
covered.
Assisted-by: Claude:claude-fable-5:Claude Code
The fixture had an iterator local function but no async local
function; the async state-machine-in-local-function shape, static
and capturing, was not covered.
Assisted-by: Claude:claude-fable-5:Claude Code
await inside a null-coalescing expression, as a do-while condition,
on a ValueTask, and inside an interpolated-string hole (the
DefaultInterpolatedStringHandler lowering) were not covered.
Assisted-by: Claude:claude-fable-5:Claude Code
dynamic used as a while-loop condition and the null-conditional
invocation operator on a dynamic receiver were not covered.
Assisted-by: Claude:claude-fable-5:Claude Code
AddChecked/MultiplyChecked/ConvertChecked nodes were not covered;
they are pretty-printed as a checked block around the tree-building
calls.
Assisted-by: Claude:claude-fable-5:Claude Code
Only group-by continuations were covered; a continuation introduced
by select-into exercises a different transparent-identifier reset.
Assisted-by: Claude:claude-fable-5:Claude Code
Virtual/override/sealed-override auto-properties, implicit and
explicit interface implementations, asymmetric accessor
accessibility, and auto-properties in structs were not covered.
Assisted-by: Claude:claude-fable-5:Claude Code
Anonymous types nested as members of other anonymous types (and read
back through the projection), ToString on an anonymous instance, and
explicit member projections were not covered.
Assisted-by: Claude:claude-fable-5:Claude Code
The lifted-operator matrix only used pure expressions; compound
assignment on nullable locals and boxing/unboxing conversions of
Nullable<T> were not covered.
Assisted-by: Claude:claude-fable-5:Claude Code
All iterator producers in the fixture used concrete element types;
a generic method pins the reconstruction of the generic state
machine type.
Assisted-by: Claude:claude-fable-5:Claude Code
Generic co-/contravariance had no test coverage at all: no out/in
variance modifier on any interface or delegate declaration and no
variant reference conversion appeared anywhere in the test suite.
The new fixture pins declarations (including a constrained covariant
interface and an explicit implementation of a variant interface) and
conversion shapes that must not produce explicit casts.
Assisted-by: Claude:claude-fable-5:Claude Code
All existing filter tests only read state; a filter that mutates a
ref local observed by the handler pins the ordering between the
filter expression and the handler body.
Assisted-by: Claude:claude-fable-5:Claude Code
Pins the reconstruction of a continue statement in a catch block
inside a foreach loop, which used to decompile to a goto targeting
a label at the end of the loop body. #2829 reported the goto form
and can be closed.
Assisted-by: Claude:claude-fable-5:Claude Code
The UnsafeCode fixture had no double-indirection coverage at all:
no pointer-to-pointer loads, stores, or indexing, and no fixed
statement over an element of a pointer array.
Assisted-by: Claude:claude-fable-5:Claude Code
Record AST transform groups and mutation steps through the C# pipeline, replay selected steps with the stepper, and carry modified-node ranges through output so the Debug Steps pane can highlight the selected mutation without replacing its full step tree.
Assisted-by: CodeAlta:gpt-5.5:CodeAlta
A field, auto-property, or event initializer is written once at its
declaration, but in IL it runs in every instance constructor that does not
chain to this(...) (and static initializers run in the static constructor).
The decompiler lifts the initializer from a single constructor, so its
breakpoint was emitted only there and the other constructors had none.
Two causes are addressed:
- The lift discarded the initializer's copies in the other constructors.
They are now kept on MemberInitializerInOtherConstructorsAnnotation and
replayed by SequencePointBuilder, mapping the same source location onto
each constructor's IL.
- PortablePdbWriter only emitted methods that DebugInfoGenerator discovered
through declaration syntax, so a constructor whose declaration is omitted
from the output (implicit default ctor, implicit static ctor, primary
ctor) dropped its generated points. Those functions are now emitted by
walking the sequence-point map directly.
PdbGen fixtures cover single, multiple, this()-chained, implicit, static,
primary-constructor, and field-like event initializers, pinning the
reconstructed breakpoint map against the C# compiler's.
Assisted-by: Claude:claude-opus-4-8:Claude Code
ILFunction.IsAsync is derived from the method signature, so .NET 11
runtime-async methods (MethodImplAsync bit, no MoveNext state machine)
report IsAsync without AsyncAwaitDecompiler ever populating
AsyncDebugInfo. Its Awaits then stays an uninitialized ImmutableArray,
and PortablePdbWriter threw an NRE building the MethodSteppingInformation
blob from that default struct. Runtime-async methods have no yield/resume
offsets to record, so guard on Awaits.IsDefault and omit the blob,
matching the C# compiler, which emits no stepping information for them
either. A genuinely zero-await classic state machine keeps an
initialized empty Awaits and is unaffected.
Assisted-by: Claude:claude-opus-4-8:Claude Code
DebugInfoGenerator asserted that every local variable's decompiled type
is equivalent to the metadata local-signature type at its slot. That
holds at IL-read time but not afterwards: variable splitting gives one
slot several typed variables, pinned-region locals are modeled as
pointers (int* vs int& pinned), and generic-context type-parameter
identity differs. The assertion therefore aborted PDB generation (Debug
builds) for ordinary inputs such as any method with a fixed statement.
The type is never written to the PDB - only the slot index and name are -
so the mismatch cannot affect the debugging experience. The slot index,
the only emitted value, is correct by construction: it is the IL
ldloc/stloc operand, sourced from the signature slot when the variable is
created and copied verbatim by SplitVariables; it is never reassigned.
Keep only the index-bound check and document why the type is not verified.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Decompiler warnings (ILFunction.Warnings, e.g. the DetectPinnedRegions
block-duplication notice) are surfaced as an EmptyStatement carrying only
a comment. VisitEmptyStatement prints no semicolon for it, and
EmptyStatement derives its StartLocation/EndLocation from its Location
field, which is only set when that semicolon token is written. The
statement was therefore left without a text location, and
SequencePointBuilder then asserted on the empty start location while
generating PDB sequence points - aborting PDB generation for any assembly
whose decompilation emits such a warning (e.g. System.Net.Requests).
Point the empty statement at the comment it carries (already printed by
the time the node ends, so its location is known), falling back to the
collapsed end-of-last-token position. Every printed node then has a
location, so the SequencePointBuilder invariant holds without
special-casing, and the statement lines up with the text it represents.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The pretty DynamicTests exercised arithmetic and relational binary operators on
dynamic operands, but never the bitwise and shift operators (& | ^ << >>). Add a
BitwiseAndShiftBinaryOperators case so this operator family is pinned by a
round-trip test alongside the others.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The pretty DynamicTests only exercised += -= *= /= on a dynamic target, leaving
%= &= |= ^= <<= >>= unverified even though VisitDynamicCompoundAssign and
GetAssignmentOperatorTypeFromExpressionType already map them. Add them so the
full set of dynamic compound-assignment operators is pinned by a round-trip
test.
Assisted-by: Claude:claude-opus-4-8: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
VisitDynamicUnaryOperatorInstruction handled every dynamic unary operator
except ExpressionType.OnesComplement, so ~x on a dynamic operand fell through
to the unsupported-opcode error expression and produced uncompilable output
(an incomplete cast that fails to parse). Map it to the bitwise-complement
operator, like the sibling unary cases.
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
The PDB sequence-point tests were missing real while-loop input, and their residual comparison treated breakpoint locations as an unordered multiset. Add coverage for while/do-while fixtures and compare residuals in sequence order so stepping-order changes are pinned.
Assisted-by: CodeAlta:gpt-5.5:CodeAlta
Extends the breakpoint-map comparison to hidden sequence points, anchoring
each hidden point to the visible point it follows so the descriptor stays
independent of the IL offsets the decompiler reconstructs. Adds PdbGen cases
spanning try/catch/finally, switch, async/await, yield, loops, LINQ, pattern
matching and more, pinning the known residuals where the decompiler folds a
compiler-hidden branch into an adjacent point.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The PdbGen tests compared the reconstructed PDB to the C# compiler's
byte-for-byte, so any non-trivial method failed on reconstructed IL
ranges, hidden sequence points and local scopes - none of which the
decompiler can reproduce exactly. That left four of seven fixtures
[Ignore]d and the suite with almost no coverage.
Compare only what a debugging user actually feels: the visible (non-hidden)
breakpoint map, parsed straight from the sequence-point blobs and keyed by
method-definition row (shared between the PDB and the PE it describes). IL
offsets, hidden points, local scopes and the embedded source are dropped.
The compiler's own PDB is the oracle, so the tests assert correct debugging
behavior rather than the decompiler's past output. Methods where the
decompiler legitimately diverges pin an auto-derived residual snapshot, the
same accept-the-diff workflow as the pretty tests; a separate oracle-free
check rejects duplicate or overlapping sequence points.
Un-ignores ForLoopTests, LambdaCapturing and Members (its source is
regenerated to match the decompiler's per-type output, collapsing ~50 lines
of indentation-induced coordinate noise to two genuine differences).
Assisted-by: Claude:claude-opus-4-8:Claude Code
F# emits tail calls pervasively, but the 'tail.' prefix was dropped
entirely at the C# output stage, so the information never reached the
decompiled text. Render it inline as '/*tail.*/' before the call,
mirroring the existing 'constrained.' prefix comment in CallBuilder.
Fix#3817
Assisted-by: Claude:claude-opus-4-8:Claude Code
The project listed 201 explicit <Compile Include> and 119 <None Include> items
with EnableDefaultItems=false. Many test-case sources had been marked <None>
only because the C# compiler available when they were written could not build
them; the current Roslyn compiles them fine. The hand-maintained lists had also
drifted: five committed fixtures (Operators.cs, Issue3751.cs, three ILPretty
.cs) were on disk with passing tests but in no item list.
Switch to the SDK default **/*.cs glob and exclude only the sources that still
cannot be compiled into the test assembly, determined empirically by building
with everything included and removing what failed:
- IL-pretty inputs that are not valid standalone C# and *.Expected.cs golden
outputs that reuse type names (compile errors / duplicate definitions).
- MetadataAttributes.cs, which applies assembly/module attributes that break
NUnit test discovery (zero tests found) when compiled into the assembly.
The excluded sources stay <None> for IDE visibility; the harness compiles them
from disk at test time regardless. This compiles 23 more fixtures than before
while keeping every previously-compiled file. Default None globbing stays off so
the non-C# inputs (.il/.vb/.fs) remain the authoritative list.
Operators.cs, now part of the build, is normalized to the repo's tab indentation
by the format hook (a CS110 block used spaces). Verified: the full suite still
reports 2257 passed, 0 failed, 35 skipped.
Assisted-by: Claude:claude-opus-4-8:Claude Code
A stackalloc whose result is a pointer is only valid C# as the initializer of a
pointer-typed local. The inliner moved a single-use pointer stackalloc into its
use, producing e.g. 'K.V(stackalloc int[3] { 1, 2, v })' or '*stackalloc ...';
in an expression position the stackalloc is typed as Span<T>, which does not
convert to a pointer, so the output did not compile. Keep such a stackalloc as a
separate local. Moving it into a local store (its declaration) and into the
Span<T>/ReadOnlySpan<T> constructor stay allowed, since those are the positions
where the pointer or span form is exactly what is wanted.
Found while exploring stackalloc-initializer coverage.
Assisted-by: Claude:claude-opus-4-8:Claude Code
HandleSequentialLocAllocInitializer formed a stackalloc initializer whenever the
explicit stores were contiguous from offset 0, even if they did not cover the
whole buffer. A 'stackalloc byte[16]' reinterpreted as int and written through
three of its four elements decompiled to 'stackalloc int[4] { 1, v, 3 }', whose
initializer has fewer elements than the declared length and does not compile.
Require every element to be written (from the constant data blob or an explicit
store) before forming the initializer; otherwise the buffer stays a plain
stackalloc with individual stores.
Found while exploring stackalloc-initializer coverage.
Assisted-by: Claude:claude-opus-4-8:Claude Code
A constant-size stackalloc initializer stores its constant elements through a
data blob (cpblk from a <PrivateImplementationDetails> field). ReadElement
decoded every element by width, so a 4-byte float was read as Int32 and an
8-byte double as Int64. The resulting constant carried the raw bit pattern
(1f decoded as 1.0653532E+09f) and its stack type no longer matched the store,
tripping StObj.CheckInvariant. Dispatch on the element's type code so Single and
Double are read as floating-point, matching the heap-array decoder.
Found while exploring stackalloc-initializer coverage around the element-type
hint fix; floating-point element types had no test case.
Assisted-by: Claude:claude-opus-4-8:Claude Code
A constant-size stackalloc initializer whose buffer is only passed to a static
call (never dereferenced as a typed pointer) threw "given Block is invalid!".
TranslateStackAllocInitializer recovers the element type from the surrounding
type hint, but that hint is unreliable for this shape: the constant allocation
size is folded to a byte count, so it can no longer be read off a
'count * sizeof(T)' expression, and the buffer is kept on the stack as a native
int instead of a T* local, leaving the hint a non-pointer. The guard only
repaired an incompatible pointer hint, so a non-pointer hint fell through to a
'byte' element type that is incompatible with the actual stores, and the
per-element check threw. Derive the element type from the type being stored
whenever the hint is not already a compatible pointer.
The regression is driven by the code shape, not the compiler version: in
optimized builds the buffer is kept on the stack as a native int whenever it is
passed to a static call without being dereferenced. This reproduces across
Roslyn versions, including the pinned test compiler, so the added fixture is red
without this fix in the optimized configurations.
Assisted-by: Claude:claude-opus-4-8:Claude Code
Source locations were virtual, computed by recursing to the first and last
child, whose leftmost and rightmost leaves are token nodes; sequence-point
coordinates likewise came from reconstructed token nodes. Store locations as
fields assigned while printing, and derive sequence-point coordinates from the
surrounding real nodes plus the decompiler's fixed formatting, so neither
depends on token children. The using/foreach await modifier becomes a plain
bool field. Characterization gates lock the emitted locations and PDB
coordinates, which are unchanged.
Assisted-by: Claude:claude-opus-4-8:Claude Code
The "IL with C#" view decompiles each method body as a bare handle, so a
static constructor is decompiled without its type's field declarations in
the syntax tree. MoveFieldInitializersToDeclarations then could not find a
declaration to move the static-field-initializer statement onto, asserted
(kind was Static, not Primary) and dropped the statement -- crashing Debug
builds and silently losing the assignment in Release.
Dropping the statement is only correct for the primary-constructor case,
where the assignment's backing member is synthesized and has no separate
declaration. For static/instance initializers a missing declaration just
means the member is not part of this partial syntax tree, so the
assignment must remain in the constructor body.
Assisted-by: Claude:claude-opus-4-8:Claude Code
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