From 3b5201cabc97076fabff74ccd5ce1fa24679d068 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Sat, 3 Oct 2026 03:33:10 +0200 Subject: [PATCH] Fix #4153: Keep foreach when a named argument reads the loop variable Once a named argument is hoisted, the call becomes a CallWithNamedArgs block, and inlining moves the Current getter into the block's first argument store. The foreach detection could not move it back out: CanInlineIntoSlot refused the block, and for an instance call the store of the 'this' argument in front counted as a write the getter cannot be moved past. That store only writes the block's own variable, so the getter is checked against the stored value instead, the same way CanExtendNamedArgument allows inlining into that position. --- .../PrettyTestRunner.cs | 6 +++ .../TestCases/Pretty/Issue4153.cs | 52 +++++++++++++++++++ .../IL/Instructions/Block.cs | 4 ++ .../IL/Transforms/ILInlining.cs | 4 ++ 4 files changed, 66 insertions(+) create mode 100644 ICSharpCode.Decompiler.Tests/TestCases/Pretty/Issue4153.cs diff --git a/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs b/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs index f5d4d50..bb5684a 100644 --- a/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs +++ b/ICSharpCode.Decompiler.Tests/PrettyTestRunner.cs @@ -932,6 +932,12 @@ namespace ICSharpCode.Decompiler.Tests await RunForLibrary(cscOptions: cscOptions); } + [Test] + public async Task Issue4153([ValueSource(nameof(roslynOnlyWithNet40Options))] CompilerOptions cscOptions) + { + await RunForLibrary(cscOptions: cscOptions); + } + [Test] public async Task AssemblyCustomAttributes([ValueSource(nameof(defaultOptions))] CompilerOptions cscOptions) { diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Issue4153.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Issue4153.cs new file mode 100644 index 0000000..85319ca --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Issue4153.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; + +namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty +{ + internal class Issue4153 + { + private static int G(int i) + { + return i; + } + + private static int M(int x, int y) + { + return x + y; + } + + private int N(int x, int y) + { + return x - y; + } + + public static int DictionaryEntries(Dictionary d) + { + int num = 0; + foreach (KeyValuePair item in d) + { + num += M(y: G(item.Key), x: G(2)); + } + return num; + } + + public static int ListOfPairs(List> list) + { + int num = 0; + foreach (KeyValuePair item in list) + { + num += M(y: G(item.Key), x: G(2)); + } + return num; + } + + public int InstanceCall(Dictionary d) + { + int num = 0; + foreach (KeyValuePair item in d) + { + num += N(y: G(item.Key), x: G(2)); + } + return num; + } + } +} diff --git a/ICSharpCode.Decompiler/IL/Instructions/Block.cs b/ICSharpCode.Decompiler/IL/Instructions/Block.cs index af03ca0..02e46d3 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/Block.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/Block.cs @@ -271,6 +271,10 @@ namespace ICSharpCode.Decompiler.IL case BlockKind.CallInlineAssign: // Allow inlining into the first instruction of the block return childIndex == 0; + case BlockKind.CallWithNamedArgs: + // Same, but the first named argument of an instance call comes after its 'this' argument, + // see NamedArgumentTransform.CanExtendNamedArgument. + return childIndex == 0 || (childIndex == 1 && FinalInstruction is CallInstruction { IsInstanceCall: true }); default: return false; } diff --git a/ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs b/ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs index 6c0c98c..2833ebc 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs @@ -942,6 +942,10 @@ namespace ICSharpCode.Decompiler.IL.Transforms for (int i = 0; i < childIndex; ++i) { ILInstruction predecessor = inst.Parent.Children[i]; + // The variable of a named argument only exists within its block, + // so only the stored value can interfere with the moved expression. + if (predecessor is StLoc { Variable.Kind: VariableKind.NamedArgument } namedArgument) + predecessor = namedArgument.Value; if (!IsSafeForInlineOver(predecessor, expressionBeingMoved)) return false; }