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.
`try { throw new ...(); } finally { await ... }` lowers to a try whose only
exit is the throw (handled by the synthetic catch). The existing matcher
required at least one outward Branch to the continuation, which is too strict
— a throw-only try body produces zero outward branches but is still a valid
lowered shape. Two follow-on fixes were also needed:
- The pre-init's ILVariable diverges from the in-handler store after
SplitVariables when the try body has no path that reaches the dispatch's
load without going through the catch; match the flag init by slot/kind/type
instead of identity (same workaround the multi-handler matcher uses).
- With a throw-only try body the new TryFinally has unreachable endpoint,
so appending the no-exception successor after it would put a non-final
unreachable-endpoint instruction in the parent block. Skip the append in
that case — the parent block's endpoint is already correctly unreachable.
Closes Cluster 4 from #3745.
The single-handler try-catch matcher was tied to the top-level shape: it
required the try-catch be the last instruction in its parent block, that the
post-catch "no exception" path be a direct Leave that exits the function, and
that the flag-init's ILVariable be identical to the in-handler flag store.
None of those hold for an inner try-catch sitting inside an outer try-finally
where both await — the inner is followed by a `br continuation`, the no-exception
path leaves the outer try-block (not the function), and SplitVariables hands
out a separate ILVariable for the pre-init store.
Drop the "must be last instruction" gate, accept Leave-to-any-ancestor and
cross-container Branch as the no-exception exit (extracted into a new
`IsContainerExit` helper), and match the flag-init by slot/kind/type the same
way the multi-handler matcher already does.
Closes Cluster 3 from #3745.
Two methods exercise `Task<int>.ConfigureAwait(bool)`: a single false-flag form
and a mixed false/true form that combines two awaits in a return expression.
Both cases run through the regular state-machine and runtime-async pipelines
(RuntimeAsync reuses Async.cs as its source).
Gated by `#if ROSLYN2` because Roslyn 2+ preserves named-argument metadata at
the call site, so the decompiler renders `continueOnCapturedContext: false`
when the binary was compiled by Roslyn 2+ and positional `false` for default
csc / Roslyn 1.3.2.
Also adds NoInliningTaskMethod — an async method carrying
[MethodImpl(MethodImplOptions.NoInlining)] — so the runtime-async path
exercises the impl-attribute masking added in the scaffolding commit:
MetadataMethod strips the synthesized MethodImplOptions.Async (0x2000) bit
from the decompiled output, and unrelated impl bits like NoInlining (0x0008)
must still render in the surfaced [MethodImpl(...)] attribute.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When a return crosses an enclosing try-finally with await, runtime-async lowers it as: capture the return value, set an int flag to a unique non-zero value, leave the try block normally so the finally runs, then post-finally check "if (flag == K) return capture;". Detect that pattern after my outer try-finally rewrite (or, in optimized builds, the compiler-emitted TryFinally directly) and replace each capture-flag-and-leave site with a direct "leave outer (capture)" — the leave still passes through the TryFinally, so the user's finally body executes before the function returns, which matches the source-level semantics.
Handles both the "if (flag == K)" and "if (flag != K)" check forms (the optimizer emits the latter). Closes the last gap in Issue2436 — RuntimeAsync now passes both Optimize and non-Optimize modes; the full RuntimeAsync* sweep is 12/12 green.
Also remap reads of the captured-obj local inside the cleaned filter so optimized builds (where Roslyn inlines the typed-cast directly into the user filter expression instead of stashing it in a local) render against the catch variable rather than against "((T)obj)".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
For an async method on a value type Roslyn cannot keep a managed reference to the caller's struct alive across an await, so it copies *this into a local at method entry and rewrites every "this.field" access to go through the copy. The decompiler then sees an extra "AsyncInStruct asyncInStruct = *this;" prelude and renders user-level "i++" as "asyncInStruct.i++". State-machine async normally avoids this because TranslateFieldsToLocalAccess already remaps the captured-this field back to the function's own this parameter.
Detect the prelude in runtime-async methods (entry-point stloc V_X(ldobj T(ldloc this)) with the local typed as the containing value type) and rewrite every "ldloc V_X" / "ldloca V_X" to go through the function's this parameter instead, then drop the now-dead copy. The mutation semantics are unchanged — runtime-async struct methods never reflect mutations back to the caller anyway, so re-pointing the access at this is purely a fidelity restoration.
Brings AsyncInStruct.Test back to its source ("i++" / "i + xx.i"). The only remaining failure in RuntimeAsync is Issue2436 (early-return-from-nested-catch encoded as a flag).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Roslyn's runtime-async lowering uses AsyncHelpers.Await(Task) for Task awaitables (already handled by TransformAsyncHelpersAwaitToAwait in EarlyExpressionTransforms) but emits a manual GetAwaiter / get_IsCompleted / AsyncHelpers.UnsafeAwaitAwaiter / GetResult sequence for non-Task awaitables — YieldAwaitable, ConfiguredCancelableAsyncEnumerable.Enumerator from await foreach, etc. Add a new RuntimeAsyncManualAwaitTransform invoked from AsyncAwaitDecompiler's runtime-async dispatch that recognizes the three-block shape (head with stloc awaiter + IsCompleted check + branch, pause block calling UnsafeAwaitAwaiter, completed block starting with GetResult), strips the suspend machinery, and replaces the GetResult call with an Await IL instruction. When GetAwaiter takes the address of a temporary set in the same block, also drop the temporary store and use the underlying awaitable expression.
This collapses the LoadsToCatch await-Task.Yield bodies. AsyncForeach should benefit too (its MoveNextAsync awaits go through this path).
When the user writes multiple catch clauses on a single try, runtime-async lowers each catch's body to "[stloc tmp(ex);] [stloc obj(...);] stloc num(K_i); br continuation" with a unique K_i per handler, and the post-catch flow becomes a switch dispatch on `num` that branches to each user-level catch body. Add a TryRewriteMultiHandlerTryCatch driver that mirrors the single-handler match (using NormalizeRuntimeAsyncFilter for filter cleanup), recognizes the post-catch SwitchInstruction, and uses the existing dominator-based block move to relocate each switch case into the corresponding handler body, remapping that handler's per-handler synthesized variables (and the shared filter obj) back to the catch variable.
The shared obj local can no longer be remapped function-wide during filter normalization — that would tag every dispatch idiom with whichever handler ran first — so record the obj per handler in a dictionary and let TryRewriteTryCatch / TryRewriteMultiHandlerTryCatch remap it scoped to each moved catch body. The pre-init "stloc num(0)" is matched by slot index rather than ILVariable identity, since SplitVariables splits the dead pre-init off from the in-handler stores.
Resolves the LoadsToCatch case. Filter normalization extends to the typeless `catch when (filter)` form (isinst Object in the filter), recovered as `catch when` in the AST output. Remaining failures in RuntimeAsync are now multi-await expressions, async-in-struct, and a couple of unrelated decompilation issues.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Roslyn lowers `catch (T ex) when (filter)` to `catch object when (BlockContainer { isinst T; obj=ex; <filter> })` even when T is `object` (the source-level `catch when (filter)` form). Run a pre-pass over every catch handler that matches the four-block diamond (entry isinst-gate, trueBody with obj-store + user filter, falseBody constant-false, merge leave-with-result), strip the obj-store machinery, retype the handler variable when T is a more specific type than object, and remap reads of the synthesized obj/tmp/typedEx variables back to the handler variable. After that the catch body is the same simple flag-store shape that TryRewriteTryCatch already handles, so the existing match runs unchanged.
Resolves the RethrowDeclaredWithFilter and ComplexCatchBlockWithFilter cases. Multi-handler catches (LoadsToCatch) still fail because they use a multi-valued discriminator that isn't reduced yet.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Roslyn's runtime-async lowering flattens these into a TryCatch[object] with a captured-rethrow pattern (try-finally) or a TryCatch[T] with a flag-int discriminator and a guarded post-catch body (try-catch). Add a new transform invoked from AsyncAwaitDecompiler when the state-machine matches fail and the method has the runtime-async impl bit; it pattern-matches both shapes and rewrites them back to TryFinally / TryCatch with the original catch body inlined into the handler.
The state-machine and runtime-async lowerings of try-finally use the same catch-handler shape and the same dominator-based finally-body extraction, so promote those to internal static helpers (MatchObjectStoreCatchHandler, MoveDominatedBlocksToContainer) on AwaitInFinallyTransform and call them from the new transform. Filter-bearing catches and multi-handler tries are still left to the standard pipeline.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Convert `call System.Runtime.CompilerServices.AsyncHelpers.Await(value)`
to the IL Await instruction whenever DecompilerSettings.RuntimeAsync is
enabled. The state-machine async pipeline (AsyncAwaitDecompiler) already
produces the IL Await for downstream transforms (UsingTransform's
MatchDisposeBlock pattern-matches on it via UnwrapAwait); doing the
conversion in EarlyExpressionTransforms gives the runtime-async output
the same canonical shape before any consumer runs.
Add TaskType.UnpackAnyTask and use it in ILReader.Init / ReadIL so methodReturnStackType and function.AsyncReturnType agree for runtime-async methods that return ValueTask/ValueTask<T> or any [AsyncMethodBuilder]-attributed custom task type. Previously only Task/Task<T> were unpacked, leaving AsyncReturnType=void while the IL Leave value carried the unpacked element type, which tripped the StackType assert in ExpressionBuilder.Translate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.
Three small follow-ups to #3705's lazy-resource fix:
* Replace `_ = RefreshInternalAsync()` with
`RefreshInternalAsync().HandleExceptions()`. The previous fire-and-
forget discard silently swallowed any exception thrown outside the
explicit Catch helper on GetMetadataFileAsync; HandleExceptions is
the codebase's idiomatic helper that surfaces the failure into the
decompile text view.
* Look up the path-root assembly via AssemblyList.FindAssembly (an
O(1) hash lookup keyed by OrdinalIgnoreCase, with Path.GetFullPath
canonicalisation) instead of an O(n) foreach with case-sensitive
string equality. Same shape used by the rest of the codebase.
* Detect the user navigating during the GetMetadataFileAsync await
(which can take ~2s for a sizeable WPF assembly) and bail out of
the path-restore. Without this, clicking another tree node mid-
refresh would be silently overwritten when the deferred SelectNode
jumped back to the captured pre-refresh path.
No behavior change for the .baml refresh fix itself.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The helper used to catch only FormatException, but the WPF type
converters (e.g. RectConverter for the saved window placement) raise
InvalidOperationException on malformed input — for instance, an empty
string from a partially-written config file. That was enough to
propagate up through SessionSettings.LoadFromXml and crash startup
before the main window could even open, with no recovery path other
than manually editing the on-disk ILSpy.xml.
Treat any conversion failure as "use the default" instead, and treat
empty strings the same as null at the entry. Effect: a single bad
saved value falls back silently and the application starts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ResXResourceWriter is a verbatim port of the Mono implementation
(see file header). Both warnings flag deliberate decisions in the
upstream port that we preserve for fidelity:
* CA1063 — Dispose() is virtual and the protected Dispose(bool) is
not, the inverse of the canonical pattern; keeping the Mono shape.
* CA2213 — the writer's stream / textwriter fields are caller-owned
and intentionally not disposed by the writer.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The C# 9 IntPtr / UIntPtr guard in IsBinaryCompatibleWithType read
type.Kind is not TypeKind.NInt or TypeKind.NUInt
which parses as `(is not NInt) or (is NUInt)` — true unless
Kind == NInt. The intent (per the surrounding comment "but not nint
or C# 11 IntPtr") is "Kind is neither NInt nor NUInt", which needs
parentheses around the alternation:
type.Kind is not (TypeKind.NInt or TypeKind.NUInt)
Effect: when Kind == NUInt the branch no longer mistakenly applies
the C# 9 IntPtr-only restrictions (suppressing compound assignment
without nint, disallowing shifts).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ExtensionDeclaration.SymbolKind (CA1065) — was throwing
NotImplementedException; return SymbolKind.TypeDefinition to match
TypeDeclaration / DelegateDeclaration, since `extension` declarations
are type-level.
* CustomAttribute.DecodeValue (CA2002) — replace `lock(this)` on the
sealed-but-internal class with a private syncRoot field.
* PlainTextOutput (CA1001) — implement IDisposable; track an
ownsWriter flag so we only dispose the underlying TextWriter when
the parameterless constructor created its own StringWriter.
* DotNetCorePathFinder (CA1060) — move the libc realpath / free
PInvokes into a private nested NativeMethods class.
* ILSpy.ReadyToRun (CA1016) — add [assembly: AssemblyVersion("1.0.0.0")]
in Properties/AssemblyInfo.cs to match the BamlDecompiler plugin.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Four cases where the analyzer rule conflicts with intentional design:
* EmptyList<T>.IDisposable.Dispose (CA1063) — explicit IDisposable on
IEnumerator<T>; making it public would conflict with the rest of the
IList<T> / IEnumerator<T> surface.
* MetadataFile.SectionHeaders (CA1065) — throw documents that this
MetadataFileKind has no PE sections; PE-like derived kinds override.
* LongSet.GetHashCode + LongSet itself (CA1065 + CA2231) — explicit
guards against using LongSet in hash containers / via equality
operators; SetEquals is the supported comparison and
IEquatable<LongSet>.Equals is itself [Obsolete].
* AnnotationList.Clone (CA2002) — AnnotationList is a private nested
type; the surrounding Annotatable class deliberately locks on the
AnnotationList instance to serialize annotation reads/writes, and
external code cannot obtain a reference to it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
MetadataFile now declares IDisposable using the canonical pattern
(public non-virtual Dispose() + protected virtual Dispose(bool)).
PEFile and WebCilFile become sealed and override Dispose(bool) to
release the PEReader and MemoryMappedViewAccessor they own;
ResourcesFile is also sealed. PortableDebugInfoProvider disposes the
MetadataReaderProvider it owns. LoadedAssembly implements IDisposable
and disposes both the loaded MetadataFile and the debug-info provider.
AssemblyList.Unload / Clear / ReloadAssembly / HotReplaceAssembly now
dispose the LoadedAssembly instances they evict, fixing a resource leak
where every "Reload Assembly" held the previous PEReader (and the
underlying file handle / memory-mapped view) alive until GC eventually
finalized it.
The disposal contract terminates at the AssemblyList tier: downstream
holders of MetadataFile (MetadataModule, DecompilerTypeSystem,
AssemblyListSnapshot, ...) hold borrowed references rather than owned
ones, so making the base IDisposable does not cascade into CA1001 /
CA2213 warnings elsewhere.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
stdout is the data channel: --resource may be writing a binary payload to
it. Update-check notices on stdout would tail-append to the binary stream
and corrupt extracted files. Move them to stderr (npm/cargo/pip
convention) so meta-output stays out of the data path.
--list-resources prints every leaf in the assembly's embedded
resources (including individual entries inside .resources containers).
--resource <name> extracts a single leaf. Names ending in .baml are
decompiled to XAML; everything else is emitted as raw bytes. Without
-o, BAML goes to stdout as text and binary leaves go to the binary
stdout stream. Unknown names exit with EX_DATAERR and print the
available leaves to stderr.
--decompile-baml is a modifier for -p that swaps in the new
BamlAwareWholeProjectDecompiler so .baml entries become .xaml Page
items in the generated project. Default -p behaviour is unchanged.
ResourceExtensions exposes the leaves of an assembly's embedded
resources — including individual entries inside .resources containers
— so callers can list and extract them by a stable path.
BamlAwareWholeProjectDecompiler subclasses WholeProjectDecompiler and
converts .baml entries to .xaml Page items, mirroring the pattern in
ILSpy.BamlDecompiler.BamlResourceFileHandler so PartialTypeInfo flows
through and the C# decompiler suppresses the duplicate Connect()
overrides.
* .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
* Use releaseTag with fallback to downloadUrl in updates.xml
* Add tests
* Prevent arbitrary downloadUrl - must start with BaseUrl as well
* Remove custom domain ilspy.net in end-user visible places