diff --git a/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs b/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs index faa5a7456..013fab62f 100644 --- a/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs +++ b/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs @@ -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 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 await RunForLibrary(cscOptions: cscOptions); } - async Task RunForLibrary([CallerMemberName] string testName = null, AssemblerOptions asmOptions = AssemblerOptions.None, CompilerOptions cscOptions = CompilerOptions.None, Action configureDecompiler = null) + async Task RunForLibrary([CallerMemberName] string testName = null, AssemblerOptions asmOptions = AssemblerOptions.None, CompilerOptions cscOptions = CompilerOptions.None, Action 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 configureDecompiler = null) + async Task Run([CallerMemberName] string testName = null, AssemblerOptions asmOptions = AssemblerOptions.None, CompilerOptions cscOptions = CompilerOptions.None, Action 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 } // 2. Decompile - var settings = Tester.GetSettings(cscOptions); + settings ??= Tester.GetSettings(cscOptions); configureDecompiler?.Invoke(settings); var decompiled = await Tester.DecompileCSharp(exeFile, settings).ConfigureAwait(false); diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/AnonymousTypeMethodGroups.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/AnonymousTypeMethodGroups.cs new file mode 100644 index 000000000..71e942def --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/AnonymousTypeMethodGroups.cs @@ -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 value) + { + return value; + } + } +} diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/DelegateCaching.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/DelegateCaching.cs new file mode 100644 index 000000000..83a9b9544 --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/DelegateCaching.cs @@ -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() + { + } + } +} diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/DelegateCachingWithExplicitConversions.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/DelegateCachingWithExplicitConversions.cs new file mode 100644 index 000000000..a7a9974b9 --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/DelegateCachingWithExplicitConversions.cs @@ -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() + { + } + } +} diff --git a/ICSharpCode.Decompiler/CSharp/CallBuilder.cs b/ICSharpCode.Decompiler/CSharp/CallBuilder.cs index 1836d8b7f..66c400201 100644 --- a/ICSharpCode.Decompiler/CSharp/CallBuilder.cs +++ b/ICSharpCode.Decompiler/CSharp/CallBuilder.cs @@ -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 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 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 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; } diff --git a/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs b/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs index 600ca0b84..a47236020 100644 --- a/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs +++ b/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs @@ -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); + } + + /// + /// Gets whether the C# compiler would cache a method-group conversion in the current context. + /// + 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); diff --git a/ICSharpCode.Decompiler/CSharp/TranslatedExpression.cs b/ICSharpCode.Decompiler/CSharp/TranslatedExpression.cs index d682da2eb..a22c8db80 100644 --- a/ICSharpCode.Decompiler/CSharp/TranslatedExpression.cs +++ b/ICSharpCode.Decompiler/CSharp/TranslatedExpression.cs @@ -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 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 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 /// would have the same semantics as the existing cast from 'inputType' to 'oldTargetType'. /// The existing cast is classified in 'conversion'. /// - 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. diff --git a/ICSharpCode.Decompiler/FlowAnalysis/DataFlowVisitor.cs b/ICSharpCode.Decompiler/FlowAnalysis/DataFlowVisitor.cs index 82d9560b1..a41ddf145 100644 --- a/ICSharpCode.Decompiler/FlowAnalysis/DataFlowVisitor.cs +++ b/ICSharpCode.Decompiler/FlowAnalysis/DataFlowVisitor.cs @@ -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); } diff --git a/ICSharpCode.Decompiler/IL/Instructions.cs b/ICSharpCode.Decompiler/IL/Instructions.cs index 02aa1bddb..429ade981 100644 --- a/ICSharpCode.Decompiler/IL/Instructions.cs +++ b/ICSharpCode.Decompiler/IL/Instructions.cs @@ -177,6 +177,8 @@ namespace ICSharpCode.Decompiler.IL UnboxAny, /// Creates an object instance and calls the constructor. NewObj, + /// Reuses a cached delegate, evaluating the argument only when the cache is empty. + CachedDelegate, /// Creates an array instance. NewArr, /// Returns the default value for a type. @@ -4580,6 +4582,103 @@ namespace ICSharpCode.Decompiler.IL } } namespace ICSharpCode.Decompiler.IL +{ + /// Reuses a cached delegate, evaluating the argument only when the cache is empty. + 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(ILVisitor visitor) + { + return visitor.VisitCachedDelegate(this); + } + public override T AcceptVisitor(ILVisitor 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 { /// Creates an array instance. public sealed partial class NewArr : ILInstruction @@ -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 { 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 { 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 "unbox", "unbox.any", "newobj", + "cached.delegate", "newarr", "default.value", "throw", @@ -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; diff --git a/ICSharpCode.Decompiler/IL/Instructions.tt b/ICSharpCode.Decompiler/IL/Instructions.tt index db6041f6c..16beb976b 100644 --- a/ICSharpCode.Decompiler/IL/Instructions.tt +++ b/ICSharpCode.Decompiler/IL/Instructions.tt @@ -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), diff --git a/ICSharpCode.Decompiler/IL/Transforms/CachedDelegateInitialization.cs b/ICSharpCode.Decompiler/IL/Transforms/CachedDelegateInitialization.cs index e67f31123..68581b738 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/CachedDelegateInitialization.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/CachedDelegateInitialization.cs @@ -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); + } + /// /// if (comp(ldsfld CachedAnonMethodDelegate == ldnull)) { /// stsfld CachedAnonMethodDelegate(DelegateConstruction) /// } /// ... one usage of CachedAnonMethodDelegate ... /// => - /// ... one usage of DelegateConstruction ... + /// ... one usage of cached.delegate(DelegateConstruction) ... /// bool CachedDelegateInitializationWithField(IfInstruction inst) { @@ -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 /// stloc v(DelegateConstruction) /// } /// => - /// stloc v(DelegateConstruction) + /// stloc v(cached.delegate(DelegateConstruction)) /// bool CachedDelegateInitializationWithLocal(IfInstruction inst) { @@ -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; } - /// - /// stloc s(ldobj(ldsflda(CachedAnonMethodDelegate)) - /// if (comp(ldloc s == null)) { - /// stloc s(stobj(ldsflda(CachedAnonMethodDelegate), DelegateConstruction)) - /// } - /// => - /// stloc s(DelegateConstruction) - /// - 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; - } - /// /// 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. /// bool CachedDelegateInitializationRoslynWithLocal(IfInstruction inst) { @@ -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 /// stloc s(ldobj System.Action(ldsflda $I4-1)) /// } /// => - /// stloc s(DelegateConstruction) + /// stloc s(cached.delegate(DelegateConstruction)) /// bool CachedDelegateInitializationVB(IfInstruction inst) { @@ -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 /// } /// leave IL_0005 (stsfld CachedAnonMethodDelegate(DelegateConstruction)) /// => - /// leave IL_0005 (DelegateConstruction) + /// leave IL_0005 (cached.delegate(DelegateConstruction)) /// bool CachedDelegateInitializationVBWithReturn(IfInstruction inst) { @@ -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 /// stloc s(stobj delegateType(ldflda CachedAnonMethodDelegate(ldloc closure), DelegateConstruction)) /// } /// => - /// stloc s(DelegateConstruction) + /// stloc s(cached.delegate(DelegateConstruction)) /// bool CachedDelegateInitializationVBWithClosure(IfInstruction inst) { @@ -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;