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.
This commit is contained in:
dualfroz committed 2026-10-03 03:33:10 +02:00
1 parent 3e462ad472
commit 3b5201cabc
4 files changed
+66

No files matched your search

@@ -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;
}
@@ -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;
}