diff --git a/.github/workflows/build-ilspy.yml b/.github/workflows/build-ilspy.yml index a112c3a39..d36ee142c 100644 --- a/.github/workflows/build-ilspy.yml +++ b/.github/workflows/build-ilspy.yml @@ -60,7 +60,7 @@ jobs: run: msbuild ILSpy.sln /p:Configuration=${{ matrix.configuration }} /p:Platform=$env:BuildPlatform /m - name: Execute unit tests - run: dotnet test --logger "junit;LogFileName=${{ matrix.configuration }}.xml" --results-directory test-results $env:Tests1 $env:Tests2 $env:Tests3 + run: dotnet test --logger "trx;LogFileName=${{ matrix.configuration }}.trx" --results-directory test-results $env:Tests1 $env:Tests2 $env:Tests3 env: Tests1: ICSharpCode.Decompiler.Tests\bin\${{ matrix.configuration }}\net8.0-windows\win-x64\ICSharpCode.Decompiler.Tests.dll Tests2: ILSpy.Tests\bin\${{ matrix.configuration }}\net8.0-windows\ILSpy.Tests.dll @@ -71,13 +71,14 @@ jobs: if: success() || failure() with: name: test-results-${{ matrix.configuration }} - path: 'test-results/${{ matrix.configuration }}.xml' + path: 'test-results/${{ matrix.configuration }}.trx' - name: Create Test Report - uses: test-summary/action@v2 + uses: icsharpcode/test-summary-action@dist if: always() with: - paths: "test-results/${{ matrix.configuration }}.xml" + paths: "test-results/${{ matrix.configuration }}.trx" + folded: true - name: Format check run: dotnet-format whitespace --verify-no-changes --verbosity detailed ILSpy.sln diff --git a/ICSharpCode.Decompiler.Tests/CorrectnessTestRunner.cs b/ICSharpCode.Decompiler.Tests/CorrectnessTestRunner.cs index a0901f255..cf3974d2f 100644 --- a/ICSharpCode.Decompiler.Tests/CorrectnessTestRunner.cs +++ b/ICSharpCode.Decompiler.Tests/CorrectnessTestRunner.cs @@ -16,8 +16,6 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. -using System; -using System.CodeDom.Compiler; using System.IO; using System.Linq; using System.Runtime.CompilerServices; @@ -313,6 +311,12 @@ namespace ICSharpCode.Decompiler.Tests await RunIL("Jmp.il"); } + [Test] + public async Task NonGenericConstrainedCallVirt() + { + await RunIL("NonGenericConstrainedCallVirt.il", CompilerOptions.UseRoslynLatest); + } + [Test] public async Task StackTests() { diff --git a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj index f19374fd5..ee4393ea7 100644 --- a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj +++ b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj @@ -17,7 +17,7 @@ True 1701;1702;1705,67,169,1058,728,1720,649,168,251,660,661,675;1998;162;8632;626;8618;8714;8602;8981 - ROSLYN;NET60;CS60;CS70;CS71;CS72;CS73;CS80;CS90;CS100;CS110;CS120 + ROSLYN;ROSLYN2;ROSLYN3;ROSLYN4;NET60;CS60;CS70;CS71;CS72;CS73;CS80;CS90;CS100;CS110;CS120 False False @@ -95,6 +95,7 @@ + diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Correctness/NonGenericConstrainedCallVirt.il b/ICSharpCode.Decompiler.Tests/TestCases/Correctness/NonGenericConstrainedCallVirt.il new file mode 100644 index 000000000..2fd15600e --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/Correctness/NonGenericConstrainedCallVirt.il @@ -0,0 +1,143 @@ +.assembly extern mscorlib +{ + .publickeytoken = (B7 7A 5C 56 19 34 E0 89 ) // .z\V.4.. + .ver 4:0:0:0 +} + +.assembly _ +{ + .hash algorithm 0x00008004 // SHA1 + .ver 0:0:0:0 +} + +.class public auto ansi beforefieldinit NonGenericConstrainedCallVirt + extends [mscorlib]System.Object +{ + // Methods + .method public hidebysig static + void Main () cil managed + { + // Method begins at RVA 0x2050 + // Code size 16 (0x10) + .entrypoint + .maxstack 8 + + IL_0000: ldstr "A" + IL_0005: newobj instance void C::.ctor(string) + IL_000a: call void NonGenericConstrainedCallVirt::Foo(class C) + IL_000f: ret + } // end of method NonGenericConstrainedCallVirt::Main + + .method private hidebysig static + void Foo ( + class C arg + ) cil managed + { + // Method begins at RVA 0x2064 + // Code size 25 (0x19) + .maxstack 8 + + IL_0000: ldarga arg + IL_0004: ldarga arg + IL_0008: call int32 NonGenericConstrainedCallVirt::Bar(class C&) + IL_000d: constrained. C + IL_0013: callvirt instance void C::Baz(int32) + IL_0018: ret + } // end of method NonGenericConstrainedCallVirt::Foo + + .method private hidebysig static + int32 Bar ( + class C& o + ) cil managed + { + // Method begins at RVA 0x2080 + // Code size 14 (0xe) + .maxstack 8 + + IL_0000: ldarg.0 + IL_0001: ldstr "B" + IL_0006: newobj instance void C::.ctor(string) + IL_000b: stind.ref + IL_000c: ldc.i4.0 + IL_000d: ret + } // end of method NonGenericConstrainedCallVirt::Bar + + .method public hidebysig specialname rtspecialname + instance void .ctor () cil managed + { + // Method begins at RVA 0x2090 + // Code size 7 (0x7) + .maxstack 8 + + IL_0000: ldarg.0 + IL_0001: call instance void [mscorlib]System.Object::.ctor() + IL_0006: ret + } // end of method NonGenericConstrainedCallVirt::.ctor + +} // end of class NonGenericConstrainedCallVirt + +.class public auto ansi beforefieldinit C + extends [mscorlib]System.Object +{ + // Fields + .field private initonly string 'k__BackingField' + .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( + 01 00 00 00 + ) + + // Methods + .method public hidebysig specialname rtspecialname + instance void .ctor ( + string n + ) cil managed + { + // Method begins at RVA 0x2098 + // Code size 14 (0xe) + .maxstack 8 + + IL_0000: ldarg.0 + IL_0001: call instance void [mscorlib]System.Object::.ctor() + IL_0006: ldarg.0 + IL_0007: ldarg.1 + IL_0008: stfld string C::'k__BackingField' + IL_000d: ret + } // end of method C::.ctor + + .method public hidebysig + instance void Baz ( + int32 arg + ) cil managed + { + // Method begins at RVA 0x20a8 + // Code size 12 (0xc) + .maxstack 8 + + IL_0000: ldarg.0 + IL_0001: call instance string C::get_Name() + IL_0006: call void [mscorlib]System.Console::WriteLine(string) + IL_000b: ret + } // end of method C::Baz + + .method public hidebysig specialname + instance string get_Name () cil managed + { + .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( + 01 00 00 00 + ) + // Method begins at RVA 0x20b8 + // Code size 7 (0x7) + .maxstack 8 + + IL_0000: ldarg.0 + IL_0001: ldfld string C::'k__BackingField' + IL_0006: ret + } // end of method C::get_Name + + // Properties + .property instance string Name() + { + .get instance string C::get_Name() + } + +} // end of class C + diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Generics.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Generics.cs index 6d9d1672a..c02845c9c 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Generics.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Generics.cs @@ -84,6 +84,11 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty { void Method1() where T : class; void Method2() where T : class; + + void Method3(int a, string b, Type c); +#if CS72 + void Method4(in int a); +#endif } public abstract class Base : IInterface @@ -95,6 +100,16 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty void IInterface.Method2() { } + + void IInterface.Method3(int a, string b, Type c) + { + } + +#if CS72 + void IInterface.Method4(in int a) + { + } +#endif } public class Derived : Base @@ -296,5 +311,44 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty { return default(T).ToString(); } + + public static void ConstrainedCall(T x, ref T y) where T : IDisposable + { + x.Dispose(); + y.Dispose(); + } + + // prior to C# 7.0 UseRefLocalsForAccurateOrderOfEvaluation is disabled, so we will inline. + // Roslyn 4 generates the explicit ldobj.if.ref pattern, so we can also inline. + // The versions in between, we don't inline, so the code doesn't look pretty. +#if ROSLYN4 || !CS70 + public static int[] Issue3438(T[] array) + { + List list = new List(); + for (int i = 0; i < array.Length; i++) + { + if (!array[i].Equals(default(T))) + { + list.Add(i); + } + } + return list.ToArray(); + } + public void Issue3438b(T[] item1, T item2, int item3) where T : IInterface + { + item1[CastToInt(item2)].Method3(CastToInt(item2), CastToString(item2), TestTypeOf()[1]); + } + public void Issue3438c(T item, T item2, int item3) where T : IInterface + { + CastFromInt(item3).Method3(CastToInt(item2), CastToString(item2), TestTypeOf()[1]); + } + //#if CS72 + // Disabled because ILInlining does not support inlining ldloca currently. + // public void Issue3438d(T[] item1, T item2, int item3) where T : IInterface + // { + // item1[CastToInt(item2)].Method4(CastToInt(item2)); + // } + //#endif +#endif } } diff --git a/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs b/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs index b239da3a2..07a4126de 100644 --- a/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs +++ b/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs @@ -149,6 +149,7 @@ namespace ICSharpCode.Decompiler.CSharp new IndexRangeTransform(), new DeconstructionTransform(), new NamedArgumentTransform(), + new RemoveUnconstrainedGenericReferenceTypeCheck(), new UserDefinedLogicTransform(), new InterpolatedStringTransform() ), @@ -1717,6 +1718,7 @@ namespace ICSharpCode.Decompiler.CSharp { var ilReader = new ILReader(typeSystem.MainModule) { UseDebugSymbols = settings.UseDebugSymbols, + UseRefLocalsForAccurateOrderOfEvaluation = settings.UseRefLocalsForAccurateOrderOfEvaluation, DebugInfo = DebugInfoProvider }; var methodDef = metadata.GetMethodDefinition((MethodDefinitionHandle)method.MetadataToken); diff --git a/ICSharpCode.Decompiler/CSharp/CallBuilder.cs b/ICSharpCode.Decompiler/CSharp/CallBuilder.cs index 5a5fe7802..b8b6680e5 100644 --- a/ICSharpCode.Decompiler/CSharp/CallBuilder.cs +++ b/ICSharpCode.Decompiler/CSharp/CallBuilder.cs @@ -356,11 +356,18 @@ namespace ICSharpCode.Decompiler.CSharp } else { + var thisArg = callArguments.FirstOrDefault(); + if (thisArg is LdObjIfRef ldObjIfRef) + { + Debug.Assert(constrainedTo != null); + thisArg = ldObjIfRef.Target; + } target = expressionBuilder.TranslateTarget( - callArguments.FirstOrDefault(), + thisArg, nonVirtualInvocation: callOpCode == OpCode.Call || method.IsConstructor, memberStatic: method.IsStatic, - memberDeclaringType: constrainedTo ?? method.DeclaringType); + memberDeclaringType: method.DeclaringType, + constrainedTo: constrainedTo); if (constrainedTo == null && target.Expression is CastExpression cast && target.ResolveResult is ConversionResolveResult conversion diff --git a/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs b/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs index 90df1890a..8398be47a 100644 --- a/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs +++ b/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs @@ -2614,7 +2614,7 @@ namespace ICSharpCode.Decompiler.CSharp } internal TranslatedExpression TranslateTarget(ILInstruction target, bool nonVirtualInvocation, - bool memberStatic, IType memberDeclaringType) + bool memberStatic, IType memberDeclaringType, IType constrainedTo = null) { // If references are missing member.IsStatic might not be set correctly. // Additionally check target for null, in order to avoid a crash. @@ -2630,8 +2630,8 @@ namespace ICSharpCode.Decompiler.CSharp } else { - IType targetTypeHint = memberDeclaringType; - if (CallInstruction.ExpectedTypeForThisPointer(memberDeclaringType) == StackType.Ref) + IType targetTypeHint = constrainedTo ?? memberDeclaringType; + if (CallInstruction.ExpectedTypeForThisPointer(memberDeclaringType, constrainedTo) == StackType.Ref) { if (target.ResultType == StackType.Ref) { @@ -2643,13 +2643,13 @@ namespace ICSharpCode.Decompiler.CSharp } } var translatedTarget = Translate(target, targetTypeHint); - if (CallInstruction.ExpectedTypeForThisPointer(memberDeclaringType) == StackType.Ref) + if (CallInstruction.ExpectedTypeForThisPointer(memberDeclaringType, constrainedTo) == StackType.Ref) { // When accessing members on value types, ensure we use a reference of the correct type, // and not a pointer or a reference to a different type (issue #1333) - if (!(translatedTarget.Type is ByReferenceType brt && NormalizeTypeVisitor.TypeErasure.EquivalentTypes(brt.ElementType, memberDeclaringType))) + if (!(translatedTarget.Type is ByReferenceType brt && NormalizeTypeVisitor.TypeErasure.EquivalentTypes(brt.ElementType, constrainedTo ?? memberDeclaringType))) { - translatedTarget = translatedTarget.ConvertTo(new ByReferenceType(memberDeclaringType), this); + translatedTarget = translatedTarget.ConvertTo(new ByReferenceType(constrainedTo ?? memberDeclaringType), this); } } if (translatedTarget.Expression is DirectionExpression) @@ -2675,9 +2675,9 @@ namespace ICSharpCode.Decompiler.CSharp } else { - return new TypeReferenceExpression(ConvertType(memberDeclaringType)) + return new TypeReferenceExpression(ConvertType(constrainedTo ?? memberDeclaringType)) .WithoutILInstruction() - .WithRR(new TypeResolveResult(memberDeclaringType)); + .WithRR(new TypeResolveResult(constrainedTo ?? memberDeclaringType)); } bool ShouldUseBaseReference() @@ -2686,7 +2686,7 @@ namespace ICSharpCode.Decompiler.CSharp return false; if (!MatchLdThis(target)) return false; - if (memberDeclaringType.GetDefinition() == resolver.CurrentTypeDefinition) + if ((constrainedTo ?? memberDeclaringType).GetDefinition() == resolver.CurrentTypeDefinition) return false; return true; } diff --git a/ICSharpCode.Decompiler/DecompilerSettings.cs b/ICSharpCode.Decompiler/DecompilerSettings.cs index f4fac4485..a0bd68f36 100644 --- a/ICSharpCode.Decompiler/DecompilerSettings.cs +++ b/ICSharpCode.Decompiler/DecompilerSettings.cs @@ -92,7 +92,6 @@ namespace ICSharpCode.Decompiler stringInterpolation = false; dictionaryInitializers = false; extensionMethodsInCollectionInitializers = false; - useRefLocalsForAccurateOrderOfEvaluation = false; getterOnlyAutomaticProperties = false; } if (languageVersion < CSharp.LanguageVersion.CSharp7) @@ -105,6 +104,7 @@ namespace ICSharpCode.Decompiler localFunctions = false; deconstruction = false; patternMatching = false; + useRefLocalsForAccurateOrderOfEvaluation = false; } if (languageVersion < CSharp.LanguageVersion.CSharp7_2) { @@ -190,11 +190,11 @@ namespace ICSharpCode.Decompiler return CSharp.LanguageVersion.CSharp7_2; // C# 7.1 missing if (outVariables || throwExpressions || tupleTypes || tupleConversions - || discards || localFunctions || deconstruction || patternMatching) + || discards || localFunctions || deconstruction || patternMatching || useRefLocalsForAccurateOrderOfEvaluation) return CSharp.LanguageVersion.CSharp7; if (awaitInCatchFinally || useExpressionBodyForCalculatedGetterOnlyProperties || nullPropagation || stringInterpolation || dictionaryInitializers || extensionMethodsInCollectionInitializers - || useRefLocalsForAccurateOrderOfEvaluation || getterOnlyAutomaticProperties) + || getterOnlyAutomaticProperties) return CSharp.LanguageVersion.CSharp6; if (asyncAwait) return CSharp.LanguageVersion.CSharp5; @@ -1126,7 +1126,7 @@ namespace ICSharpCode.Decompiler /// order of evaluation. /// See https://github.com/icsharpcode/ILSpy/issues/2050 /// - [Category("C# 6.0 / VS 2015")] + [Category("C# 7.0 / VS 2017")] [Description("DecompilerSettings.UseRefLocalsForAccurateOrderOfEvaluation")] public bool UseRefLocalsForAccurateOrderOfEvaluation { get { return useRefLocalsForAccurateOrderOfEvaluation; } diff --git a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj index 2a9eb220b..87797610f 100644 --- a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj +++ b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj @@ -107,6 +107,7 @@ + diff --git a/ICSharpCode.Decompiler/IL/ILReader.cs b/ICSharpCode.Decompiler/IL/ILReader.cs index 226218e12..272a8ed91 100644 --- a/ICSharpCode.Decompiler/IL/ILReader.cs +++ b/ICSharpCode.Decompiler/IL/ILReader.cs @@ -122,6 +122,7 @@ namespace ICSharpCode.Decompiler.IL readonly MetadataReader metadata; public bool UseDebugSymbols { get; set; } + public bool UseRefLocalsForAccurateOrderOfEvaluation { get; set; } public DebugInfo.IDebugInfoProvider? DebugInfo { get; set; } public List Warnings { get; } = new List(); @@ -1766,15 +1767,37 @@ namespace ICSharpCode.Decompiler.IL { int firstArgument = (opCode != OpCode.NewObj && !method.IsStatic) ? 1 : 0; var arguments = new ILInstruction[firstArgument + method.Parameters.Count]; + IType typeOfThis = constrainedPrefix ?? method.DeclaringType; + StackType expectedStackType = CallInstruction.ExpectedTypeForThisPointer(method.DeclaringType, constrainedPrefix); + bool requiresLdObjIfRef = firstArgument == 1 + && !firstArgumentIsStObjTarget + && UseRefLocalsForAccurateOrderOfEvaluation + && expectedStackType == StackType.Ref && typeOfThis.IsReferenceType != false; for (int i = method.Parameters.Count - 1; i >= 0; i--) { + if (requiresLdObjIfRef) + { + FlushExpressionStack(); + } + arguments[firstArgument + i] = Pop(method.Parameters[i].Type.GetStackType()); } if (firstArgument == 1) { - arguments[0] = firstArgumentIsStObjTarget - ? PopStObjTarget() - : Pop(CallInstruction.ExpectedTypeForThisPointer(constrainedPrefix ?? method.DeclaringType)); + ILInstruction firstArgumentInstruction; + if (firstArgumentIsStObjTarget) + { + firstArgumentInstruction = PopStObjTarget(); + } + else + { + firstArgumentInstruction = Pop(expectedStackType); + if (requiresLdObjIfRef) + { + firstArgumentInstruction = new LdObjIfRef(firstArgumentInstruction, typeOfThis); + } + } + arguments[0] = firstArgumentInstruction; } // arguments is in reverse order of the Pop calls, thus // arguments is now in the correct evaluation order. diff --git a/ICSharpCode.Decompiler/IL/Instructions.cs b/ICSharpCode.Decompiler/IL/Instructions.cs index c4ce2d8af..4f3ef843d 100644 --- a/ICSharpCode.Decompiler/IL/Instructions.cs +++ b/ICSharpCode.Decompiler/IL/Instructions.cs @@ -164,6 +164,8 @@ namespace ICSharpCode.Decompiler.IL IsInst, /// Indirect load (ref/pointer dereference). LdObj, + /// If argument is a ref to a reference type, loads the object reference, stores it in a temporary, and evaluates to the address of that temporary (address.of(ldobj(arg))). Otherwise, returns the argument ref as-is.This instruction represents the memory-load semantics of callvirt with a generic type as receiver (where the IL always takes a ref, but only methods on value types expect one, for method on reference types there's an implicit ldobj, which this instruction makes explicit in order to preserve the order-of-evaluation). + LdObjIfRef, /// Indirect store (store to ref/pointer). /// Evaluates to the value that was stored (when using type byte/short: evaluates to the truncated value, sign/zero extended back to I4 based on type.GetSign()) StObj, @@ -4085,6 +4087,116 @@ namespace ICSharpCode.Decompiler.IL } } namespace ICSharpCode.Decompiler.IL +{ + /// If argument is a ref to a reference type, loads the object reference, stores it in a temporary, and evaluates to the address of that temporary (address.of(ldobj(arg))). Otherwise, returns the argument ref as-is.This instruction represents the memory-load semantics of callvirt with a generic type as receiver (where the IL always takes a ref, but only methods on value types expect one, for method on reference types there's an implicit ldobj, which this instruction makes explicit in order to preserve the order-of-evaluation). + public sealed partial class LdObjIfRef : ILInstruction + { + public LdObjIfRef(ILInstruction target, IType type) : base(OpCode.LdObjIfRef) + { + this.Target = target; + this.type = type; + } + public static readonly SlotInfo TargetSlot = new SlotInfo("Target", canInlineInto: true); + ILInstruction target = null!; + public ILInstruction Target { + get { return this.target; } + set { + ValidateChild(value); + SetChildInstruction(ref this.target, value, 0); + } + } + protected sealed override int GetChildCount() + { + return 1; + } + protected sealed override ILInstruction GetChild(int index) + { + switch (index) + { + case 0: + return this.target; + default: + throw new IndexOutOfRangeException(); + } + } + protected sealed override void SetChild(int index, ILInstruction value) + { + switch (index) + { + case 0: + this.Target = value; + break; + default: + throw new IndexOutOfRangeException(); + } + } + protected sealed override SlotInfo GetChildSlot(int index) + { + switch (index) + { + case 0: + return TargetSlot; + default: + throw new IndexOutOfRangeException(); + } + } + public sealed override ILInstruction Clone() + { + var clone = (LdObjIfRef)ShallowClone(); + clone.Target = this.target.Clone(); + return clone; + } + IType type; + /// Returns the type operand. + public IType Type { + get { return type; } + set { type = value; InvalidateFlags(); } + } + public override StackType ResultType { get { return StackType.Ref; } } + protected override InstructionFlags ComputeFlags() + { + return target.Flags | InstructionFlags.SideEffect | InstructionFlags.MayThrow; + } + public override InstructionFlags DirectFlags { + get { + return InstructionFlags.SideEffect | InstructionFlags.MayThrow; + } + } + public override void WriteTo(ITextOutput output, ILAstWritingOptions options) + { + WriteILRange(output, options); + output.Write(OpCode); + output.Write(' '); + type.WriteTo(output); + output.Write('('); + this.target.WriteTo(output, options); + output.Write(')'); + } + public override void AcceptVisitor(ILVisitor visitor) + { + visitor.VisitLdObjIfRef(this); + } + public override T AcceptVisitor(ILVisitor visitor) + { + return visitor.VisitLdObjIfRef(this); + } + public override T AcceptVisitor(ILVisitor visitor, C context) + { + return visitor.VisitLdObjIfRef(this, context); + } + protected internal override bool PerformMatch(ILInstruction? other, ref Patterns.Match match) + { + var o = other as LdObjIfRef; + return o != null && this.target.PerformMatch(o.target, ref match) && type.Equals(o.type); + } + internal override void CheckInvariant(ILPhase phase) + { + base.CheckInvariant(phase); + DebugAssert(target.ResultType == StackType.Ref || target.ResultType == StackType.I); + } + } +} +namespace ICSharpCode.Decompiler.IL { /// Indirect store (store to ref/pointer). /// Evaluates to the value that was stored (when using type byte/short: evaluates to the truncated value, sign/zero extended back to I4 based on type.GetSign()) @@ -7071,6 +7183,10 @@ namespace ICSharpCode.Decompiler.IL { Default(inst); } + protected internal virtual void VisitLdObjIfRef(LdObjIfRef inst) + { + Default(inst); + } protected internal virtual void VisitStObj(StObj inst) { Default(inst); @@ -7473,6 +7589,10 @@ namespace ICSharpCode.Decompiler.IL { return Default(inst); } + protected internal virtual T VisitLdObjIfRef(LdObjIfRef inst) + { + return Default(inst); + } protected internal virtual T VisitStObj(StObj inst) { return Default(inst); @@ -7875,6 +7995,10 @@ namespace ICSharpCode.Decompiler.IL { return Default(inst, context); } + protected internal virtual T VisitLdObjIfRef(LdObjIfRef inst, C context) + { + return Default(inst, context); + } protected internal virtual T VisitStObj(StObj inst, C context) { return Default(inst, context); @@ -8086,6 +8210,7 @@ namespace ICSharpCode.Decompiler.IL "castclass", "isinst", "ldobj", + "ldobj.if.ref", "stobj", "box", "unbox", @@ -8591,6 +8716,19 @@ namespace ICSharpCode.Decompiler.IL type = default(IType); return false; } + public bool MatchLdObjIfRef([NotNullWhen(true)] out ILInstruction? target, [NotNullWhen(true)] out IType? type) + { + var inst = this as LdObjIfRef; + if (inst != null) + { + target = inst.Target; + type = inst.Type; + return true; + } + target = default(ILInstruction); + type = default(IType); + return false; + } public bool MatchStObj([NotNullWhen(true)] out ILInstruction? target, [NotNullWhen(true)] out ILInstruction? value, [NotNullWhen(true)] out IType? type) { var inst = this as StObj; diff --git a/ICSharpCode.Decompiler/IL/Instructions.tt b/ICSharpCode.Decompiler/IL/Instructions.tt index 72da23a16..6f42205af 100644 --- a/ICSharpCode.Decompiler/IL/Instructions.tt +++ b/ICSharpCode.Decompiler/IL/Instructions.tt @@ -255,6 +255,9 @@ new OpCode("ldobj", "Indirect load (ref/pointer dereference).", CustomClassName("LdObj"), CustomArguments(("target", new[] { "Ref", "I" })), HasTypeOperand, MemoryAccess, CustomWriteToButKeepOriginal, SupportsVolatilePrefix, SupportsUnalignedPrefix, MayThrow, ResultType("type.GetStackType()")), + new OpCode("ldobj.if.ref", "If argument is a ref to a reference type, loads the object reference, stores it in a temporary, and evaluates to the address of that temporary (address.of(ldobj(arg))). Otherwise, returns the argument ref as-is.This instruction represents the memory-load semantics of callvirt with a generic type as receiver (where the IL always takes a ref, but only methods on value types expect one, for method on reference types there's an implicit ldobj, which this instruction makes explicit in order to preserve the order-of-evaluation).", + CustomClassName("LdObjIfRef"), CustomArguments(("target", new[] { "Ref", "I" })), HasTypeOperand, MemoryAccess, + MayThrow, ResultType("Ref")), new OpCode("stobj", "Indirect store (store to ref/pointer)." + Environment.NewLine + "Evaluates to the value that was stored (when using type byte/short: evaluates to the truncated value, sign/zero extended back to I4 based on type.GetSign())", CustomClassName("StObj"), CustomArguments(("target", new[] { "Ref", "I" }), ("value", new[] { "type.GetStackType()" })), HasTypeOperand, MemoryAccess, CustomWriteToButKeepOriginal, @@ -916,7 +919,8 @@ namespace ICSharpCode.Decompiler.IL b = new StringBuilder(); b.AppendLine("protected sealed override ILInstruction GetChild(int index)"); b.AppendLine("{"); - b.AppendLine("\tswitch (index) {"); + b.AppendLine("\tswitch (index)"); + b.AppendLine("\t{"); for (int i = 0; i < childCount; i++) { b.AppendLine("\t\tcase " + i + ":"); b.AppendLine("\t\t\treturn this." + children[i].Name + ";"); @@ -933,7 +937,8 @@ namespace ICSharpCode.Decompiler.IL b = new StringBuilder(); b.AppendLine("protected sealed override void SetChild(int index, ILInstruction value)"); b.AppendLine("{"); - b.AppendLine("\tswitch (index) {"); + b.AppendLine("\tswitch (index)"); + b.AppendLine("\t{"); for (int i = 0; i < childCount; i++) { b.AppendLine("\t\tcase " + i + ":"); b.AppendLine("\t\t\tthis." + children[i].PropertyName + " = value;"); @@ -953,7 +958,8 @@ namespace ICSharpCode.Decompiler.IL b = new StringBuilder(); b.AppendLine("protected sealed override SlotInfo GetChildSlot(int index)"); b.AppendLine("{"); - b.AppendLine("\tswitch (index) {"); + b.AppendLine("\tswitch (index)"); + b.AppendLine("\t{"); for (int i = 0; i < childCount; i++) { b.AppendLine("\t\tcase " + i + ":"); b.AppendLine("\t\t\treturn " + children[i].SlotName + ";"); @@ -1113,7 +1119,8 @@ protected override void Disconnected() opCode.Members.Add("/// Returns the method operand." + Environment.NewLine + $"public IMethod{n} Method => method;"); opCode.GenerateWriteTo = true; - opCode.WriteOperand.Add("if (method != null) {"); + opCode.WriteOperand.Add("if (method != null)"); + opCode.WriteOperand.Add("{"); opCode.WriteOperand.Add("\toutput.Write(' ');"); opCode.WriteOperand.Add("\tmethod.WriteTo(output);"); opCode.WriteOperand.Add("}"); diff --git a/ICSharpCode.Decompiler/IL/Instructions/CallInstruction.cs b/ICSharpCode.Decompiler/IL/Instructions/CallInstruction.cs index ccdf1f5fa..ab61817eb 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/CallInstruction.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/CallInstruction.cs @@ -98,16 +98,19 @@ namespace ICSharpCode.Decompiler.IL /// /// Gets the expected stack type for passing the this pointer in a method call. - /// Returns StackType.O for reference types (this pointer passed as object reference), + /// Returns StackType.Ref if constrainedTo is not null, + /// StackType.O for reference types (this pointer passed as object reference), /// and StackType.Ref for type parameters and value types (this pointer passed as managed reference). /// /// Returns StackType.Unknown if the input type is unknown. /// - internal static StackType ExpectedTypeForThisPointer(IType type) + internal static StackType ExpectedTypeForThisPointer(IType declaringType, IType? constrainedTo) { - if (type.Kind == TypeKind.TypeParameter) + if (constrainedTo != null) return StackType.Ref; - switch (type.IsReferenceType) + if (declaringType.Kind == TypeKind.TypeParameter) + return StackType.Ref; + switch (declaringType.IsReferenceType) { case true: return StackType.O; @@ -125,7 +128,7 @@ namespace ICSharpCode.Decompiler.IL Debug.Assert(Method.Parameters.Count + firstArgument == Arguments.Count); if (firstArgument == 1) { - if (!(Arguments[0].ResultType == ExpectedTypeForThisPointer(ConstrainedTo ?? Method.DeclaringType))) + if (!(Arguments[0].ResultType == ExpectedTypeForThisPointer(Method.DeclaringType, ConstrainedTo))) Debug.Fail($"Stack type mismatch in 'this' argument in call to {Method.Name}()"); } for (int i = 0; i < Method.Parameters.Count; ++i) diff --git a/ICSharpCode.Decompiler/IL/Instructions/PatternMatching.cs b/ICSharpCode.Decompiler/IL/Instructions/PatternMatching.cs index 5c5391723..25e9a6498 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/PatternMatching.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/PatternMatching.cs @@ -581,5 +581,17 @@ namespace ICSharpCode.Decompiler.IL { return this; } + + public bool MatchLdObj([NotNullWhen(true)] out ILInstruction? target, IType type) + { + var inst = this as LdObj; + if (inst != null && inst.Type.Equals(type)) + { + target = inst.Target; + return true; + } + target = default(ILInstruction); + return false; + } } } diff --git a/ICSharpCode.Decompiler/IL/Transforms/ExpressionTransforms.cs b/ICSharpCode.Decompiler/IL/Transforms/ExpressionTransforms.cs index 1b8d09264..57b475694 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/ExpressionTransforms.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/ExpressionTransforms.cs @@ -475,6 +475,28 @@ namespace ICSharpCode.Decompiler.IL.Transforms } } + protected internal override void VisitLdObjIfRef(LdObjIfRef inst) + { + base.VisitLdObjIfRef(inst); + if (inst.Target is AddressOf) + { + context.Step("ldobj.if.ref(addressof(...)) -> addressof(...)", inst); + // there already is a temporary, so the ldobj.if.ref is a no-op in both cases + inst.ReplaceWith(inst.Target); + return; + } + if (inst.Target.MatchLdLoc(out var s) && s.IsSingleDefinition && s.LoadCount == 1 + && s.StoreInstructions.SingleOrDefault() is StLoc { + Value: LdLoca { Variable: { AddressCount: 1, StoreCount: 1 } } + }) + { + context.Step("Single use of ldobj.if.ref(ldloc v) -> ldloc v", inst); + // there already is a temporary, so the ldobj.if.ref is a no-op in both cases + inst.ReplaceWith(inst.Target); + return; + } + } + protected internal override void VisitStObj(StObj inst) { base.VisitStObj(inst); diff --git a/ICSharpCode.Decompiler/IL/Transforms/IILTransform.cs b/ICSharpCode.Decompiler/IL/Transforms/IILTransform.cs index e42551471..347e8e35c 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/IILTransform.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/IILTransform.cs @@ -79,6 +79,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms { return new ILReader(TypeSystem.MainModule) { UseDebugSymbols = Settings.UseDebugSymbols, + UseRefLocalsForAccurateOrderOfEvaluation = Settings.UseRefLocalsForAccurateOrderOfEvaluation, DebugInfo = DebugInfo }; } diff --git a/ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs b/ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs index 3e2c743c8..2b92f6cc5 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs @@ -170,14 +170,9 @@ namespace ICSharpCode.Decompiler.IL.Transforms public static bool InlineOneIfPossible(Block block, int pos, InliningOptions options, ILTransformContext context) { context.CancellationToken.ThrowIfCancellationRequested(); - StLoc stloc = block.Instructions[pos] as StLoc; - if (stloc == null || stloc.Variable.Kind == VariableKind.PinnedLocal) + if (block.Instructions[pos] is not StLoc stloc) return false; - ILVariable v = stloc.Variable; - // ensure the variable is accessed only a single time - if (v.StoreCount != 1) - return false; - if (v.LoadCount > 1 || v.LoadCount + v.AddressCount != 1) + if (!VariableCanBeUsedForInlining(stloc.Variable)) return false; // TODO: inlining of small integer types might be semantically incorrect, // but we can't avoid it this easily without breaking lots of tests. @@ -186,6 +181,18 @@ namespace ICSharpCode.Decompiler.IL.Transforms return InlineOne(stloc, options, context); } + public static bool VariableCanBeUsedForInlining(ILVariable v) + { + if (v.Kind == VariableKind.PinnedLocal) + return false; + // ensure the variable is accessed only a single time + if (v.StoreCount != 1) + return false; + if (v.LoadCount + v.AddressCount != 1) + return false; + return true; + } + /// /// Inlines the stloc instruction at block.Instructions[pos] into the next instruction. /// @@ -394,6 +401,10 @@ namespace ICSharpCode.Decompiler.IL.Transforms if (ldloca.Variable.Type.IsReferenceType ?? false) return false; ILInstruction inst = ldloca; + if (inst.Parent is LdObjIfRef) + { + inst = inst.Parent; + } while (inst.Parent is LdFlda ldflda) { inst = ldflda; @@ -904,6 +915,30 @@ namespace ICSharpCode.Decompiler.IL.Transforms return true; } + /// + /// Gets whether 'expressionBeingMoved' can be moved from somewhere before 'stmt' to become the replacement of 'targetLoad'. + /// + public static bool CanMoveIntoCallVirt(ILInstruction expressionBeingMoved, ILInstruction stmt, CallVirt nestedCallVirt, ILInstruction targetLoad) + { + Debug.Assert(targetLoad.IsDescendantOf(stmt) && nestedCallVirt.IsDescendantOf(stmt)); + ILInstruction thisArg = nestedCallVirt.Arguments[0]; + Debug.Assert(thisArg is LdObjIfRef); + for (ILInstruction inst = targetLoad; inst != stmt; inst = inst.Parent) + { + if (!inst.Parent.CanInlineIntoSlot(inst.ChildIndex, expressionBeingMoved)) + return false; + // Check whether re-ordering with predecessors is valid: + int childIndex = inst.ChildIndex; + for (int i = 0; i < childIndex; ++i) + { + ILInstruction predecessor = inst.Parent.Children[i]; + if (predecessor != thisArg && !IsSafeForInlineOver(predecessor, expressionBeingMoved)) + return false; + } + } + return true; + } + /// /// Gets whether arg can be un-inlined out of stmt. /// diff --git a/ICSharpCode.Decompiler/IL/Transforms/NamedArgumentTransform.cs b/ICSharpCode.Decompiler/IL/Transforms/NamedArgumentTransform.cs index 41e30e598..4454bf75e 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/NamedArgumentTransform.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/NamedArgumentTransform.cs @@ -99,7 +99,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms if (call.IsInstanceCall) { IType thisVarType = call.ConstrainedTo ?? call.Method.DeclaringType; - if (CallInstruction.ExpectedTypeForThisPointer(thisVarType) == StackType.Ref) + if (CallInstruction.ExpectedTypeForThisPointer(call.Method.DeclaringType, call.ConstrainedTo) == StackType.Ref) { thisVarType = new ByReferenceType(thisVarType); } diff --git a/ICSharpCode.Decompiler/IL/Transforms/NullPropagationTransform.cs b/ICSharpCode.Decompiler/IL/Transforms/NullPropagationTransform.cs index cb7d2161c..7a4b18deb 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/NullPropagationTransform.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/NullPropagationTransform.cs @@ -283,6 +283,10 @@ namespace ICSharpCode.Decompiler.IL.Transforms { inst = arg; } + else if (inst is LdObjIfRef ldObjIfRef) + { + inst = ldObjIfRef.Target; + } // ensure the access chain does not contain any 'nullable.unwrap' that aren't directly part of the chain if (ArgumentsAfterFirstMayUnwrapNull(call.Arguments)) return false; @@ -362,7 +366,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms && arg.MatchLdLoc(testedVar); case Mode.UnconstrainedType: // unconstrained generic type (expect: ldloc(testedVar)) - return inst.MatchLdLoc(testedVar); + return inst.MatchLdLoc(testedVar) || (inst.MatchLdObjIfRef(out var testedVarLoad, out _) && testedVarLoad.MatchLdLoc(testedVar)); default: throw new ArgumentOutOfRangeException(nameof(mode)); } diff --git a/ICSharpCode.Decompiler/IL/Transforms/RemoveInfeasiblePathTransform.cs b/ICSharpCode.Decompiler/IL/Transforms/RemoveInfeasiblePathTransform.cs index a337ea1ac..1a84016bf 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/RemoveInfeasiblePathTransform.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/RemoveInfeasiblePathTransform.cs @@ -54,8 +54,6 @@ namespace ICSharpCode.Decompiler.IL.Transforms } } } - - private bool DoTransform(Block block, ILTransformContext context) { if (!MatchBlock1(block, out var s, out int value, out var br)) diff --git a/ICSharpCode.Decompiler/IL/Transforms/RemoveUnconstrainedGenericReferenceTypeCheck.cs b/ICSharpCode.Decompiler/IL/Transforms/RemoveUnconstrainedGenericReferenceTypeCheck.cs new file mode 100644 index 000000000..83f48b5f6 --- /dev/null +++ b/ICSharpCode.Decompiler/IL/Transforms/RemoveUnconstrainedGenericReferenceTypeCheck.cs @@ -0,0 +1,145 @@ +// Copyright (c) 2025 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. + +#nullable enable + +using System.Collections.Generic; +using System.Linq; + +namespace ICSharpCode.Decompiler.IL.Transforms +{ + /// + /// [stloc address(...)] + /// stloc temp(default.value ``0) + /// if (comp.o(ldloc temp == ldnull)) Block IL_002a { + /// stloc temp(ldobj ``0(ldloc address)) + /// stloc address(ldloca temp) + /// } + /// stloc V_i(expr_i) + /// ...(constrained[``0].callvirt Method(ldobj.if.ref ``0(ldloc address), ldloc V_i ...))... + /// + ///=> + /// + /// [stloc address(...)] + /// stloc address(ldobj.if.ref(ldloc address)) + /// stloc V_i(expr_i) + /// ...(constrained[``0].callvirt Method(ldobj.if.ref ``0(ldloc address), ldloc V_i ...))... + /// + /// Then ldobj.if.ref in the call is redundant because any object reference was already loaded into an immutable temporary. + /// So we can removed and inlining of the arguments (V_i) becomes possible. + /// + /// Finally the newly created ldobj.if.ref is inlined into the place where the old ldobj.if.ref was. + /// + /// => + /// + /// [stloc address(...)] + /// ...(constrained[``0].callvirt Method(ldobj.if.ref ``0(ldloc address), expr_i ...))... + /// + /// + class RemoveUnconstrainedGenericReferenceTypeCheck : IStatementTransform + { + void IStatementTransform.Run(Block block, int pos, StatementTransformContext context) + { + int startPos = pos; + // stloc temp(default.value ``0) + if (!block.Instructions[pos].MatchStLoc(out var temp, out var defaultValue)) + return; + if (!defaultValue.MatchDefaultValue(out var type)) + return; + if (temp.StoreCount != 2 || temp.LoadCount != 1 || temp.AddressCount != 1) + return; + pos++; + // if (comp.o(ldloc temp == ldnull)) Block IL_002a { + // stloc temp(ldobj ``0(ldloc address)) + // stloc address(ldloca temp) + // } + if (block.Instructions.ElementAtOrDefault(pos) is not IfInstruction ifInst) + return; + if (!ifInst.Condition.MatchCompEqualsNull(out var tempLoadForNullCheck)) + return; + if (!tempLoadForNullCheck.MatchLdLoc(temp)) + return; + if (ifInst.TrueInst is not Block dereferenceBlock) + return; + if (ifInst.FalseInst is not Nop) + return; + if (dereferenceBlock.Instructions is not [StLoc tempStore, StLoc addressReassign]) + return; + if (tempStore.Variable != temp) + return; + if (!tempStore.Value.MatchLdObj(out var addressLoadForLdObj, type)) + return; + if (!addressLoadForLdObj.MatchLdLoc(addressReassign.Variable)) + return; + if (!addressReassign.Value.MatchLdLoca(temp)) + return; + var address = addressReassign.Variable; + if (address.StoreCount != 2 || address.LoadCount != 2 || address.AddressCount != 0) + return; + pos++; + // pos now is the first store to V_i + // ...(constrained[``0].callvirt Method(ldobj.if.ref ``0(ldloc address), ldloc V_i ...))... + var callTarget = address.LoadInstructions.Single(l => addressLoadForLdObj != l); + if (callTarget.Parent is not LdObjIfRef { Parent: CallVirt call } ldobjIfRef) + return; + if (call.Arguments.Count == 0 || call.Arguments[0] != ldobjIfRef || !type.Equals(call.ConstrainedTo)) + return; + ILInstruction containingStmt = call; + while (containingStmt.Parent != block) + { + if (containingStmt.Parent == null) + return; + containingStmt = containingStmt.Parent; + } + if (containingStmt.ChildIndex < pos) + return; + // check if we can inline all temporaries used in the call: + int temporaryInitIndex = containingStmt.ChildIndex - 1; + List<(ILInstruction, ILInstruction)> replacements = new(); + for (int argIndex = call.Arguments.Count - 1; argIndex > 0 && temporaryInitIndex >= pos; argIndex--) + { + var argument = call.Arguments[argIndex]; + switch (argument) + { + case LdLoc load: + if (block.Instructions[temporaryInitIndex].MatchStLoc(load.Variable, out var expr) && ILInlining.VariableCanBeUsedForInlining(load.Variable)) + { + if (!ILInlining.CanMoveIntoCallVirt(expr, containingStmt, call, argument)) + { + return; + } + replacements.Add((argument, expr)); + temporaryInitIndex--; + } + break; + } + } + // all stores to V_i processed? + if (temporaryInitIndex != pos - 1) + { + return; + } + context.Step("RemoveUnconstrainedGenericReferenceTypeCheck", block.Instructions[startPos]); + foreach (var (argument, expr) in replacements) + { + argument.ReplaceWith(expr); + } + block.Instructions.RemoveRange(startPos, containingStmt.ChildIndex - startPos); + } + } +} \ No newline at end of file diff --git a/ICSharpCode.Decompiler/IL/Transforms/SplitVariables.cs b/ICSharpCode.Decompiler/IL/Transforms/SplitVariables.cs index 4f3bbd698..4699f9f57 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/SplitVariables.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/SplitVariables.cs @@ -107,6 +107,9 @@ namespace ICSharpCode.Decompiler.IL.Transforms case LdObj _: case StObj stobj when stobj.Target == addressLoadingInstruction: return AddressUse.Immediate; + case LdObjIfRef: + // This is either an immediate use, or we need to check how the parent uses the address. + return DetermineAddressUse(addressLoadingInstruction.Parent, targetVar); case LdFlda ldflda: return DetermineAddressUse(ldflda, targetVar); case Await await: diff --git a/ICSharpCode.Decompiler/IL/Transforms/TransformCollectionAndObjectInitializers.cs b/ICSharpCode.Decompiler/IL/Transforms/TransformCollectionAndObjectInitializers.cs index c632e22a2..ea32c3972 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/TransformCollectionAndObjectInitializers.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/TransformCollectionAndObjectInitializers.cs @@ -318,6 +318,10 @@ namespace ICSharpCode.Decompiler.IL.Transforms if (resolveContext != null && !IsMethodApplicable(method, call.Arguments, rootType, resolveContext, settings)) goto default; inst = call.Arguments[0]; + if (inst is LdObjIfRef ldObjIfRef) + { + inst = ldObjIfRef.Target; + } if (method.IsAccessor) { if (method.AccessorOwner is IProperty property && @@ -371,6 +375,22 @@ namespace ICSharpCode.Decompiler.IL.Transforms } goto default; } + case LdObjIfRef ldobj: + { + if (ldobj.Target is LdFlda ldflda && (kind != AccessPathKind.Setter || !ldflda.Field.IsReadOnly)) + { + path.Insert(0, new AccessPathElement(ldobj.OpCode, ldflda.Field)); + inst = ldflda.Target; + break; + } + if (ldobj.Target is LdLoca ldloca) + { + target = ldloca.Variable; + inst = null; + break; + } + goto default; + } case StObj stobj: { if (stobj.Target is LdFlda ldflda) diff --git a/ICSharpCode.Decompiler/IL/Transforms/TransformExpressionTrees.cs b/ICSharpCode.Decompiler/IL/Transforms/TransformExpressionTrees.cs index 2f4121b2a..b60c1e86a 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/TransformExpressionTrees.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/TransformExpressionTrees.cs @@ -643,7 +643,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms ILInstruction PrepareCallTarget(IType expectedType, ILInstruction target, IType targetType) { ILInstruction result; - switch (CallInstruction.ExpectedTypeForThisPointer(expectedType)) + switch (CallInstruction.ExpectedTypeForThisPointer(expectedType, null)) { case StackType.Ref: if (target.ResultType == StackType.Ref) diff --git a/ICSharpCode.Decompiler/IL/Transforms/UsingTransform.cs b/ICSharpCode.Decompiler/IL/Transforms/UsingTransform.cs index 020166ac0..26f8cbda2 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/UsingTransform.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/UsingTransform.cs @@ -316,7 +316,10 @@ namespace ICSharpCode.Decompiler.IL.Transforms // the null check of reference types might have been transformed into "objVar?.Dispose();" if (!(rewrap.Argument is CallVirt cv)) return false; - if (!(cv.Arguments.FirstOrDefault() is NullableUnwrap unwrap)) + target = cv.Arguments.FirstOrDefault(); + if (target is LdObjIfRef ldObjIfRef) + target = ldObjIfRef.Target; + if (!(target is NullableUnwrap unwrap)) return false; numObjVarLoadsInCheck = 1; disposeCall = cv; @@ -342,6 +345,8 @@ namespace ICSharpCode.Decompiler.IL.Transforms target = cv.Arguments.FirstOrDefault(); if (target == null) return false; + if (target is LdObjIfRef ldObjIfRef) + target = ldObjIfRef.Target; if (target.MatchBox(out var newTarget, out var type) && type.Equals(objVar.Type)) target = newTarget; else if (isInlinedIsInst && target.MatchIsInst(out newTarget, out type) && type.IsKnownType(disposeTypeCode)) diff --git a/ILSpy/Languages/ILAstLanguage.cs b/ILSpy/Languages/ILAstLanguage.cs index 46ccd0fe4..948a79511 100644 --- a/ILSpy/Languages/ILAstLanguage.cs +++ b/ILSpy/Languages/ILAstLanguage.cs @@ -109,6 +109,7 @@ namespace ICSharpCode.ILSpy var typeSystem = new DecompilerTypeSystem(module, assemblyResolver); var reader = new ILReader(typeSystem.MainModule); reader.UseDebugSymbols = options.DecompilerSettings.UseDebugSymbols; + reader.UseRefLocalsForAccurateOrderOfEvaluation = options.DecompilerSettings.UseRefLocalsForAccurateOrderOfEvaluation; var methodBody = module.GetMethodBody(methodDef.RelativeVirtualAddress); ILFunction il = reader.ReadIL((SRM.MethodDefinitionHandle)method.MetadataToken, methodBody, kind: ILFunctionKind.TopLevelFunction, cancellationToken: options.CancellationToken); var decompiler = new CSharpDecompiler(typeSystem, options.DecompilerSettings) { CancellationToken = options.CancellationToken };