diff --git a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj index cba53f058..58ebff48b 100644 --- a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj +++ b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj @@ -274,6 +274,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 000000000..597834121 --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/Ugly/NoUnsignedRightShift.Expected.cs @@ -0,0 +1,30 @@ +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) + { + s.ShortField = (short)((uint)s.ShortField >> 5); + } + + public static void ArrayElement(short[] a) + { + a[0] = (short)((uint)a[0] >> 5); + } + } +} diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Ugly/NoUnsignedRightShift.cs b/ICSharpCode.Decompiler.Tests/TestCases/Ugly/NoUnsignedRightShift.cs new file mode 100644 index 000000000..b01d6a1d4 --- /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 49d151986..c89807843 100644 --- a/ICSharpCode.Decompiler.Tests/UglyTestRunner.cs +++ b/ICSharpCode.Decompiler.Tests/UglyTestRunner.cs @@ -174,6 +174,12 @@ namespace ICSharpCode.Decompiler.Tests }); } + [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 f55bee55a..c2898ed71 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/CopyPropagation.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/CopyPropagation.cs @@ -16,6 +16,7 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. +using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -145,6 +146,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms static void DoPropagate(ILVariable v, ILInstruction copiedExpr, Block block, ref int i, ILTransformContext context) { context.Step($"Copy propagate {v.Name}", copiedExpr); + int firstUninlinedArg = i; // un-inline the arguments of the ldArg instruction ILVariable[] uninlinedArgs = new ILVariable[copiedExpr.Children.Count]; for (int j = 0; j < uninlinedArgs.Length; j++) @@ -168,11 +170,38 @@ namespace ICSharpCode.Decompiler.IL.Transforms } // We are copying an expression from far away, reusing the ILRange would result in incorrect sequence points. clone.SetILRange(new Interval()); + if (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, mirroring + // InliningOptions.AllowChangingOrderOfEvaluationForExceptions in ILInlining. + // Unlike inlining, copy propagation has no third option of just giving up: the + // defining store is gone by the end of this loop, so every load must be replaced. + // Refusing the copy is only possible before propagation starts, which is what + // CanPerformCopyPropagation does when ref locals are requested; the assert + // guards the public Propagate() entry point, which bypasses that check. + Debug.Assert(!context.Settings.UseRefLocalsForAccurateOrderOfEvaluation); + if (clone is LdFlda ldflda) + ldflda.DelayExceptions = true; + else if (clone is LdElema ldelema) + ldelema.DelayExceptions = true; + } expr.ReplaceWith(clone); } block.Instructions.RemoveAt(i); int c = ILInlining.InlineInto(block, i, InliningOptions.None, context: context); i -= c + 1; + if (uninlinedArgs.Length > 0) + { + // The stores holding the un-inlined arguments are themselves copy-propagation + // candidates: copying an ldflda leaves its target address behind in a stack slot, + // and if that address is e.g. an ldloca it can be copied as well. They sit before + // the current position, so rewind far enough for the caller's loop to visit them; + // otherwise a multiply-loaded stack slot survives as a spurious ref local. + i = Math.Min(i, firstUninlinedArg - 1); + } } } }