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
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
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
Decompiler.Tests targeted net11.0-windows only because
Microsoft.DiaSymReader.Native is Win-only and Tester.cs hardcoded ilasm.exe
/ ildasm.exe filenames. Both are narrow problems: gate the Native package
to Windows, replace the ".exe" suffix with an OS-aware helper, and drop
the -windows TFM. The Microsoft.NETCore.IL{,D}Asm metapackages' RID graph
already ships ilasm/ildasm for linux + osx via per-RID runtime sub-packages
when RuntimeIdentifier resolves dynamically, so the tool just lands in the
test output dir with the right suffix on every host.
Windows-only test fixtures (RoundtripAssembly + the RunWithTestRunner,
SignAssembly, FindMSBuild call sites) gate via Assert.Ignore +
[Platform("Win")] so they skip on non-Windows instead of failing. CA1416
joins NoWarn -- the analyzer can't see the runtime guards, and the test
inputs (Console.CapsLock, Registry access) are decompilation targets, not
infrastructure.
BuildTools/pre-commit learns to find dotnet-format on non-Windows (XDG
basedir + no .exe suffix) so the hook can run wherever the build now does.
Assisted-by: Claude:claude-opus-4-7:Claude Code
The flag-based early-return rewriter was tied to one specific lowered shape:
the try body's flag-setter had to be exactly `stloc flag(K); leave try`, the
post-try check had to be a `br checkBlock` (not an inline `IfInstruction`), and
the early path had to be a direct Leave or a forward to a one-instruction
leave-block whose target was the function body. None of those hold for
`try { try { return X; } finally { await ... } } finally { await ... }`:
- The inner flag-setter has a leading capture-forwarding store
(`stloc capture(X); stloc innerFlag(K); leave inner-try`).
- The inner check-block's early path branches to a multi-instruction helper
that sets the *outer* flag and leaves the outer try, instead of being a
direct return.
- SplitVariables hands out a separate ILVariable for the pre-init flag store
when the in-handler store is in a disjoint dataflow region.
Rebuild the matcher around the idea of a "template" — the chain of stores
the early path performs before its terminating Leave. Each flag-setter then
becomes its own prefix stores + a clone of the template, which collapses the
inner-then-outer flag chain in two passes (inner first, outer second, because
descendant order visits the inner TryFinally first). Also extend the
flag-setter scan to walk the whole try-block's descendants — after the inner
rewrite, the inner's spliced flag-setter lives inside the inner-try container
but still leaves outwards to the outer try, so it's an outer flag-setter from
the outer's perspective.
Add a `RUNTIMEASYNC` preprocessor symbol (defined when `EnableRuntimeAsync`
is set) and gate the new return-from-try-finally fixtures on it — the
state-machine async pipeline doesn't recover this shape, so it would expand
the same source into the `int result; try { ...; result = X; } finally { ... }
return result;` verbose form and the Async (state-machine) pretty test would
regress.
Closes Cluster 1 (1.1, 1.3) from #3745. Cluster 1.2 (void `return;` at the
end of a try-finally body) and 1.4 (break/continue across a try-finally) are
left for a follow-up: both round-trip semantically equivalently but the AST
emitter drops a trailing void `return;` and the break/continue lowering uses
a switch dispatch that the current single-K matcher can't recognize.
Detect MethodImplOptions.Async (0x2000) in ILReader and unpack Task/Task<T>
return types so the IL Leave value and function.AsyncReturnType match the
source signature. Add CSharp15_0 (Preview also bumped to 1500) and a
RuntimeAsync setting (default on, gated to >=CSharp15_0), expose it in the
Languages dropdown, mask the synthetic MethodImplAsync bit out of the
decompiled [MethodImpl], and add a .runtimeasync test suffix.
* .NET 11 RC2 minimal changes
* Heuristic for transport feed Roslyn selection
* Microsoft.CodeAnalysis.NetAnalyzers from main NuGet feed
* Use the VS2026 image
* Switch all test projects to net11
* Extract constants
* Include vsix with plain nuget.config files
* Basics of net8.0. Breaking unit tests expected.
* Missed that TestRunner project was already upgraded to net7.0 (search and replace fail)
* Use Preview 6 locally
* Use .NET 8.0 RTM
* Final fixups
---------
Co-authored-by: Christoph Wille <christoph.wille@gmail.com>