mirror of
https://github.com/icsharpcode/ILSpy.git
synced 2026-10-12 02:15:58 +00:00
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:
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user