Skip to content

Decompile await on dynamic expressions#3872

Open
siegfriedpammer wants to merge 1 commit into
masterfrom
dynamic-await
Open

Decompile await on dynamic expressions#3872
siegfriedpammer wants to merge 1 commit into
masterfrom
dynamic-await

Conversation

@siegfriedpammer

Copy link
Copy Markdown
Member

Awaiting a dynamic value was not decompiled. Because the async decompiler runs before DynamicCallSiteTransform, GetAwaiter/IsCompleted/GetResult were still dynamic callsites that await detection could not recognize, so the method was emitted as a raw state machine (or crashed in AnalyzeAwaitBlock, #1388).

What changed

  • AnalyzeStateMachine 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 a single block.
  • DetectAwaitPattern matches the dynamic GetAwaiter/IsCompleted/GetResult shape and emits await expr. A synthesized dynamic GetResult method gives the await (and the awaited local) the dynamic type.
  • DynamicCallSiteTransform follows callsite targets spilled into state-machine locals across an await, so an awaited value flowing into a dynamic callsite (e.g. d.Result = await ..., dynamic JObject() not decompile correctly #1928) decompiles too.

Tests

New DynamicAwait pretty-test fixture covering dynamic awaiters (await d, await d.RunAsync()), dynamic operations around an await, and awaited values feeding a dynamic set-member/invoke/index/binary-op/convert. Full Async* + DynamicTests + DynamicAwait pretty-test suites pass with no regression.

Fixes #1388
Fixes #1928

🤖 Generated with 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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR enhances ILSpy’s async/await decompiler so it can recognize and emit await when the awaited value/awaiter is dynamic, avoiding raw state-machine output and fixing crashes in await analysis for dynamic callsite-based awaiters.

Changes:

  • Normalizes compiler-emitted dual-branch AwaitOnCompleted/AwaitUnsafeOnCompleted patterns so await analysis can proceed consistently even when the awaiter type isn’t statically known.
  • Extends await-pattern detection to match dynamic GetAwaiter/IsCompleted/GetResult callsite shapes and propagate dynamic typing through the await.
  • Enhances dynamic callsite transformation to follow callsite targets spilled across awaits and adds a new DynamicAwait pretty-test fixture + runner entry.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
ICSharpCode.Decompiler/IL/Transforms/IntroduceDynamicTypeOnLocals.cs Marks locals as dynamic when they participate in a dynamic await, improving type reconstruction.
ICSharpCode.Decompiler/IL/Transforms/DynamicCallSiteTransform.cs Refactors callsite detection/transform; adds per-basic-block entrypoint used during async analysis.
ICSharpCode.Decompiler/IL/ControlFlow/ControlFlowSimplification.cs Exposes block-combine helper for reuse by async decompiler post-processing.
ICSharpCode.Decompiler/IL/ControlFlow/AsyncAwaitDecompiler.cs Normalizes dynamic awaiter runtime branches, detects dynamic await patterns, synthesizes dynamic awaiter methods, and coalesces blocks post-transform.
ICSharpCode.Decompiler.Tests/TestCases/Pretty/DynamicAwait.cs Adds new pretty-test cases covering dynamic awaiters and dynamic operations around/through awaits.
ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs Wires up the new DynamicAwait pretty-test suite.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +65 to +70
Dictionary<IField, CallSiteInfo> callsites = new Dictionary<IField, CallSiteInfo>();
FindDynamicCallSitesInBlock(block, callsites, context);
// Deleting the now-unreachable callsite-init blocks is deferred to the SortBlocks call
// at the end of AsyncAwaitDecompiler.AnalyzeStateMachine: this runs while that method is
// iterating over the container's blocks, so removing blocks here would corrupt the loop.
TransformCallSites((BlockContainer)block.Parent, callsites, context);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

dynamic JObject() not decompile correctly Async await on dynamic expression is not detected.

2 participants