Merge pull request #4072 from icsharpcode/fix/3704-static-type-target

Fix #3704: keep the static type of a dynamic call target
This commit is contained in:
Siegfried Pammer
2026-08-29 23:09:35 +02:00
committed by GitHub
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));