Generate ID strings directly from metadata instead of the type system,
so callers holding only a MetadataFile and an EntityHandle do not need
to materialize a compilation first, and raw metadata names survive
verbatim. GetIdString(IEntity) stays as a thin wrapper over the new
implementation, keeping the published entity-based entry point intact.
The implementation is validated by a differential suite comparing every
symbol of a test corpus against Roslyn's GetDocumentationCommentId.
Corners pinned by those tests: generic arguments distribute to their
nesting level (Outer{A}.Inner{B}), op_CheckedExplicit carries the
~ReturnType suffix like the other conversion operators, and custom
modifiers are ignored like Roslyn ignores them (a virtual method's 'in'
parameter is modreq(InAttribute) but documented as T@). Array shapes
use the spec's lowerbound:size notation, covered by a hand-built
module, since C# cannot express non-default array bounds in signatures.
Assisted-by: Claude:claude-fable-5:Claude Code
The netcore-2.2 reference set consists of the shared framework's facade
assemblies split across many files, and vbc only binds special types
like System.Void from an assembly that defines them rather than
following type forwards, so without an implicit SDK it needs the same
reference list as the C# side. The VB runtime must come from the legacy
reference set: before .NET Core 3.0 there is no
Microsoft.VisualBasic.Core.dll and the core build of the VB runtime is
a trimmed-down subset (no UBound etc.). Referencing the target
framework's own Microsoft.VisualBasic facade alongside that -vbruntime
choice is a BC32210 identity conflict, so it is dropped from both the
default reference list and the ReferenceVisualBasic flag handling.
All of this applies only where vbc runs without its implicit desktop
SDK path, i.e. off Windows; on Windows vbc.exe keeps the plain
reference list that already worked.
Assisted-by: Claude:claude-fable-5:Claude Code
The Roslyn 1.3.2 and 2.10.0 configurations were excluded from the
compiler matrix on non-Windows platforms because Microsoft.Net.Compilers
only ships .NET Framework executables. Both can be enabled:
- Roslyn 2.10.0 has a dotnet-hosted sibling package,
Microsoft.NETCore.Compilers, whose tools/bincore/csc.dll runs on the
installed runtime with --roll-forward LatestMajor (its runtimeconfig
pins the out-of-support .NET Core 2.0). Fetched on non-Windows into
the version's tools/bincore directory; GetCSharpCompiler probes for a
direct csc.dll next to the installed path in addition to the
bincore/ subfolder layout of the newer toolset packages.
- Roslyn 1.3.2 has no .NET build; when a mono executable is found on
the PATH, it is kept in the matrix and WrapCompiler hosts the .exe
compilers through mono. Because the native DiaSymReader needed for
Windows PDBs is unavailable there, GeneratePdb requests portable
PDBs on non-Windows (Roslyn 2.x+ falls back on its own, 1.x needs
the explicit -debug:portable).
The mcs configurations stay excluded: the bundled mcs 2.6.4 needs the
Reflection.Emit COMPILER_ACCESS mode that current Mono runtimes no
longer implement. Old-compiler configurations also stay excluded from
correctness-style fixtures: their output targets .NET Framework or
.NET Core 2.2, which the runners cannot execute here.
Microsoft.NETCore.Compilers-2.10.0.nupkg should be added to
ILSpy-tests/nuget to keep the fetch offline-capable.
Assisted-by: Claude:claude-fable-5: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
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>