diff --git a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj index 29015d86ae..3e05ecf6eb 100644 --- a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj +++ b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj @@ -253,6 +253,8 @@ + + diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Ugly/NoUnsignedRightShift.Expected.cs b/ICSharpCode.Decompiler.Tests/TestCases/Ugly/NoUnsignedRightShift.Expected.cs new file mode 100644 index 0000000000..efd76dceed --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/Ugly/NoUnsignedRightShift.Expected.cs @@ -0,0 +1,33 @@ +namespace ICSharpCode.Decompiler.Tests.TestCases.Ugly +{ + internal class NoUnsignedRightShift + { + public struct CustomStruct + { + public short ShortField; + } + + public class CustomClass + { + public short ShortField; + } + + public static void ClassField(CustomClass c) + { + c.ShortField = (short)((uint)c.ShortField >> 5); + } + + public static void StructField(CustomStruct s) + { + ref CustomStruct reference = ref s; + reference.ShortField = (short)((uint)reference.ShortField >> 5); + } + + public static void ArrayElement(short[] a) + { + short[] array = a; + int num = 0; + array[num] = (short)((uint)array[num] >> 5); + } + } +} diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Ugly/NoUnsignedRightShift.cs b/ICSharpCode.Decompiler.Tests/TestCases/Ugly/NoUnsignedRightShift.cs new file mode 100644 index 0000000000..b01d6a1d40 --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/Ugly/NoUnsignedRightShift.cs @@ -0,0 +1,52 @@ +// Copyright (c) 2026 Siegfried Pammer +// +// 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. + +namespace ICSharpCode.Decompiler.Tests.TestCases.Ugly +{ + // The unsigned right shift operator requires C# 11, so at a lower language version the + // compound assignments below have to be expanded into a plain assignment. That turns the + // stored value into a non-pure expression, which the target of the store must not be + // copy-propagated across. + internal class NoUnsignedRightShift + { + public struct CustomStruct + { + public short ShortField; + } + + public class CustomClass + { + public short ShortField; + } + + public static void ClassField(CustomClass c) + { + c.ShortField >>>= 5; + } + + public static void StructField(CustomStruct s) + { + s.ShortField >>>= 5; + } + + public static void ArrayElement(short[] a) + { + a[0] >>>= 5; + } + } +} diff --git a/ICSharpCode.Decompiler.Tests/UglyTestRunner.cs b/ICSharpCode.Decompiler.Tests/UglyTestRunner.cs index 6256ec52ac..4dde3afa02 100644 --- a/ICSharpCode.Decompiler.Tests/UglyTestRunner.cs +++ b/ICSharpCode.Decompiler.Tests/UglyTestRunner.cs @@ -144,6 +144,12 @@ public async Task AggressiveScalarReplacementOfAggregates([ValueSource(nameof(ro }); } + [Test] + public async Task NoUnsignedRightShift([ValueSource(nameof(roslynOnlyOptions))] CompilerOptions cscOptions) + { + await RunForLibrary(cscOptions: cscOptions, decompilerSettings: new DecompilerSettings(CSharp.LanguageVersion.CSharp6)); + } + [Test] public async Task NoNewOfT([ValueSource(nameof(defaultOptions))] CompilerOptions cscOptions) { diff --git a/ICSharpCode.Decompiler/IL/Transforms/CopyPropagation.cs b/ICSharpCode.Decompiler/IL/Transforms/CopyPropagation.cs index f55bee55a5..18284867e3 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/CopyPropagation.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/CopyPropagation.cs @@ -168,6 +168,22 @@ static void DoPropagate(ILVariable v, ILInstruction copiedExpr, Block block, ref } // We are copying an expression from far away, reusing the ILRange would result in incorrect sequence points. clone.SetILRange(new Interval()); + if (!context.Settings.UseRefLocalsForAccurateOrderOfEvaluation + && expr.SlotInfo == StObj.TargetSlot && clone.HasDirectFlag(InstructionFlags.MayThrow) + && !expr.Parent.SatisfiesSlotRestrictionForInlining(expr.ChildIndex, clone)) + { + // A LdFlda/LdElema used as StObj target has to delay its exception, because C# + // computes the value to be stored before dereferencing the target. Accept the + // changed point at which the exception is thrown in order to avoid introducing a + // ref local. This mirrors InliningOptions.AllowChangingOrderOfEvaluationForExceptions + // in ILInlining, and is bound to the same setting: callers of the public Propagate() + // reach this code path without passing through CanPerformCopyPropagation, so the + // setting has to be honored here rather than assumed. + if (clone is LdFlda ldflda) + ldflda.DelayExceptions = true; + else if (clone is LdElema ldelema) + ldelema.DelayExceptions = true; + } expr.ReplaceWith(clone); } block.Instructions.RemoveAt(i);