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
This commit is contained in:
Siegfried Pammer
2026-08-29 22:41:05 +02:00
parent 7a08c6c57d
commit 8ab3f93f5e
5 changed files with 154 additions and 27 deletions
@@ -162,6 +162,17 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
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)
@@ -23,6 +23,13 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
}
}
private class CtorTarget
{
public CtorTarget(dynamic d, int i)
{
}
}
private struct MyValueType
{
private readonly dynamic _getOnlyProperty;
@@ -368,6 +375,69 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
{
}
private static byte[] GetData()
{
return null;
}
private static void M4(dynamic d, int i)
{
}
private static dynamic M5(dynamic d, int i)
{
return null;
}
private static void M6<T>(dynamic d, int i)
{
}
#if CS60
// #3704: the call target of a dynamic member access on a static type is typeof(DynamicTests).
// The array is loaded into a temporary that survives as its own statement, which strands the
// typeof between the two, so it cannot reach the call site by ordinary inlining.
private static void StaticTargetBehindSurvivingTemporary(dynamic d)
{
#if EXPECTED_OUTPUT
byte[] data = GetData();
DynamicTests.M4(d, (data != null) ? data.Length : 0);
#else
DynamicTests.M4(d, GetData()?.Length ?? 0);
#endif
}
private static dynamic StaticTargetResultUsed(dynamic d)
{
#if EXPECTED_OUTPUT
byte[] data = GetData();
return DynamicTests.M5(d, (data != null) ? data.Length : 0);
#else
return DynamicTests.M5(d, GetData()?.Length ?? 0);
#endif
}
private static void StaticTargetWithTypeArguments(dynamic d)
{
#if EXPECTED_OUTPUT
byte[] data = GetData();
DynamicTests.M6<int>(d, (data != null) ? data.Length : 0);
#else
DynamicTests.M6<int>(d, GetData()?.Length ?? 0);
#endif
}
private static void StaticCtorBehindSurvivingTemporary(dynamic d)
{
#if EXPECTED_OUTPUT
byte[] data = GetData();
new CtorTarget(d, (data != null) ? data.Length : 0);
#else
new CtorTarget(d, GetData()?.Length ?? 0);
#endif
}
#endif
private static void NotDynamicDispatch(dynamic d)
{
DynamicTests.M(d);
@@ -4573,9 +4573,8 @@ namespace ICSharpCode.Decompiler.CSharp
protected internal override TranslatedExpression VisitDynamicInvokeConstructorInstruction(DynamicInvokeConstructorInstruction inst, TranslationContext context)
{
if (!(inst.ArgumentInfo[0].HasFlag(CSharpArgumentInfoFlags.IsStaticType) && IL.Transforms.TransformExpressionTrees.MatchGetTypeFromHandle(inst.Arguments[0], out var constructorType)))
return ErrorExpression("Could not detect static type for DynamicInvokeConstructorInstruction");
var arguments = TranslateDynamicArguments(inst.Arguments.Skip(1), inst.ArgumentInfo.Skip(1)).ToList();
var constructorType = inst.Type;
var arguments = TranslateDynamicArguments(inst.Arguments, inst.ArgumentInfo.Skip(1)).ToList();
var constructor = CreateDynamicConstructorSymbol(constructorType, inst.ArgumentInfo.Skip(1).ToArray());
return new ObjectCreateExpression(ConvertType(constructorType), arguments.Select(a => a.Expression))
.WithILInstruction(inst)
@@ -4585,7 +4584,11 @@ namespace ICSharpCode.Decompiler.CSharp
protected internal override TranslatedExpression VisitDynamicInvokeMemberInstruction(DynamicInvokeMemberInstruction inst, TranslationContext context)
{
Expression targetExpr;
var target = TranslateDynamicTarget(inst.Arguments[0], inst.ArgumentInfo[0]);
var target = inst.StaticTargetType is IType staticTargetType
? new TypeReferenceExpression(ConvertType(staticTargetType))
.WithoutILInstruction()
.WithRR(new TypeResolveResult(staticTargetType))
: TranslateDynamicTarget(inst.Arguments[0], inst.ArgumentInfo[0]);
if (inst.BinderFlags.HasFlag(CSharpBinderFlags.InvokeSimpleName) && target.Expression is ThisReferenceExpression)
{
targetExpr = new IdentifierExpression(inst.Name);
@@ -4595,7 +4598,8 @@ namespace ICSharpCode.Decompiler.CSharp
{
targetExpr = new MemberReferenceExpression(target, inst.Name, inst.TypeArguments.Select(ConvertType));
}
var arguments = TranslateDynamicArguments(inst.Arguments.Skip(1), inst.ArgumentInfo.Skip(1)).ToList();
IEnumerable<ILInstruction> argumentValues = inst.StaticTargetType != null ? inst.Arguments : inst.Arguments.Skip(1);
var arguments = TranslateDynamicArguments(argumentValues, inst.ArgumentInfo.Skip(1)).ToList();
return new InvocationExpression(targetExpr, arguments.Select(a => a.Expression))
.WithILInstruction(inst)
.WithRR(new DynamicInvocationResolveResult(target.ResolveResult, DynamicInvocationType.Invocation, arguments.Select(a => a.ResolveResult).ToArray(), symbol: CreateDynamicInvokeMemberSymbol(inst.Name, inst.ArgumentInfo[0], inst.ArgumentInfo.Skip(1).ToArray(), inst.TypeArguments)));
@@ -172,11 +172,22 @@ namespace ICSharpCode.Decompiler.IL
public IReadOnlyList<IType> TypeArguments { get; }
public IReadOnlyList<CSharpArgumentInfo> ArgumentInfo { get; }
public DynamicInvokeMemberInstruction(CSharpBinderFlags binderFlags, string name, IType[]? typeArguments, IType? context, CSharpArgumentInfo[] argumentInfo, ILInstruction[] arguments)
/// <summary>
/// The type the member is invoked on, set when the target carries CSharpArgumentInfoFlags.IsStaticType;
/// null for a dynamic receiver. When set, the target is not in <see cref="Arguments"/>, but ArgumentInfo[0]
/// still describes it.
/// </summary>
public readonly IType? StaticTargetType;
/// <summary>Offset from an index into <see cref="Arguments"/> to the matching ArgumentInfo entry.</summary>
int ArgumentInfoOffset => StaticTargetType != null ? 1 : 0;
public DynamicInvokeMemberInstruction(CSharpBinderFlags binderFlags, string name, IType[]? typeArguments, IType? context, IType? staticTargetType, CSharpArgumentInfo[] argumentInfo, ILInstruction[] arguments)
: base(OpCode.DynamicInvokeMemberInstruction, binderFlags, context)
{
Name = name;
TypeArguments = typeArguments ?? Empty<IType>.Array;
StaticTargetType = staticTargetType;
ArgumentInfo = argumentInfo;
Arguments = new InstructionCollection<ILInstruction>(this, 0);
Arguments.AddRange(arguments);
@@ -188,6 +199,11 @@ namespace ICSharpCode.Decompiler.IL
output.Write(OpCode);
WriteBinderFlags(output, options);
output.Write(' ');
if (StaticTargetType != null)
{
StaticTargetType.WriteTo(output);
output.Write('.');
}
output.Write(Name);
if (TypeArguments.Count > 0)
{
@@ -202,13 +218,14 @@ namespace ICSharpCode.Decompiler.IL
}
output.Write('>');
}
WriteArgumentList(output, options, Arguments.Zip(ArgumentInfo));
WriteArgumentList(output, options, Arguments.Zip(ArgumentInfo.Skip(ArgumentInfoOffset)));
}
public override StackType ResultType => StackType.O;
public override CSharpArgumentInfo GetArgumentInfoOfChild(int index)
{
index += ArgumentInfoOffset;
if (index < 0 || index >= ArgumentInfo.Count)
throw new ArgumentOutOfRangeException(nameof(index));
return ArgumentInfo[index];
@@ -356,17 +373,21 @@ namespace ICSharpCode.Decompiler.IL
partial class DynamicInvokeConstructorInstruction
{
readonly IType? resultType;
public IReadOnlyList<CSharpArgumentInfo> ArgumentInfo { get; }
public DynamicInvokeConstructorInstruction(CSharpBinderFlags binderFlags, IType? type, IType? context, CSharpArgumentInfo[] argumentInfo, ILInstruction[] arguments)
/// <summary>
/// The type being constructed. The target is never in <see cref="Arguments"/>, but ArgumentInfo[0] still
/// describes it.
/// </summary>
public readonly IType Type;
public DynamicInvokeConstructorInstruction(CSharpBinderFlags binderFlags, IType type, IType? context, CSharpArgumentInfo[] argumentInfo, ILInstruction[] arguments)
: base(OpCode.DynamicInvokeConstructorInstruction, binderFlags, context)
{
Type = type;
ArgumentInfo = argumentInfo;
Arguments = new InstructionCollection<ILInstruction>(this, 0);
Arguments.AddRange(arguments);
this.resultType = type;
}
protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options)
@@ -375,15 +396,17 @@ namespace ICSharpCode.Decompiler.IL
output.Write(OpCode);
WriteBinderFlags(output, options);
output.Write(' ');
resultType?.WriteTo(output);
Type.WriteTo(output);
output.Write(".ctor");
WriteArgumentList(output, options, Arguments.Zip(ArgumentInfo));
WriteArgumentList(output, options, Arguments.Zip(ArgumentInfo.Skip(1)));
}
public override StackType ResultType => resultType?.GetStackType() ?? StackType.Unknown;
public override StackType ResultType => Type.GetStackType();
public override CSharpArgumentInfo GetArgumentInfoOfChild(int index)
{
// ArgumentInfo[0] describes the constructed type, which is not an argument.
index += 1;
if (index < 0 || index >= ArgumentInfo.Count)
throw new ArgumentOutOfRangeException(nameof(index));
return ArgumentInfo[index];
@@ -120,6 +120,8 @@ namespace ICSharpCode.Decompiler.IL.Transforms
storesToRemove.Add(stLoc);
if (value.MatchLdFld(out cacheFieldLoad, out var targetFieldCopy) && cacheFieldLoad.MatchLdsFld(out cacheFieldCopy) && cacheField.Equals(cacheFieldCopy) && targetField.Equals(targetFieldCopy))
storesToRemove.Add(stLoc);
if (TransformExpressionTrees.MatchGetTypeFromHandle(value, out _))
storesToRemove.Add(stLoc);
}
}
}
@@ -149,6 +151,21 @@ namespace ICSharpCode.Decompiler.IL.Transforms
|| (variable.Kind == VariableKind.Local && variable.StateMachineField != null);
}
/// <summary>
/// Matches a GetTypeFromHandle call, or a load of a variable that is assigned one exactly once. The
/// latter form occurs when control flow in a later argument forces the value off the evaluation stack.
/// Any variable kind qualifies: only the type is read here.
/// </summary>
static bool MatchTypeOf(ILInstruction inst, out IType type)
{
if (TransformExpressionTrees.MatchGetTypeFromHandle(inst, out type))
return true;
return inst.MatchLdLoc(out var variable)
&& variable.IsSingleDefinition
&& variable.StoreInstructions.FirstOrDefault() is StLoc initStore
&& TransformExpressionTrees.MatchGetTypeFromHandle(initStore.Value, out type);
}
static void FindDynamicCallSitesInBlock(Block block, Dictionary<IField, CallSiteInfo> callsites, ILTransformContext context)
{
if (block.Instructions.Count < 2)
@@ -243,33 +260,35 @@ namespace ICSharpCode.Decompiler.IL.Transforms
);
case BinderMethodKind.InvokeConstructor:
var arguments = targetInvokeCall.Arguments.Skip(2).ToArray();
// Extract type information from targetInvokeCall:
// Must either be an inlined type or
// a reference to a variable that is initialized with a type.
if (!TransformExpressionTrees.MatchGetTypeFromHandle(arguments[0], out var type))
{
if (!(arguments[0].MatchLdLoc(out var temp) && temp.IsSingleDefinition && temp.StoreInstructions.FirstOrDefault() is StLoc initStore))
return null;
if (!TransformExpressionTrees.MatchGetTypeFromHandle(initStore.Value, out type))
return null;
}
deadArguments.AddRange(targetInvokeCall.Arguments.Take(2));
// ArgumentInfos[0] is always IsStaticType here: an object creation names the type it constructs.
if (!MatchTypeOf(arguments[0], out var type))
return null;
deadArguments.AddRange(targetInvokeCall.Arguments.Take(3));
return new DynamicInvokeConstructorInstruction(
binderFlags: callsite.Flags,
type: type ?? SpecialType.UnknownType,
context: callsite.Context,
argumentInfo: callsite.ArgumentInfos,
arguments: arguments
arguments: arguments.Skip(1).ToArray()
);
case BinderMethodKind.InvokeMember:
var memberArguments = targetInvokeCall.Arguments.Skip(2).ToArray();
IType staticTargetType = null;
if (callsite.ArgumentInfos[0].HasFlag(CSharpArgumentInfoFlags.IsStaticType)
&& MatchTypeOf(memberArguments[0], out staticTargetType))
{
deadArguments.Add(memberArguments[0]);
memberArguments = memberArguments.Skip(1).ToArray();
}
deadArguments.AddRange(targetInvokeCall.Arguments.Take(2));
return new DynamicInvokeMemberInstruction(
binderFlags: callsite.Flags,
name: callsite.MemberName,
typeArguments: callsite.TypeArguments,
context: callsite.Context,
staticTargetType: staticTargetType,
argumentInfo: callsite.ArgumentInfos,
arguments: targetInvokeCall.Arguments.Skip(2).ToArray()
arguments: memberArguments
);
case BinderMethodKind.IsEvent:
deadArguments.AddRange(targetInvokeCall.Arguments.Take(2));