Skip to content

Delay exceptions when copy-propagating an address into a StObj target - #3964

Open
siegfriedpammer wants to merge 1 commit into
masterfrom
fix-copypropagation-stobj-target
Open

Delay exceptions when copy-propagating an address into a StObj target#3964
siegfriedpammer wants to merge 1 commit into
masterfrom
fix-copypropagation-stobj-target

Conversation

@siegfriedpammer

Copy link
Copy Markdown
Member

Decompiling with a language version below C# 7 can trip a debug assertion in StObj.CheckTargetSlot:

Debug.Assert(SemanticHelper.IsPure(this.Value.Flags))
  ICSharpCode.Decompiler/IL/Instructions/LdFlda.cs:93

Reproduces on the existing test assemblies, e.g. CompoundAssignmentTest.ShortUnsignedRightShiftTest at C# 6 and below, and DynamicTests.StructMemberAccess at C# 3 and below. Minimal case:

public struct CustomStruct { public short ShortField; }
static void M(CustomStruct s) { s.ShortField >>>= 5; }   // decompiled at <= C# 6

CanPerformCopyPropagation permits copying LdFlda/LdElema when UseRefLocalsForAccurateOrderOfEvaluation is off, but DoPropagate puts the clone into a StObj target slot without consulting StObj.SatisfiesSlotRestrictionForInlining, leaving stobj(ldflda F(ldloc C_0), <impure value>).

ILInlining.FindLoadInNext already handles the same conflict by setting DelayExceptions on the address instead of falling back to a ref local. Copy propagation now does the same, gated on the same setting that drives InliningOptions.AllowChangingOrderOfEvaluationForExceptions. The check sits in DoPropagate rather than CanPerformCopyPropagation because the public Propagate() entry point bypasses the latter -- AsyncAwaitDecompiler copies an ldflda of the builder field through it regardless of the setting.

Decompiled output for DynamicTests and CompoundAssignmentTest at C# 3 and C# 6 is byte-identical before and after; only the IL invariant changes.

New Ugly test NoUnsignedRightShift (decompiled at LanguageVersion.CSharp6) fails on master and passes with the fix.


Prepared by an AI agent (Claude) on Siegfried's behalf.

Below C# 7 ref locals are unavailable, so CopyPropagation is allowed to copy
LdFlda/LdElema. When such a copy lands in a StObj target slot whose value is
impure, it violates the invariant checked by StObj.CheckTargetSlot: C# computes
the value to be stored before dereferencing the target, so the exception moves.
ILInlining resolves the same conflict by marking the address as delayed rather
than falling back to a ref local; copy propagation now does the same, which
keeps the generated code unchanged and only repairs the IL.

Refusing the propagation instead would emit a ref local at a language version
that cannot express one. The relaxation is bound to the setting that gates
InliningOptions.AllowChangingOrderOfEvaluationForExceptions, and the check sits
in DoPropagate rather than CanPerformCopyPropagation because the public
Propagate() entry point bypasses the latter -- AsyncAwaitDecompiler copies an
ldflda of the builder field through it irrespective of the setting.

Assisted-by: Claude:claude-opus-5:Claude Code
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant