diff --git a/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs b/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs index 6ddbc2f5f..013fab62f 100644 --- a/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs +++ b/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs @@ -338,12 +338,25 @@ namespace ICSharpCode.Decompiler.Tests 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) { @@ -679,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] @@ -1070,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"); @@ -1097,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 index 8484a4930..83a9b9544 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/DelegateCaching.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/DelegateCaching.cs @@ -22,6 +22,8 @@ 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() { @@ -33,6 +35,16 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty 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 d4cb8e090..2ae3465b9 100644 --- a/ICSharpCode.Decompiler/CSharp/CallBuilder.cs +++ b/ICSharpCode.Decompiler/CSharp/CallBuilder.cs @@ -2037,7 +2037,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 @@ -2045,36 +2050,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) @@ -2124,7 +2110,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) @@ -2242,13 +2228,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 67867375f..74a66600f 100644 --- a/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs +++ b/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs @@ -560,7 +560,33 @@ namespace ICSharpCode.Decompiler.CSharp protected internal override TranslatedExpression VisitCachedDelegate(CachedDelegate inst, TranslationContext context) { - return Translate(inst.Argument, context.TypeHint).WithILInstruction(inst); + 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) diff --git a/ICSharpCode.Decompiler/CSharp/TranslatedExpression.cs b/ICSharpCode.Decompiler/CSharp/TranslatedExpression.cs index 58c5018b2..a22c8db80 100644 --- a/ICSharpCode.Decompiler/CSharp/TranslatedExpression.cs +++ b/ICSharpCode.Decompiler/CSharp/TranslatedExpression.cs @@ -27,7 +27,6 @@ using ICSharpCode.Decompiler.CSharp.Transforms; using ICSharpCode.Decompiler.IL; using ICSharpCode.Decompiler.Semantics; using ICSharpCode.Decompiler.TypeSystem; -using ICSharpCode.Decompiler.TypeSystem.Implementation; using ICSharpCode.Decompiler.Util; #nullable enable @@ -255,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 @@ -271,13 +270,10 @@ namespace ICSharpCode.Decompiler.CSharp else if (Expression is ObjectCreateExpression oce && conversion.Conversion.IsMethodGroupConversion && oce.Arguments.Count == 1 && expressionBuilder.settings.UseImplicitMethodGroupConversion) { - // C# 11 caches static method groups. Keep explicit construction when the IL creates a fresh delegate. - if (conversion.Conversion.Method.IsStatic - && conversion.Conversion.Method is not LocalFunctionMethod { IsStaticLocalFunction: false } - && conversion.Conversion.Method.Parameters.Count == type.GetDelegateInvokeMethod()?.Parameters.Count - && expressionBuilder.settings.GetMinimumRequiredVersion() >= LanguageVersion.CSharp11_0 - && expressionBuilder.currentFunction.Kind != ILFunctionKind.ExpressionTree - && !ILInstructions.Any(i => i is CachedDelegate)) + // 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; } @@ -360,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) @@ -690,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 e0bcba5c5..a41ddf145 100644 --- a/ICSharpCode.Decompiler/FlowAnalysis/DataFlowVisitor.cs +++ b/ICSharpCode.Decompiler/FlowAnalysis/DataFlowVisitor.cs @@ -751,34 +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) { - DebugStartPoint(inst); - State cachedState = state.Clone(); - inst.Argument.AcceptVisitor(this); - state.JoinWith(cachedState); - DebugEndPoint(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/Transforms/CachedDelegateInitialization.cs b/ICSharpCode.Decompiler/IL/Transforms/CachedDelegateInitialization.cs index 93941cdbe..68581b738 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/CachedDelegateInitialization.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/CachedDelegateInitialization.cs @@ -33,46 +33,40 @@ 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) @@ -147,38 +141,6 @@ namespace ICSharpCode.Decompiler.IL.Transforms return true; } - /// - /// stloc s(ldobj(ldsflda(CachedAnonMethodDelegate)) - /// if (comp(ldloc s == null)) { - /// stloc s(stobj(ldsflda(CachedAnonMethodDelegate), DelegateConstruction)) - /// } - /// => - /// stloc s(cached.delegate(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 = new CachedDelegate(stobj.Value); - return true; - } - /// /// stloc s(ldobj(ldflda(CachedAnonMethodDelegate)) /// if (comp(ldloc s == null)) { @@ -186,6 +148,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms /// } /// => /// stloc s(cached.delegate(DelegateConstruction)) + /// The same pattern applies to static cache fields accessed with ldsflda. /// bool CachedDelegateInitializationRoslynWithLocal(IfInstruction inst) { @@ -200,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 = new CachedDelegate(stobj.Value); + storeBeforeIf.Value = new CachedDelegate(delegateConstruction); return true; } diff --git a/global.json b/global.json index 58a3053ca..115ca7301 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "11.0.100-preview.7.26381.103", + "version": "11.0.0", "rollForward": "major", "allowPrerelease": true },