From 9f77f8a91977171c08fa367ffb539dd81c658381 Mon Sep 17 00:00:00 2001
From: Siegfried Pammer <siegfriedpammer@gmail.com>
Date: Wed, 19 Mar 2025 19:16:23 +0100
Subject: [PATCH] Prevent inlining of call arguments when doing so would change
 order of evaluation with regards to the implicit ldobj performed by a
 constrained.callvirt.

---
 .../CorrectnessTestRunner.cs                  |   8 +-
 .../ICSharpCode.Decompiler.Tests.csproj       |   1 +
 .../NonGenericConstrainedCallVirt.il          | 143 ++++++++++++++++++
 .../TestCases/Pretty/Generics.cs              |   6 +
 ICSharpCode.Decompiler/CSharp/CallBuilder.cs  |  11 +-
 .../CSharp/ExpressionBuilder.cs               |  18 +--
 ICSharpCode.Decompiler/IL/ILReader.cs         |  27 +++-
 ICSharpCode.Decompiler/IL/Instructions.cs     | 138 +++++++++++++++++
 ICSharpCode.Decompiler/IL/Instructions.tt     |  15 +-
 .../IL/Instructions/CallInstruction.cs        |  13 +-
 .../IL/Transforms/ExpressionTransforms.cs     |  12 ++
 .../IL/Transforms/ILInlining.cs               |   4 +
 .../IL/Transforms/NamedArgumentTransform.cs   |   2 +-
 .../IL/Transforms/NullPropagationTransform.cs |   6 +-
 .../RemoveInfeasiblePathTransform.cs          |   2 -
 .../IL/Transforms/SplitVariables.cs           |   3 +
 ...ransformCollectionAndObjectInitializers.cs |  20 +++
 .../IL/Transforms/TransformExpressionTrees.cs |   2 +-
 .../IL/Transforms/UsingTransform.cs           |   7 +-
 19 files changed, 407 insertions(+), 31 deletions(-)
 create mode 100644 ICSharpCode.Decompiler.Tests/TestCases/Correctness/NonGenericConstrainedCallVirt.il

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..b40bf5b35 100644
--- a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj
+++ b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj
@@ -95,6 +95,7 @@
     <None Include="TestCases\ILPretty\Issue2260SwitchString.il" />
     <None Include="TestCases\ILPretty\Issue3442.il" />
     <None Include="TestCases\ILPretty\MonoFixed.il" />
+    <None Include="TestCases\Correctness\NonGenericConstrainedCallVirt.il" />
     <None Include="TestCases\ILPretty\UnknownTypes.cs" />
     <None Include="TestCases\ILPretty\UnknownTypes.il" />
     <None Include="TestCases\ILPretty\EvalOrder.cs" />
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 '<Name>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::'<Name>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::'<Name>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..82a81c43e 100644
--- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Generics.cs
+++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/Generics.cs
@@ -296,5 +296,11 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
 		{
 			return default(T).ToString();
 		}
+
+		public static void ConstrainedCall<T>(T x, ref T y) where T : IDisposable
+		{
+			x.Dispose();
+			y.Dispose();
+		}
 	}
 }
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/IL/ILReader.cs b/ICSharpCode.Decompiler/IL/ILReader.cs
index 226218e12..254eb068e 100644
--- a/ICSharpCode.Decompiler/IL/ILReader.cs
+++ b/ICSharpCode.Decompiler/IL/ILReader.cs
@@ -1766,15 +1766,36 @@ 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
+					&& 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,
 		/// <summary>Indirect load (ref/pointer dereference).</summary>
 		LdObj,
+		/// <summary>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.<para>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).</para></summary>
+		LdObjIfRef,
 		/// <summary>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())</summary>
 		StObj,
@@ -4085,6 +4087,116 @@ namespace ICSharpCode.Decompiler.IL
 	}
 }
 namespace ICSharpCode.Decompiler.IL
+{
+	/// <summary>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.<para>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).</para></summary>
+	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;
+		/// <summary>Returns the type operand.</summary>
+		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<T>(ILVisitor<T> visitor)
+		{
+			return visitor.VisitLdObjIfRef(this);
+		}
+		public override T AcceptVisitor<C, T>(ILVisitor<C, T> 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
 {
 	/// <summary>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())</summary>
@@ -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.<para>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).</para>",
+			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("/// <summary>Returns the method operand.</summary>" + 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
 
 		/// <summary>
 		/// 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.
 		/// </summary>
-		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/Transforms/ExpressionTransforms.cs b/ICSharpCode.Decompiler/IL/Transforms/ExpressionTransforms.cs
index 1b8d09264..744008c7e 100644
--- a/ICSharpCode.Decompiler/IL/Transforms/ExpressionTransforms.cs
+++ b/ICSharpCode.Decompiler/IL/Transforms/ExpressionTransforms.cs
@@ -475,6 +475,18 @@ 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;
+			}
+		}
+
 		protected internal override void VisitStObj(StObj inst)
 		{
 			base.VisitStObj(inst);
diff --git a/ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs b/ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs
index 3e2c743c8..88b2b95c4 100644
--- a/ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs
+++ b/ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs
@@ -394,6 +394,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;
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/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))