Files
ILSpy/ICSharpCode.Decompiler.Tests/TestCases/Pretty/DynamicAwait.cs
T
Siegfried Pammer 8ab3f93f5e Fix #3704: keep the static type of a dynamic call target
A dynamic call site that names a type passes typeof(T) as its target. When
a later argument contains control flow, the compiler stores that typeof in
a temporary. ILInlining does not undo this: it inlines a store only into
the immediately following instruction, so an intervening statement leaves
the typeof behind, and the expression builder printed the temporary instead
of the type. Substituting the typeof back into the target slot is not an
option either, because a call there has side effects and blocks inlining of
the remaining arguments.

The type is therefore stored on the instruction. Object creation already
did this, but kept the field private, so the expression builder re-derived
the type from the target argument and failed on the spilled form. Member
invocation gets the same field. Both drop the target from Arguments, so the
dead-argument handling removes the store; ArgumentInfo keeps its entry for
the target, because the invocation symbol is built from it.

The type of an object creation is not nullable: the transform returns
before constructing the instruction when it cannot match the typeof, and
substitutes the unknown type otherwise. An unresolvable type therefore
reaches the expression builder as the unresolved type it is, and prints as
`?` like any other, rather than being indistinguishable from a missing one.

Those two are also the only binder method kinds that ever see
CSharpArgumentInfoFlags.IsStaticType. Every other way of naming a type as
the receiver binds statically and leaves only a conversion of the dynamic
operand behind.

Assisted-by: Claude:claude-opus-5[1m]:Claude Code
2026-08-29 22:41:05 +02:00

203 lines
4.1 KiB
C#

using System;
using System.Threading.Tasks;
namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
{
internal class DynamicAwait
{
private static Task DoAsync(object o)
{
return Task.CompletedTask;
}
private static dynamic GetDynamic()
{
return null;
}
private static Task<int> GetIntAsync(object o)
{
return Task.FromResult(0);
}
private static Task<object> GetObjAsync()
{
return Task.FromResult<object>(null);
}
// --- dynamic operations around a normal (real-Task) await ---
private static async Task SetMemberBeforeAwait(dynamic d)
{
d.Before = 1;
await DoAsync(null);
}
private static async Task SetMemberAfterAwait(dynamic d)
{
await DoAsync(null);
d.After = 1;
}
private static async Task SetMemberAroundAwait(dynamic d)
{
d.Before = 1;
await DoAsync(null);
d.After = 2;
}
private static async Task InvokeBeforeAwait(dynamic d)
{
d.Before();
await DoAsync(null);
}
private static async Task InvokeAfterAwait(dynamic d)
{
await DoAsync(null);
d.After();
}
private static async Task DynamicInLoopWithAwait(dynamic d)
{
for (int i = 0; i < 10; i++)
{
d.Add(i);
await DoAsync(null);
}
}
private static async Task DynamicWhileConditionWithAwait(dynamic d)
{
while ((bool)d.HasNext)
{
await DoAsync(null);
}
}
private static async Task TwoAwaitsWithDynamicBetween(dynamic d)
{
await DoAsync(null);
d.Middle = 1;
await DoAsync(null);
}
private static async Task DynamicConditionGuardingAwait(dynamic d)
{
if ((bool)d.Flag)
{
await DoAsync(null);
}
}
private static async Task DynamicAwaitInTryCatch(dynamic d)
{
try
{
d.Before = 1;
await DoAsync(null);
}
catch
{
}
}
// --- dynamic feeding the awaited expression (awaiter stays a real Task) ---
private static async Task DynamicConvertedArgumentToAwaitedCall(dynamic d)
{
await DoAsync((string)d.Value);
}
private static async Task AwaitTaskFromDynamicCast(dynamic d)
{
await (Task)d.GetTask();
}
private static async Task<int> AwaitIntTaskFromDynamicCast(dynamic d)
{
return await (Task<int>)d.GetIntTask();
}
// --- awaited result feeding a dynamic operation ---
private static async Task StoreAwaitResultInDynamicMember(dynamic d)
{
d.Result = await GetIntAsync(null);
}
private static async Task PassAwaitResultToDynamicInvoke(dynamic d)
{
d.Consume(await GetObjAsync());
}
private static async Task DynamicIndexerWithAwaitedIndex(dynamic d)
{
d[await GetIntAsync(null)] = 1;
}
private static async Task<object> DynamicBinaryOpWithAwaitedOperand(dynamic d)
{
return d + await GetIntAsync(null);
}
private static async Task<int> DynamicConvertOfAwaitedResult()
{
return (int)(dynamic)(await GetObjAsync());
}
// --- real-world shapes from the referenced issues ---
// awaited dynamic stored in a dynamic local, then used (#1388)
private static async Task<dynamic> AwaitDynamicIntoUsedLocal()
{
dynamic val = await GetDynamic();
val.UseMe();
return val;
}
// dynamic-dispatched awaited call with a dynamic-converted result (#1928)
private static async Task<string> AwaitDynamicConvertResult(dynamic d)
{
return (string)(await d.RunAsync());
}
// --- static-type call target spilled across an await ---
private static void StaticMember(dynamic d, int value)
{
}
private static async Task StaticTargetWithAwaitedArgument(object o)
{
DynamicAwait.StaticMember((dynamic)o, await GetIntAsync(null));
}
// --- fully dynamic awaiter (the awaitable itself is dynamic) ---
private static async Task AwaitDynamicValue(dynamic d)
{
await d;
}
private static async Task AwaitDynamicMethodResult(dynamic d)
{
await d.RunAsync();
}
private static async Task AwaitDynamicLocalWithStatementBefore()
{
// Optimized builds drop the local's debug name, so the decompiler regenerates one from the type.
#if OPT
dynamic dynamic = GetDynamic();
Console.WriteLine("before await");
await dynamic;
#else
dynamic x = GetDynamic();
Console.WriteLine("before await");
await x;
#endif
}
}
}