Browse Source

Merge 701df21df6 into 052aec9b89

pull/4126/merge
marcus m 21 hours ago committed by GitHub
parent
commit
64cae5b910
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 29
      ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs
  2. 48
      ICSharpCode.Decompiler.Tests/TestCases/Pretty/AnonymousTypeMethodGroups.cs
  3. 52
      ICSharpCode.Decompiler.Tests/TestCases/Pretty/DelegateCaching.cs
  4. 39
      ICSharpCode.Decompiler.Tests/TestCases/Pretty/DelegateCachingWithExplicitConversions.cs
  5. 45
      ICSharpCode.Decompiler/CSharp/CallBuilder.cs
  6. 31
      ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs
  7. 17
      ICSharpCode.Decompiler/CSharp/TranslatedExpression.cs
  8. 17
      ICSharpCode.Decompiler/FlowAnalysis/DataFlowVisitor.cs
  9. 123
      ICSharpCode.Decompiler/IL/Instructions.cs
  10. 3
      ICSharpCode.Decompiler/IL/Instructions.tt
  11. 131
      ICSharpCode.Decompiler/IL/Transforms/CachedDelegateInitialization.cs

29
ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs

@ -332,12 +332,31 @@ namespace ICSharpCode.Decompiler.Tests @@ -332,12 +332,31 @@ namespace ICSharpCode.Decompiler.Tests
});
}
[Test]
public async Task DelegateCaching([ValueSource(nameof(roslyn4OrNewerOptions))] CompilerOptions cscOptions)
{
await RunForLibrary(cscOptions: cscOptions);
}
[Test]
public async Task DelegateCachingWithExplicitConversions([ValueSource(nameof(roslyn4OrNewerOptions))] CompilerOptions cscOptions)
{
await RunForLibrary(cscOptions: cscOptions, configureDecompiler: settings => settings.UseImplicitMethodGroupConversion = false);
}
[Test]
public async Task AnonymousTypes([ValueSource(nameof(defaultOptionsWithMcs))] CompilerOptions cscOptions)
{
await RunForLibrary(cscOptions: cscOptions);
}
[Test]
public async Task AnonymousTypeMethodGroups([ValueSource(nameof(roslyn3OrNewerOptions))] CompilerOptions cscOptions)
{
// Older compilers emit uncached method groups even when decompiling to a language version that supports caching.
await RunForLibrary(cscOptions: cscOptions, settings: new DecompilerSettings { FileScopedNamespaces = false });
}
[Test]
public async Task StringConcatenation([ValueSource(nameof(roslyn3OrNewerOptions))] CompilerOptions cscOptions)
{
@ -673,6 +692,8 @@ namespace ICSharpCode.Decompiler.Tests @@ -673,6 +692,8 @@ namespace ICSharpCode.Decompiler.Tests
public async Task FunctionPointers([ValueSource(nameof(roslyn3OrNewerOptions))] CompilerOptions cscOptions)
{
await RunForLibrary(cscOptions: cscOptions);
// Disabling implicit delegate conversions must not introduce function-pointer casts.
await RunForLibrary(cscOptions: cscOptions, configureDecompiler: settings => settings.UseImplicitMethodGroupConversion = false);
}
[Test]
@ -1064,12 +1085,12 @@ namespace ICSharpCode.Decompiler.Tests @@ -1064,12 +1085,12 @@ namespace ICSharpCode.Decompiler.Tests
await RunForLibrary(cscOptions: cscOptions);
}
async Task RunForLibrary([CallerMemberName] string testName = null, AssemblerOptions asmOptions = AssemblerOptions.None, CompilerOptions cscOptions = CompilerOptions.None, Action<DecompilerSettings> configureDecompiler = null)
async Task RunForLibrary([CallerMemberName] string testName = null, AssemblerOptions asmOptions = AssemblerOptions.None, CompilerOptions cscOptions = CompilerOptions.None, Action<DecompilerSettings> configureDecompiler = null, DecompilerSettings settings = null)
{
await Run(testName, asmOptions | AssemblerOptions.Library, cscOptions | CompilerOptions.Library, configureDecompiler);
await Run(testName, asmOptions | AssemblerOptions.Library, cscOptions | CompilerOptions.Library, configureDecompiler, settings);
}
async Task Run([CallerMemberName] string testName = null, AssemblerOptions asmOptions = AssemblerOptions.None, CompilerOptions cscOptions = CompilerOptions.None, Action<DecompilerSettings> configureDecompiler = null)
async Task Run([CallerMemberName] string testName = null, AssemblerOptions asmOptions = AssemblerOptions.None, CompilerOptions cscOptions = CompilerOptions.None, Action<DecompilerSettings> configureDecompiler = null, DecompilerSettings settings = null)
{
var csFile = Path.Combine(TestCasePath, testName + ".cs");
var exeFile = TestsAssemblyOutput.GetFilePath(TestCasePath, testName, Tester.GetSuffix(cscOptions) + ".exe");
@ -1091,7 +1112,7 @@ namespace ICSharpCode.Decompiler.Tests @@ -1091,7 +1112,7 @@ namespace ICSharpCode.Decompiler.Tests
}
// 2. Decompile
var settings = Tester.GetSettings(cscOptions);
settings ??= Tester.GetSettings(cscOptions);
configureDecompiler?.Invoke(settings);
var decompiled = await Tester.DecompileCSharp(exeFile, settings).ConfigureAwait(false);

48
ICSharpCode.Decompiler.Tests/TestCases/Pretty/AnonymousTypeMethodGroups.cs

@ -0,0 +1,48 @@ @@ -0,0 +1,48 @@
// Copyright (c) 2026 marcusmalloc
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System.Linq;
namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
{
internal static class AnonymousTypeMethodGroups
{
public static object AnonymousTypeArgument()
{
return new[] {
new {
X = 1
}
}.Select(Identity).Single();
}
public static object AnonymousArrayTypeArgument()
{
return new[] { new[] {
new {
X = 1
}
} }.Select(Identity).Single();
}
private static T Identity<T>(T value)
{
return value;
}
}
}

52
ICSharpCode.Decompiler.Tests/TestCases/Pretty/DelegateCaching.cs

@ -0,0 +1,52 @@ @@ -0,0 +1,52 @@
// Copyright (c) 2026 marcusmalloc
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
{
internal static class DelegateCaching
{
public delegate void CustomDelegate();
// Issue #3921: explicit construction must preserve a fresh delegate on each call.
public static Action FreshDelegate()
{
return new Action(M);
}
public static Action CachedDelegate()
{
return M;
}
public static object FreshDelegateAsObject()
{
return new CustomDelegate(M);
}
public static object CachedDelegateAsObject()
{
return (CustomDelegate)M;
}
private static void M()
{
}
}
}

39
ICSharpCode.Decompiler.Tests/TestCases/Pretty/DelegateCachingWithExplicitConversions.cs

@ -0,0 +1,39 @@ @@ -0,0 +1,39 @@
// Copyright (c) 2026 marcusmalloc
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
{
internal static class DelegateCachingWithExplicitConversions
{
public static Action FreshDelegate()
{
return new Action(M);
}
public static Action CachedDelegate()
{
return (Action)M;
}
private static void M()
{
}
}
}

45
ICSharpCode.Decompiler/CSharp/CallBuilder.cs

@ -2058,7 +2058,12 @@ namespace ICSharpCode.Decompiler.CSharp @@ -2058,7 +2058,12 @@ namespace ICSharpCode.Decompiler.CSharp
}
}
private bool CanUseDelegateConstruction(IMethod targetMethod, ILInstruction thisArg, IMethod invokeMethod)
static bool IsBoundExtensionMethod(IMethod method, IMethod? invokeMethod)
{
return method.IsExtensionMethod && method.Parameters.Count - 1 == invokeMethod?.Parameters.Count;
}
private static bool CanUseDelegateConstruction(IMethod targetMethod, ILInstruction thisArg, IMethod? invokeMethod)
{
// Accessors cannot be directly referenced as method group in C#
// see https://github.com/icsharpcode/ILSpy/issues/1741#issuecomment-540179101
@ -2066,36 +2071,17 @@ namespace ICSharpCode.Decompiler.CSharp @@ -2066,36 +2071,17 @@ namespace ICSharpCode.Decompiler.CSharp
return false;
if (targetMethod.IsStatic)
{
// If the invoke method is known, we can compare the parameter counts to figure out whether the
// delegate is static or binds the first argument
if (invokeMethod != null)
{
if (invokeMethod.Parameters.Count == targetMethod.Parameters.Count)
{
return thisArg.MatchLdNull();
}
else if (targetMethod.IsExtensionMethod && invokeMethod.Parameters.Count == targetMethod.Parameters.Count - 1)
{
return true;
}
else
{
return false;
}
}
else
if (invokeMethod == null)
{
// delegate type unknown:
// Delegate type unknown.
return thisArg.MatchLdNull() || targetMethod.IsExtensionMethod;
}
// An unbound static delegate supplies every method parameter through Invoke.
if (invokeMethod.Parameters.Count == targetMethod.Parameters.Count)
return thisArg.MatchLdNull();
return IsBoundExtensionMethod(targetMethod, invokeMethod);
}
else
{
// targetMethod is instance method
if (invokeMethod != null && invokeMethod.Parameters.Count != targetMethod.Parameters.Count)
return false;
return true;
}
return invokeMethod == null || invokeMethod.Parameters.Count == targetMethod.Parameters.Count;
}
internal TranslatedExpression Build(LdVirtDelegate inst)
@ -2145,7 +2131,7 @@ namespace ICSharpCode.Decompiler.CSharp @@ -2145,7 +2131,7 @@ namespace ICSharpCode.Decompiler.CSharp
Debug.Assert(localFunction != null);
return (default, addTypeArguments: true, localFunction.Name!, ToMethodGroup(method, localFunction));
}
if (method.IsExtensionMethod && method.Parameters.Count - 1 == invokeMethod?.Parameters.Count)
if (IsBoundExtensionMethod(method, invokeMethod))
{
IType targetType = method.Parameters[0].Type;
if (targetType.Kind == TypeKind.ByReference && thisArg is Box thisArgBox)
@ -2263,13 +2249,14 @@ namespace ICSharpCode.Decompiler.CSharp @@ -2263,13 +2249,14 @@ namespace ICSharpCode.Decompiler.CSharp
TranslatedExpression HandleDelegateConstruction(IType delegateType, IMethod method, ExpectedTargetDetails expectedTargetDetails, ILInstruction thisArg, ILInstruction inst)
{
var invokeMethod = delegateType.GetDelegateInvokeMethod();
bool capturesFirstArgument = !method.IsStatic || IsBoundExtensionMethod(method, invokeMethod);
var targetExpression = BuildDelegateReference(method, invokeMethod, expectedTargetDetails, thisArg);
var oce = new ObjectCreateExpression(expressionBuilder.ConvertType(delegateType), targetExpression)
.WithILInstruction(inst)
.WithRR(new ConversionResolveResult(
delegateType,
targetExpression.ResolveResult,
Conversion.MethodGroupConversion(method, expectedTargetDetails.CallOpCode == OpCode.CallVirt, false)));
Conversion.MethodGroupConversion(method, expectedTargetDetails.CallOpCode == OpCode.CallVirt, capturesFirstArgument)));
return oce;
}

31
ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs

@ -559,6 +559,37 @@ namespace ICSharpCode.Decompiler.CSharp @@ -559,6 +559,37 @@ namespace ICSharpCode.Decompiler.CSharp
return new CallBuilder(this, typeSystem, settings).Build(inst, context.TypeHint);
}
protected internal override TranslatedExpression VisitCachedDelegate(CachedDelegate inst, TranslationContext context)
{
var expression = Translate(inst.Argument, context.TypeHint);
if (expression.Expression is ObjectCreateExpression objectCreation && objectCreation.Arguments.Count == 1
&& expression.ResolveResult is ConversionResolveResult { Conversion.IsMethodGroupConversion: true })
{
// A method-group conversion allows caching; explicit construction would allocate.
// ConvertTo can remove the cast when the context supplies the delegate type.
var cast = new CastExpression(objectCreation.Type.Detach(), objectCreation.Arguments.Single().Detach())
.CopyAnnotationsFrom(objectCreation);
return new TranslatedExpression(cast, expression.ResolveResult).WithILInstruction(inst);
}
return expression.WithILInstruction(inst);
}
/// <summary>
/// Gets whether the C# compiler would cache a method-group conversion in the current context.
/// </summary>
internal bool MethodGroupConversionWouldBeCached(Conversion conversion)
{
if (settings.GetMinimumRequiredVersion() < LanguageVersion.CSharp11_0
|| currentFunction.Kind == ILFunctionKind.ExpressionTree
|| decompilationContext.CurrentMember is { SymbolKind: SymbolKind.Constructor, IsStatic: true })
{
return false;
}
// Local-function symbols report IsStatic even when the C# declaration cannot be static.
return !conversion.DelegateCapturesFirstArgument
&& conversion.Method is not LocalFunctionMethod { IsStaticLocalFunction: false };
}
protected internal override TranslatedExpression VisitLdVirtDelegate(LdVirtDelegate inst, TranslationContext context)
{
return new CallBuilder(this, typeSystem, settings).Build(inst);

17
ICSharpCode.Decompiler/CSharp/TranslatedExpression.cs

@ -254,7 +254,7 @@ namespace ICSharpCode.Decompiler.CSharp @@ -254,7 +254,7 @@ namespace ICSharpCode.Decompiler.CSharp
case ConversionResolveResult conversion:
{
if (Expression is CastExpression cast && CastCanBeMadeImplicit(
Resolver.CSharpConversions.Get(expressionBuilder.compilation),
expressionBuilder,
conversion.Conversion,
conversion.Input.Type,
type, targetType
@ -270,6 +270,13 @@ namespace ICSharpCode.Decompiler.CSharp @@ -270,6 +270,13 @@ namespace ICSharpCode.Decompiler.CSharp
else if (Expression is ObjectCreateExpression oce && conversion.Conversion.IsMethodGroupConversion
&& oce.Arguments.Count == 1 && expressionBuilder.settings.UseImplicitMethodGroupConversion)
{
// Preserve explicit construction if a method-group conversion would introduce caching.
// Delegate types containing anonymous types must be inferred instead.
if (expressionBuilder.MethodGroupConversionWouldBeCached(conversion.Conversion)
&& (!expressionBuilder.settings.AnonymousTypes || !type.ContainsAnonymousType()))
{
return this;
}
return this.UnwrapChild(oce.Arguments.Single());
}
break;
@ -349,7 +356,7 @@ namespace ICSharpCode.Decompiler.CSharp @@ -349,7 +356,7 @@ namespace ICSharpCode.Decompiler.CSharp
var conversions = Resolver.CSharpConversions.Get(compilation);
if (ResolveResult is ConversionResolveResult conv && Expression is CastExpression cast2
&& !conv.Conversion.IsUserDefined
&& CastCanBeMadeImplicit(conversions, conv.Conversion, conv.Input.Type, type, targetType))
&& CastCanBeMadeImplicit(expressionBuilder, conv.Conversion, conv.Input.Type, type, targetType))
{
var unwrapped = Unwrapped(this.UnwrapChild(cast2.Expression));
if (allowImplicitConversion)
@ -679,8 +686,12 @@ namespace ICSharpCode.Decompiler.CSharp @@ -679,8 +686,12 @@ namespace ICSharpCode.Decompiler.CSharp
/// would have the same semantics as the existing cast from 'inputType' to 'oldTargetType'.
/// The existing cast is classified in 'conversion'.
/// </summary>
bool CastCanBeMadeImplicit(Resolver.CSharpConversions conversions, Conversion conversion, IType inputType, IType oldTargetType, IType newTargetType)
bool CastCanBeMadeImplicit(ExpressionBuilder expressionBuilder, Conversion conversion, IType inputType, IType oldTargetType, IType newTargetType)
{
if (conversion.IsMethodGroupConversion && oldTargetType.Kind == TypeKind.Delegate
&& !expressionBuilder.settings.UseImplicitMethodGroupConversion)
return false;
var conversions = Resolver.CSharpConversions.Get(expressionBuilder.compilation);
if (!conversion.IsImplicit)
{
// If the cast was required for the old conversion, avoid making it implicit.

17
ICSharpCode.Decompiler/FlowAnalysis/DataFlowVisitor.cs

@ -751,25 +751,30 @@ namespace ICSharpCode.Decompiler.FlowAnalysis @@ -751,25 +751,30 @@ namespace ICSharpCode.Decompiler.FlowAnalysis
protected internal override void VisitNullCoalescingInstruction(NullCoalescingInstruction inst)
{
HandleBinaryWithOptionalEvaluation(inst, inst.ValueInst, inst.FallbackInst);
HandleOptionalEvaluation(inst, inst.FallbackInst, precedingArgument: inst.ValueInst);
}
protected internal override void VisitCachedDelegate(CachedDelegate inst)
{
HandleOptionalEvaluation(inst, inst.Argument);
}
protected internal override void VisitDynamicLogicOperatorInstruction(DynamicLogicOperatorInstruction inst)
{
HandleBinaryWithOptionalEvaluation(inst, inst.Left, inst.Right);
HandleOptionalEvaluation(inst, inst.Right, precedingArgument: inst.Left);
}
protected internal override void VisitUserDefinedLogicOperator(UserDefinedLogicOperator inst)
{
HandleBinaryWithOptionalEvaluation(inst, inst.Left, inst.Right);
HandleOptionalEvaluation(inst, inst.Right, precedingArgument: inst.Left);
}
void HandleBinaryWithOptionalEvaluation(ILInstruction parent, ILInstruction left, ILInstruction right)
void HandleOptionalEvaluation(ILInstruction parent, ILInstruction optionalArgument, ILInstruction precedingArgument = null)
{
DebugStartPoint(parent);
left.AcceptVisitor(this);
precedingArgument?.AcceptVisitor(this);
State branchState = state.Clone();
right.AcceptVisitor(this);
optionalArgument.AcceptVisitor(this);
state.JoinWith(branchState);
DebugEndPoint(parent);
}

123
ICSharpCode.Decompiler/IL/Instructions.cs

@ -177,6 +177,8 @@ namespace ICSharpCode.Decompiler.IL @@ -177,6 +177,8 @@ namespace ICSharpCode.Decompiler.IL
UnboxAny,
/// <summary>Creates an object instance and calls the constructor.</summary>
NewObj,
/// <summary>Reuses a cached delegate, evaluating the argument only when the cache is empty.</summary>
CachedDelegate,
/// <summary>Creates an array instance.</summary>
NewArr,
/// <summary>Returns the default value for a type.</summary>
@ -4580,6 +4582,103 @@ namespace ICSharpCode.Decompiler.IL @@ -4580,6 +4582,103 @@ namespace ICSharpCode.Decompiler.IL
}
}
namespace ICSharpCode.Decompiler.IL
{
/// <summary>Reuses a cached delegate, evaluating the argument only when the cache is empty.</summary>
public sealed partial class CachedDelegate : ILInstruction
{
public CachedDelegate(ILInstruction argument) : base(OpCode.CachedDelegate)
{
this.Argument = argument;
}
public static readonly SlotInfo ArgumentSlot = new SlotInfo("Argument");
ILInstruction argument = null!;
public ILInstruction Argument {
get { return this.argument; }
set {
ValidateChild(value);
SetChildInstruction(ref this.argument, value, 0);
}
}
protected sealed override int GetChildCount()
{
return 1;
}
protected sealed override ILInstruction GetChild(int index)
{
switch (index)
{
case 0:
return this.argument;
default:
throw new IndexOutOfRangeException();
}
}
protected sealed override void SetChild(int index, ILInstruction value)
{
switch (index)
{
case 0:
this.Argument = value;
break;
default:
throw new IndexOutOfRangeException();
}
}
protected sealed override SlotInfo GetChildSlot(int index)
{
switch (index)
{
case 0:
return ArgumentSlot;
default:
throw new IndexOutOfRangeException();
}
}
public sealed override ILInstruction Clone()
{
var clone = (CachedDelegate)ShallowClone();
clone.Argument = this.argument.Clone();
return clone;
}
public override StackType ResultType => Argument.ResultType;
public override IType InferType(ICompilation compilation) => Argument.InferType(compilation);
protected override InstructionFlags ComputeFlags()
{
return argument.Flags | InstructionFlags.ControlFlow;
}
public override InstructionFlags DirectFlags {
get {
return InstructionFlags.ControlFlow;
}
}
protected override void WriteToCore(ITextOutput output, ILAstWritingOptions options)
{
WriteILRange(output, options);
output.Write(OpCode);
output.Write('(');
this.argument.WriteTo(output, options);
output.Write(')');
}
public override void AcceptVisitor(ILVisitor visitor)
{
visitor.VisitCachedDelegate(this);
}
public override T AcceptVisitor<T>(ILVisitor<T> visitor)
{
return visitor.VisitCachedDelegate(this);
}
public override T AcceptVisitor<C, T>(ILVisitor<C, T> visitor, C context)
{
return visitor.VisitCachedDelegate(this, context);
}
protected internal override bool PerformMatch(ILInstruction? other, ref Patterns.Match match)
{
var o = other as CachedDelegate;
return o != null && this.argument.PerformMatch(o.argument, ref match);
}
}
}
namespace ICSharpCode.Decompiler.IL
{
/// <summary>Creates an array instance.</summary>
public sealed partial class NewArr : ILInstruction
@ -7412,6 +7511,10 @@ namespace ICSharpCode.Decompiler.IL @@ -7412,6 +7511,10 @@ namespace ICSharpCode.Decompiler.IL
{
Default(inst);
}
protected internal virtual void VisitCachedDelegate(CachedDelegate inst)
{
Default(inst);
}
protected internal virtual void VisitNewArr(NewArr inst)
{
Default(inst);
@ -7822,6 +7925,10 @@ namespace ICSharpCode.Decompiler.IL @@ -7822,6 +7925,10 @@ namespace ICSharpCode.Decompiler.IL
{
return Default(inst);
}
protected internal virtual T VisitCachedDelegate(CachedDelegate inst)
{
return Default(inst);
}
protected internal virtual T VisitNewArr(NewArr inst)
{
return Default(inst);
@ -8232,6 +8339,10 @@ namespace ICSharpCode.Decompiler.IL @@ -8232,6 +8339,10 @@ namespace ICSharpCode.Decompiler.IL
{
return Default(inst, context);
}
protected internal virtual T VisitCachedDelegate(CachedDelegate inst, C context)
{
return Default(inst, context);
}
protected internal virtual T VisitNewArr(NewArr inst, C context)
{
return Default(inst, context);
@ -8433,6 +8544,7 @@ namespace ICSharpCode.Decompiler.IL @@ -8433,6 +8544,7 @@ namespace ICSharpCode.Decompiler.IL
"unbox",
"unbox.any",
"newobj",
"cached.delegate",
"newarr",
"default.value",
"throw",
@ -9003,6 +9115,17 @@ namespace ICSharpCode.Decompiler.IL @@ -9003,6 +9115,17 @@ namespace ICSharpCode.Decompiler.IL
type = default(IType);
return false;
}
public bool MatchCachedDelegate([NotNullWhen(true)] out ILInstruction? argument)
{
var inst = this as CachedDelegate;
if (inst != null)
{
argument = inst.Argument;
return true;
}
argument = default(ILInstruction);
return false;
}
public bool MatchNewArr([NotNullWhen(true)] out IType? type)
{
var inst = this as NewArr;

3
ICSharpCode.Decompiler/IL/Instructions.tt

@ -287,6 +287,9 @@ @@ -287,6 +287,9 @@
Unary, HasTypeOperand, MemoryAccess, MayThrow, ResultType("this.type")),
new OpCode("newobj", "Creates an object instance and calls the constructor.",
CustomClassName("NewObj"), Call, ResultType("Method.DeclaringType")),
new OpCode("cached.delegate", "Reuses a cached delegate, evaluating the argument only when the cache is empty.",
CustomChildren(new [] { new ChildInfo("argument") }), ControlFlow,
ResultType("Argument.InferType(compilation)", "Argument.ResultType")),
new OpCode("newarr", "Creates an array instance.",
CustomClassName("NewArr"), HasTypeOperand,
CustomChildren(new [] { new ArgumentInfo("indices") { IsCollection = true } }, true),

131
ICSharpCode.Decompiler/IL/Transforms/CachedDelegateInitialization.cs

@ -33,53 +33,47 @@ namespace ICSharpCode.Decompiler.IL.Transforms @@ -33,53 +33,47 @@ namespace ICSharpCode.Decompiler.IL.Transforms
return;
for (int i = context.IndexOfFirstAlreadyTransformedInstruction - 1; i >= 0; i--)
{
if (block.Instructions[i] is IfInstruction inst)
if (block.Instructions[i] is IfInstruction inst && TryTransform(block, i, inst))
{
if (CachedDelegateInitializationWithField(inst))
{
block.Instructions.RemoveAt(i);
context.IndexOfFirstAlreadyTransformedInstruction = block.Instructions.Count;
continue;
}
if (CachedDelegateInitializationWithLocal(inst))
{
ILInlining.InlineOneIfPossible(block, i, InliningOptions.Aggressive, context);
context.IndexOfFirstAlreadyTransformedInstruction = block.Instructions.Count;
continue;
}
if (CachedDelegateInitializationRoslynInStaticWithLocal(inst) || CachedDelegateInitializationRoslynWithLocal(inst))
{
block.Instructions.RemoveAt(i);
context.IndexOfFirstAlreadyTransformedInstruction = block.Instructions.Count;
continue;
}
if (CachedDelegateInitializationVB(inst))
{
context.IndexOfFirstAlreadyTransformedInstruction = block.Instructions.Count;
continue;
}
if (CachedDelegateInitializationVBWithReturn(inst))
{
block.Instructions.RemoveAt(i);
context.IndexOfFirstAlreadyTransformedInstruction = block.Instructions.Count;
continue;
}
if (CachedDelegateInitializationVBWithClosure(inst))
{
context.IndexOfFirstAlreadyTransformedInstruction = block.Instructions.Count;
continue;
}
context.IndexOfFirstAlreadyTransformedInstruction = block.Instructions.Count;
}
}
}
bool TryTransform(Block block, int i, IfInstruction inst)
{
if (CachedDelegateInitializationWithField(inst))
{
block.Instructions.RemoveAt(i);
return true;
}
if (CachedDelegateInitializationWithLocal(inst))
{
ILInlining.InlineOneIfPossible(block, i, InliningOptions.Aggressive, context);
return true;
}
if (CachedDelegateInitializationRoslynWithLocal(inst))
{
block.Instructions.RemoveAt(i);
return true;
}
if (CachedDelegateInitializationVB(inst))
return true;
if (CachedDelegateInitializationVBWithReturn(inst))
{
block.Instructions.RemoveAt(i);
return true;
}
return CachedDelegateInitializationVBWithClosure(inst);
}
/// <summary>
/// if (comp(ldsfld CachedAnonMethodDelegate == ldnull)) {
/// stsfld CachedAnonMethodDelegate(DelegateConstruction)
/// }
/// ... one usage of CachedAnonMethodDelegate ...
/// =>
/// ... one usage of DelegateConstruction ...
/// ... one usage of cached.delegate(DelegateConstruction) ...
/// </summary>
bool CachedDelegateInitializationWithField(IfInstruction inst)
{
@ -101,7 +95,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms @@ -101,7 +95,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms
if (usages.Length != 1)
return false;
context.Step("CachedDelegateInitializationWithField", inst);
usages[0].ReplaceWith(value);
usages[0].ReplaceWith(new CachedDelegate(value));
context.EndStep(value);
return true;
}
@ -111,7 +105,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms @@ -111,7 +105,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms
/// stloc v(DelegateConstruction)
/// }
/// =>
/// stloc v(DelegateConstruction)
/// stloc v(cached.delegate(DelegateConstruction))
/// </summary>
bool CachedDelegateInitializationWithLocal(IfInstruction inst)
{
@ -141,50 +135,20 @@ namespace ICSharpCode.Decompiler.IL.Transforms @@ -141,50 +135,20 @@ namespace ICSharpCode.Decompiler.IL.Transforms
return false;
context.Step("CachedDelegateInitializationWithLocal", inst);
((Block)otherStore.Parent).Instructions.Remove(otherStore);
((StLoc)storeInst).Value = new CachedDelegate(value);
inst.ReplaceWith(storeInst);
context.EndStep(storeInst);
return true;
}
/// <summary>
/// stloc s(ldobj(ldsflda(CachedAnonMethodDelegate))
/// if (comp(ldloc s == null)) {
/// stloc s(stobj(ldsflda(CachedAnonMethodDelegate), DelegateConstruction))
/// }
/// =>
/// stloc s(DelegateConstruction)
/// </summary>
bool CachedDelegateInitializationRoslynInStaticWithLocal(IfInstruction inst)
{
Block trueInst = inst.TrueInst as Block;
if (trueInst == null || (trueInst.Instructions.Count != 1) || !inst.FalseInst.MatchNop())
return false;
if (!inst.Condition.MatchCompEquals(out ILInstruction left, out ILInstruction right) || !left.MatchLdLoc(out ILVariable s) || !right.MatchLdNull())
return false;
var storeInst = trueInst.Instructions.Last() as StLoc;
var storeBeforeIf = inst.Parent.Children.ElementAtOrDefault(inst.ChildIndex - 1) as StLoc;
if (storeBeforeIf == null || storeInst == null || storeBeforeIf.Variable != s || storeInst.Variable != s)
return false;
if (!(storeInst.Value is StObj stobj) || !(storeBeforeIf.Value is LdObj ldobj))
return false;
if (!(stobj.Value is NewObj))
return false;
if (!stobj.Target.MatchLdsFlda(out var field1) || !ldobj.Target.MatchLdsFlda(out var field2) || !field1.Equals(field2))
return false;
if (!DelegateConstruction.MatchDelegateConstruction((NewObj)stobj.Value, out _, out _, out _, true))
return false;
context.Step("CachedDelegateInitializationRoslynInStaticWithLocal", inst);
storeBeforeIf.Value = stobj.Value;
return true;
}
/// <summary>
/// stloc s(ldobj(ldflda(CachedAnonMethodDelegate))
/// if (comp(ldloc s == null)) {
/// stloc s(stobj(ldflda(CachedAnonMethodDelegate), DelegateConstruction))
/// }
/// =>
/// stloc s(DelegateConstruction)
/// stloc s(cached.delegate(DelegateConstruction))
/// The same pattern applies to static cache fields accessed with ldsflda.
/// </summary>
bool CachedDelegateInitializationRoslynWithLocal(IfInstruction inst)
{
@ -199,14 +163,19 @@ namespace ICSharpCode.Decompiler.IL.Transforms @@ -199,14 +163,19 @@ namespace ICSharpCode.Decompiler.IL.Transforms
return false;
if (!(storeInst.Value is StObj stobj) || !(storeBeforeIf.Value is LdObj ldobj))
return false;
if (!(stobj.Value is NewObj))
if (stobj.Value is not NewObj delegateConstruction)
return false;
if (!stobj.Target.MatchLdFlda(out var _, out var field1) || !ldobj.Target.MatchLdFlda(out var __, out var field2) || !field1.Equals(field2))
bool sameField = (stobj.Target, ldobj.Target) switch {
(LdsFlda first, LdsFlda second) => first.Field.Equals(second.Field),
(LdFlda first, LdFlda second) => first.Field.Equals(second.Field),
_ => false
};
if (!sameField)
return false;
if (!DelegateConstruction.MatchDelegateConstruction((NewObj)stobj.Value, out _, out _, out _, true))
if (!DelegateConstruction.MatchDelegateConstruction(delegateConstruction, out _, out _, out _, true))
return false;
context.Step("CachedDelegateInitializationRoslynWithLocal", inst);
storeBeforeIf.Value = stobj.Value;
storeBeforeIf.Value = new CachedDelegate(delegateConstruction);
return true;
}
@ -217,7 +186,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms @@ -217,7 +186,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms
/// stloc s(ldobj System.Action(ldsflda $I4-1))
/// }
/// =>
/// stloc s(DelegateConstruction)
/// stloc s(cached.delegate(DelegateConstruction))
/// </summary>
bool CachedDelegateInitializationVB(IfInstruction inst)
{
@ -249,7 +218,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms @@ -249,7 +218,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms
if (!DelegateConstruction.MatchDelegateConstruction(delegateConstruction, out _, out _, out _, true))
return false;
context.Step("CachedDelegateInitializationVB", inst);
var stloc = new StLoc(s, delegateConstruction);
var stloc = new StLoc(s, new CachedDelegate(delegateConstruction));
inst.ReplaceWith(stloc);
context.EndStep(stloc);
return true;
@ -261,7 +230,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms @@ -261,7 +230,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms
/// }
/// leave IL_0005 (stsfld CachedAnonMethodDelegate(DelegateConstruction))
/// =>
/// leave IL_0005 (DelegateConstruction)
/// leave IL_0005 (cached.delegate(DelegateConstruction))
/// </summary>
bool CachedDelegateInitializationVBWithReturn(IfInstruction inst)
{
@ -279,7 +248,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms @@ -279,7 +248,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms
if (!DelegateConstruction.MatchDelegateConstruction(delegateConstruction, out _, out _, out _, true))
return false;
context.Step("CachedDelegateInitializationVBWithReturn", inst);
leaveAfterIf.Value = delegateConstruction;
leaveAfterIf.Value = new CachedDelegate(delegateConstruction);
return true;
}
@ -290,7 +259,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms @@ -290,7 +259,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms
/// stloc s(stobj delegateType(ldflda CachedAnonMethodDelegate(ldloc closure), DelegateConstruction))
/// }
/// =>
/// stloc s(DelegateConstruction)
/// stloc s(cached.delegate(DelegateConstruction))
/// </summary>
bool CachedDelegateInitializationVBWithClosure(IfInstruction inst)
{
@ -322,7 +291,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms @@ -322,7 +291,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms
if (!DelegateConstruction.MatchDelegateConstruction(delegateConstruction, out _, out _, out _, true))
return false;
context.Step("CachedDelegateInitializationVBWithClosure", inst);
var stloc = new StLoc(s, delegateConstruction);
var stloc = new StLoc(s, new CachedDelegate(delegateConstruction));
inst.ReplaceWith(stloc);
context.EndStep(stloc);
return true;

Loading…
Cancel
Save