diff --git a/.github/workflows/build-ilspy.yml b/.github/workflows/build-ilspy.yml
index 444d22b3c..10e5d8e9d 100644
--- a/.github/workflows/build-ilspy.yml
+++ b/.github/workflows/build-ilspy.yml
@@ -35,7 +35,7 @@ jobs:
       uses: microsoft/setup-msbuild@v2
 
     - name: Install dotnet-format
-      run: dotnet tool install -g dotnet-format --version "6.2.315104" --add-source https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet6/nuget/v3/index.json
+      run: dotnet tool install -g dotnet-format --version "8.0.453106" --add-source https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet8/nuget/v3/index.json
 
     - name: Get Version
       id: version
diff --git a/BuildTools/format.bat b/BuildTools/format.bat
new file mode 100644
index 000000000..be49dc68c
--- /dev/null
+++ b/BuildTools/format.bat
@@ -0,0 +1,3 @@
+@rem This file can be used to trigger the commit hook's formatting,
+@rem modifying the local formatting even if not committing all changes.
+"%ProgramFiles%\Git\usr\bin\bash.exe" BuildTools\pre-commit --format
\ No newline at end of file
diff --git a/BuildTools/pre-commit b/BuildTools/pre-commit
index a55e3bdc4..eee675c87 100644
--- a/BuildTools/pre-commit
+++ b/BuildTools/pre-commit
@@ -5,16 +5,21 @@
 
 set -eu
 
-DOTNET_FORMAT_VERSION=6.2.315104
+DOTNET_FORMAT_VERSION=8.0.453106
 DOTNET_PATH="$LOCALAPPDATA/ICSharpCode/ILSpy/dotnet-format-$DOTNET_FORMAT_VERSION"
 if [ ! -d "$DOTNET_PATH" ]; then
- dotnet tool install --tool-path "$DOTNET_PATH" dotnet-format --version "$DOTNET_FORMAT_VERSION" --add-source "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet6/nuget/v3/index.json"
+ echo "Downloading dotnet-format $DOTNET_FORMAT_VERSION..."
+ dotnet tool install --tool-path "$DOTNET_PATH" dotnet-format --version "$DOTNET_FORMAT_VERSION" --add-source "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet8/nuget/v3/index.json"
 fi
 
 "$DOTNET_PATH/dotnet-format.exe" --version
-if git diff --quiet --ignore-submodules; then
+if [ "${1:-}" = "--format" ]; then
+	# called via format.bat
+	"$DOTNET_PATH/dotnet-format.exe" whitespace --no-restore --verbosity detailed ILSpy.sln
+elif git diff --quiet --ignore-submodules; then
 	"$DOTNET_PATH/dotnet-format.exe" whitespace --no-restore --verbosity detailed ILSpy.sln
 	git add -u -- \*\*.cs
 else
+	echo Partial commit: only verifying formatting
 	exec "$DOTNET_PATH/dotnet-format.exe" whitespace --verify-no-changes --no-restore --verbosity detailed ILSpy.sln
 fi
diff --git a/Directory.Packages.props b/Directory.Packages.props
index f70e32fbc..a99f48fcc 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -16,8 +16,8 @@
     <PackageVersion Include="K4os.Compression.LZ4" Version="1.3.8" />
     <PackageVersion Include="McMaster.Extensions.CommandLineUtils" Version="4.1.1" />
     <PackageVersion Include="McMaster.Extensions.Hosting.CommandLine" Version="4.1.1" />
-    <PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.9.2" />
-    <PackageVersion Include="Microsoft.CodeAnalysis.VisualBasic" Version="4.9.2" />
+    <PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.10.0" />
+    <PackageVersion Include="Microsoft.CodeAnalysis.VisualBasic" Version="4.10.0" />
     <PackageVersion Include="Microsoft.DiaSymReader.Converter.Xml" Version="1.1.0-beta2-22171-02" />
     <PackageVersion Include="Microsoft.DiaSymReader" Version="1.4.0" />
     <PackageVersion Include="Microsoft.DiaSymReader.Native" Version="17.0.0-beta1.21524.1" />
diff --git a/ICSharpCode.Decompiler.Tests/Helpers/Tester.cs b/ICSharpCode.Decompiler.Tests/Helpers/Tester.cs
index 92eb339f6..bdea4570f 100644
--- a/ICSharpCode.Decompiler.Tests/Helpers/Tester.cs
+++ b/ICSharpCode.Decompiler.Tests/Helpers/Tester.cs
@@ -528,7 +528,7 @@ namespace ICSharpCode.Decompiler.Tests.Helpers
 			{
 				CompilerResults results = new CompilerResults();
 				results.PathToAssembly = outputFileName;
-				string testBasePath = RoundtripAssembly.TestDir;
+				string testBasePath = Roundtrip.RoundtripAssembly.TestDir;
 				if (!Directory.Exists(testBasePath))
 				{
 					Assert.Ignore($"Compilation with mcs ignored: test directory '{testBasePath}' needs to be checked out separately." + Environment.NewLine +
@@ -596,7 +596,7 @@ namespace ICSharpCode.Decompiler.Tests.Helpers
 					CompilerOptions.UseRoslyn1_3_2 => CSharp.LanguageVersion.CSharp6,
 					CompilerOptions.UseRoslyn2_10_0 => CSharp.LanguageVersion.CSharp7_3,
 					CompilerOptions.UseRoslyn3_11_0 => CSharp.LanguageVersion.CSharp9_0,
-					_ => cscOptions.HasFlag(CompilerOptions.Preview) ? CSharp.LanguageVersion.Latest : CSharp.LanguageVersion.CSharp11_0,
+					_ => cscOptions.HasFlag(CompilerOptions.Preview) ? CSharp.LanguageVersion.Latest : CSharp.LanguageVersion.CSharp12_0,
 				};
 				DecompilerSettings settings = new(langVersion) {
 					// Never use file-scoped namespaces
diff --git a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj
index 7970f6265..697ca18d9 100644
--- a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj
+++ b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj
@@ -17,6 +17,7 @@
     <AllowUnsafeBlocks>True</AllowUnsafeBlocks>
 
     <NoWarn>1701;1702;1705,67,169,1058,728,1720,649,168,251,660,661,675;1998;162;8632;626;8618;8714;8602;8981</NoWarn>
+    <DefineConstants>ROSLYN;NET60;CS60;CS70;CS71;CS72;CS73;CS80;CS90;CS100;CS110;CS120</DefineConstants>
 
     <GenerateAssemblyVersionAttribute>False</GenerateAssemblyVersionAttribute>
     <GenerateAssemblyFileVersionAttribute>False</GenerateAssemblyFileVersionAttribute>
@@ -40,11 +41,11 @@
   </PropertyGroup>
 
   <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
-    <DefineConstants>TRACE;DEBUG;ROSLYN;NET60;CS60;CS70;CS71;CS72;CS73;CS80;CS90;CS100</DefineConstants>
+    <DefineConstants>TRACE;DEBUG;$(DefineConstants)</DefineConstants>
   </PropertyGroup>
 
   <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
-    <DefineConstants>TRACE;ROSLYN;NET60;CS60;CS70;CS71;CS72;CS73;CS80;CS90;CS100</DefineConstants>
+    <DefineConstants>TRACE;$(DefineConstants)</DefineConstants>
   </PropertyGroup>
 
   <ItemGroup>
@@ -152,7 +153,7 @@
     <None Include="TestCases\Pretty\Records.cs" />
     <Compile Include="TestCases\VBPretty\Issue2192.cs" />
     <Compile Include="Util\FileUtilityTests.cs" />
-    <None Include="TestCases\Pretty\FunctionPointers.cs" />
+    <Compile Include="TestCases\Pretty\FunctionPointers.cs" />
     <None Include="TestCases\Pretty\CS9_ExtensionGetEnumerator.cs" />
     <None Include="TestCases\Pretty\UsingVariables.cs" />
     <None Include="TestCases\Pretty\AsyncForeach.cs" />
diff --git a/ICSharpCode.Decompiler.Tests/RoundtripAssembly.cs b/ICSharpCode.Decompiler.Tests/RoundtripAssembly.cs
index 3043c9246..de1639cf0 100644
--- a/ICSharpCode.Decompiler.Tests/RoundtripAssembly.cs
+++ b/ICSharpCode.Decompiler.Tests/RoundtripAssembly.cs
@@ -29,11 +29,12 @@ using CliWrap;
 using ICSharpCode.Decompiler.CSharp;
 using ICSharpCode.Decompiler.CSharp.ProjectDecompiler;
 using ICSharpCode.Decompiler.Metadata;
+using ICSharpCode.Decompiler.Tests;
 using ICSharpCode.Decompiler.Tests.Helpers;
 
 using NUnit.Framework;
 
-namespace ICSharpCode.Decompiler.Tests
+namespace ICSharpCode.Decompiler.Roundtrip
 {
 	[TestFixture, Parallelizable(ParallelScope.All)]
 	public class RoundtripAssembly
diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Correctness/OverloadResolution.cs b/ICSharpCode.Decompiler.Tests/TestCases/Correctness/OverloadResolution.cs
index e206872a5..50925bddd 100644
--- a/ICSharpCode.Decompiler.Tests/TestCases/Correctness/OverloadResolution.cs
+++ b/ICSharpCode.Decompiler.Tests/TestCases/Correctness/OverloadResolution.cs
@@ -36,6 +36,7 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Correctness
 			Issue1747();
 			CallAmbiguousOutParam();
 			CallWithInParam();
+			CallWithRefReadOnlyParam();
 #if CS90
 			NativeIntTests(new IntPtr(1), 2);
 #endif
@@ -277,6 +278,73 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Correctness
 		}
 		#endregion
 
+		#region Ref readonly Parameter
+
+		static void CallWithRefReadOnlyParam()
+		{
+#if CS120
+#pragma warning disable CS9193
+			Console.WriteLine("OverloadSetWithRefReadOnlyParam:");
+			OverloadSetWithRefReadOnlyParam(1);
+			OverloadSetWithRefReadOnlyParam(2L);
+			int i = 3;
+			OverloadSetWithRefReadOnlyParam(in i);
+			OverloadSetWithRefReadOnlyParam((long)4);
+
+			Console.WriteLine("OverloadSetWithRefReadOnlyParam2:");
+			OverloadSetWithRefReadOnlyParam2(1);
+			OverloadSetWithRefReadOnlyParam2((object)1);
+
+			Console.WriteLine("OverloadSetWithRefReadOnlyParam3:");
+			OverloadSetWithRefReadOnlyParam3(1);
+			OverloadSetWithRefReadOnlyParam3<int>(2);
+			OverloadSetWithRefReadOnlyParam3((object)3);
+
+			Console.WriteLine("RefReadOnlyVsRegularParam:");
+			RefReadOnlyVsRegularParam(1);
+			i = 2;
+			RefReadOnlyVsRegularParam(in i);
+#endif
+		}
+
+#if CS120
+		static void OverloadSetWithRefReadOnlyParam(ref readonly int i)
+		{
+			Console.WriteLine("ref readonly int " + i);
+		}
+		static void OverloadSetWithRefReadOnlyParam(long l)
+		{
+			Console.WriteLine("long " + l);
+		}
+		static void OverloadSetWithRefReadOnlyParam2(ref readonly long i)
+		{
+			Console.WriteLine("ref readonly long " + i);
+		}
+		static void OverloadSetWithRefReadOnlyParam2(object o)
+		{
+			Console.WriteLine("object " + o);
+		}
+		static void OverloadSetWithRefReadOnlyParam3(ref readonly int i)
+		{
+			Console.WriteLine("ref readonly int " + i);
+		}
+		static void OverloadSetWithRefReadOnlyParam3<T>(T a)
+		{
+			Console.WriteLine("T " + a);
+		}
+		static void RefReadOnlyVsRegularParam(ref readonly int i)
+		{
+			Console.WriteLine("ref readonly int " + i);
+		}
+		static void RefReadOnlyVsRegularParam(int i)
+		{
+			Console.WriteLine("int " + i);
+		}
+
+#endif
+
+		#endregion
+
 		#region In Parameter
 		static void CallWithInParam()
 		{
diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/CompoundAssignmentTest.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/CompoundAssignmentTest.cs
index 1cc74865c..34275f252 100644
--- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/CompoundAssignmentTest.cs
+++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/CompoundAssignmentTest.cs
@@ -4719,12 +4719,14 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
 		}
 #endif
 
-		private static void StringPropertyCompoundAssign()
+		private static void StringPropertyCompoundAssign(char c)
 		{
 			StaticStringProperty += "a";
 			StaticStringProperty += 1;
+			StaticStringProperty += c;
 			new CustomClass().StringProp += "a";
 			new CustomClass().StringProp += 1;
+			new CustomClass().StringProp += c;
 		}
 
 		public uint PreIncrementIndexer(string name)
diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/DynamicTests.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/DynamicTests.cs
index 01edd50d2..7b90338f1 100644
--- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/DynamicTests.cs
+++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/DynamicTests.cs
@@ -1,4 +1,7 @@
 using System;
+#if CS120
+using System.Collections.Generic;
+#endif
 
 namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
 {
@@ -74,6 +77,12 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
 		}
 #endif
 
+#if CS120
+		private static void CallWithRefReadonly(ref readonly Dictionary<object, dynamic> d)
+		{
+		}
+#endif
+
 		private static void CallWithRef(ref dynamic d)
 		{
 		}
diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/FunctionPointers.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/FunctionPointers.cs
index 9423c4254..aa49e2b79 100644
--- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/FunctionPointers.cs
+++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/FunctionPointers.cs
@@ -95,14 +95,17 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
 		public unsafe delegate*<object, ref readonly dynamic> F12;
 		public unsafe delegate*<in dynamic, object> F13;
 		public unsafe delegate*<out dynamic, object> F14;
-		public unsafe D<delegate*<dynamic>[], dynamic> F15;
-		public unsafe delegate*<A<object>.B<dynamic>> F16;
+#if CS120
+		public unsafe delegate*<ref readonly dynamic, object> F15;
+#endif
+		public unsafe D<delegate*<dynamic>[], dynamic> F16;
+		public unsafe delegate*<A<object>.B<dynamic>> F17;
 	}
 
 	internal class FunctionPointersWithNativeIntegerTypes
 	{
 		public unsafe delegate*<nint, nint, nint> F1;
-		#if !(CS110 && NET70)
+#if !(CS110 && NET70)
 		public unsafe delegate*<IntPtr, IntPtr, nint> F2;
 		public unsafe delegate*<nint, IntPtr, IntPtr> F3;
 		public unsafe delegate*<IntPtr, nint, IntPtr> F4;
@@ -111,7 +114,7 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
 		public unsafe delegate*<delegate*<IntPtr, IntPtr, nint>, IntPtr> F7;
 		public unsafe delegate*<IntPtr, delegate*<IntPtr, nint, IntPtr>> F8;
 		public unsafe delegate*<IntPtr, delegate*<IntPtr, IntPtr, IntPtr>> F9;
-		#endif
+#endif
 	}
 
 	internal class FunctionPointersWithRefParams
diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/OptionalArguments.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/OptionalArguments.cs
index f68b896d0..82cda462b 100644
--- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/OptionalArguments.cs
+++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/OptionalArguments.cs
@@ -24,6 +24,8 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
 {
 	internal class OptionalArguments : List<int>
 	{
+		public delegate int D(int p = 10);
+
 		public enum MyEnum
 		{
 			A,
@@ -283,5 +285,18 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
 		{
 		}
 #endif
+
+#if CS120
+		public static D LambdaWithOptionalParameter()
+		{
+			return (int x = 10) => x;
+		}
+
+		public static void Use(D d)
+		{
+			d();
+			d(42);
+		}
+#endif
 	}
 }
diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/RefLocalsAndReturns.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/RefLocalsAndReturns.cs
index f4c0f1187..7a65ff10a 100644
--- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/RefLocalsAndReturns.cs
+++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/RefLocalsAndReturns.cs
@@ -320,5 +320,24 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
 			LastOrDefault() = 10000;
 			Console.WriteLine(ElementAtOrDefault(-5));
 		}
+
+#if CS120
+		public ref readonly int M(in int x)
+		{
+			return ref x;
+		}
+
+		public ref readonly int M2(ref readonly int x)
+		{
+			return ref x;
+		}
+
+		public void Test()
+		{
+			int x = 32;
+			M(in x);
+			M2(in x);
+		}
+#endif
 	}
 }
diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/StringInterpolation.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/StringInterpolation.cs
index fc667d275..0dbbc9064 100644
--- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/StringInterpolation.cs
+++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/StringInterpolation.cs
@@ -117,5 +117,30 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
 		public void RequiresCast(IFormattable value)
 		{
 		}
+
+		public string ConcatStringCharSC(string s, char c)
+		{
+			return s + c;
+		}
+
+		public string ConcatStringCharCS(string s, char c)
+		{
+			return c + s;
+		}
+
+		public string ConcatStringCharSCS(string s, char c)
+		{
+			return s + c + s;
+		}
+
+		public string ConcatStringCharCSS(string s, char c)
+		{
+			return c + s + s;
+		}
+
+		public string ConcatStringCharCSSC(string s, char c)
+		{
+			return c + s + s + c;
+		}
 	}
 }
diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/ValueTypes.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/ValueTypes.cs
index c40e55f4c..87f54d52f 100644
--- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/ValueTypes.cs
+++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/ValueTypes.cs
@@ -291,5 +291,24 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty
 		public static void Test(object x)
 		{
 		}
+
+#if CS120
+		public static void AcceptIn(in S o)
+		{
+		}
+
+		public static void AcceptRefReadOnly(ref readonly S o)
+		{
+		}
+
+		private static void Use(in S param)
+		{
+			AcceptIn(new S(5));
+			S o = new S(10);
+			AcceptRefReadOnly(in o);
+			AcceptIn(in param);
+			AcceptRefReadOnly(in param);
+		}
+#endif
 	}
 }
\ No newline at end of file
diff --git a/ICSharpCode.Decompiler.Tests/TypeSystem/TypeSystemLoaderTests.cs b/ICSharpCode.Decompiler.Tests/TypeSystem/TypeSystemLoaderTests.cs
index b27a1ecc5..2059f6c42 100644
--- a/ICSharpCode.Decompiler.Tests/TypeSystem/TypeSystemLoaderTests.cs
+++ b/ICSharpCode.Decompiler.Tests/TypeSystem/TypeSystemLoaderTests.cs
@@ -1390,18 +1390,18 @@ namespace ICSharpCode.Decompiler.Tests.TypeSystem
 		{
 			ITypeDefinition impl = GetTypeDefinition(typeof(ExplicitGenericInterfaceImplementation));
 			IType genericInterfaceOfString = compilation.FindType(typeof(IGenericInterface<string>));
-			IMethod implMethod1 = impl.Methods.Single(m => !m.IsConstructor && !m.Parameters[1].IsRef);
-			IMethod implMethod2 = impl.Methods.Single(m => !m.IsConstructor && m.Parameters[1].IsRef);
+			IMethod implMethod1 = impl.Methods.Single(m => !m.IsConstructor && m.Parameters[1].ReferenceKind == ReferenceKind.None);
+			IMethod implMethod2 = impl.Methods.Single(m => !m.IsConstructor && m.Parameters[1].ReferenceKind == ReferenceKind.Ref);
 			Assert.That(implMethod1.IsExplicitInterfaceImplementation);
 			Assert.That(implMethod2.IsExplicitInterfaceImplementation);
 
 			IMethod interfaceMethod1 = (IMethod)implMethod1.ExplicitlyImplementedInterfaceMembers.Single();
 			Assert.That(interfaceMethod1.DeclaringType, Is.EqualTo(genericInterfaceOfString));
-			Assert.That(!interfaceMethod1.Parameters[1].IsRef);
+			Assert.That(interfaceMethod1.Parameters[1].ReferenceKind == ReferenceKind.None);
 
 			IMethod interfaceMethod2 = (IMethod)implMethod2.ExplicitlyImplementedInterfaceMembers.Single();
 			Assert.That(interfaceMethod2.DeclaringType, Is.EqualTo(genericInterfaceOfString));
-			Assert.That(interfaceMethod2.Parameters[1].IsRef);
+			Assert.That(interfaceMethod2.Parameters[1].ReferenceKind == ReferenceKind.Ref);
 		}
 
 		[Test]
diff --git a/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs b/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs
index 9d44a14a3..b4597e4eb 100644
--- a/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs
+++ b/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs
@@ -91,7 +91,6 @@ namespace ICSharpCode.Decompiler.CSharp
 				new InlineReturnTransform(), // must run before DetectPinnedRegions
 				new RemoveInfeasiblePathTransform(),
 				new DetectPinnedRegions(), // must run after inlining but before non-critical control flow transforms
-				new ParameterNullCheckTransform(), // must run after inlining but before yield/async
 				new YieldReturnDecompiler(), // must run after inlining but before loop detection
 				new AsyncAwaitDecompiler(),  // must run after inlining but before loop detection
 				new DetectCatchWhenConditionBlocks(), // must run after inlining but before loop detection
@@ -185,7 +184,6 @@ namespace ICSharpCode.Decompiler.CSharp
 				new AddCheckedBlocks(),
 				new DeclareVariables(), // should run after most transforms that modify statements
 				new TransformFieldAndConstructorInitializers(), // must run after DeclareVariables
-				new DecimalConstantTransform(),
 				new PrettifyAssignments(), // must run after DeclareVariables
 				new IntroduceUsingDeclarations(),
 				new IntroduceExtensionMethods(), // must run after IntroduceUsingDeclarations
@@ -1169,7 +1167,7 @@ namespace ICSharpCode.Decompiler.CSharp
 										 Roles.Comment);
 				var forwardingCall = new InvocationExpression(new MemberReferenceExpression(new ThisReferenceExpression(), memberDecl.Name,
 					methodDecl.TypeParameters.Select(tp => new SimpleType(tp.Name))),
-					methodDecl.Parameters.Select(p => ForwardParameter(p))
+					methodDecl.Parameters.Select(ForwardParameter)
 				);
 				if (m.ReturnType.IsKnownType(KnownTypeCode.Void))
 				{
@@ -1187,12 +1185,17 @@ namespace ICSharpCode.Decompiler.CSharp
 		{
 			switch (p.ParameterModifier)
 			{
-				case ParameterModifier.Ref:
+				case ReferenceKind.None:
+					return new IdentifierExpression(p.Name);
+				case ReferenceKind.Ref:
+				case ReferenceKind.RefReadOnly:
 					return new DirectionExpression(FieldDirection.Ref, new IdentifierExpression(p.Name));
-				case ParameterModifier.Out:
+				case ReferenceKind.Out:
 					return new DirectionExpression(FieldDirection.Out, new IdentifierExpression(p.Name));
+				case ReferenceKind.In:
+					return new DirectionExpression(FieldDirection.In, new IdentifierExpression(p.Name));
 				default:
-					return new IdentifierExpression(p.Name);
+					throw new NotSupportedException();
 			}
 		}
 
diff --git a/ICSharpCode.Decompiler/CSharp/CSharpLanguageVersion.cs b/ICSharpCode.Decompiler/CSharp/CSharpLanguageVersion.cs
index 87314b0c4..019774580 100644
--- a/ICSharpCode.Decompiler/CSharp/CSharpLanguageVersion.cs
+++ b/ICSharpCode.Decompiler/CSharp/CSharpLanguageVersion.cs
@@ -34,6 +34,7 @@ namespace ICSharpCode.Decompiler.CSharp
 		CSharp9_0 = 900,
 		CSharp10_0 = 1000,
 		CSharp11_0 = 1100,
+		CSharp12_0 = 1200,
 		Preview = 1100,
 		Latest = 0x7FFFFFFF
 	}
diff --git a/ICSharpCode.Decompiler/CSharp/CallBuilder.cs b/ICSharpCode.Decompiler/CSharp/CallBuilder.cs
index d34ec1cfd..56f420e07 100644
--- a/ICSharpCode.Decompiler/CSharp/CallBuilder.cs
+++ b/ICSharpCode.Decompiler/CSharp/CallBuilder.cs
@@ -78,7 +78,7 @@ namespace ICSharpCode.Decompiler.CSharp
 				ResolveResult GetResolveResult(int index, TranslatedExpression expression)
 				{
 					var param = expectedParameters[index];
-					if (useImplicitlyTypedOut && param.IsOut && expression.Type is ByReferenceType brt)
+					if (useImplicitlyTypedOut && param.ReferenceKind == ReferenceKind.Out && expression.Type is ByReferenceType brt)
 						return new OutVarResolveResult(brt.ElementType);
 					return expression.ResolveResult;
 				}
@@ -134,8 +134,7 @@ namespace ICSharpCode.Decompiler.CSharp
 				{
 					if (!useImplicitlyTypedOut)
 						return expression;
-					if (expression.GetResolveResult() is ByReferenceResolveResult brrr
-						&& brrr.IsOut)
+					if (expression.GetResolveResult() is ByReferenceResolveResult { ReferenceKind: ReferenceKind.Out } brrr)
 					{
 						expression.AddAnnotation(UseImplicitlyTypedOutAnnotation.Instance);
 					}
@@ -224,10 +223,94 @@ namespace ICSharpCode.Decompiler.CSharp
 					valueTupleAssembly: inst.Method.DeclaringType.GetDefinition()?.ParentModule
 				)).WithILInstruction(inst);
 			}
+			if (settings.StringConcat && IsSpanBasedStringConcat(inst, out var operands))
+			{
+				return BuildStringConcat(inst.Method, operands).WithILInstruction(inst);
+			}
 			return Build(inst.OpCode, inst.Method, inst.Arguments, constrainedTo: inst.ConstrainedTo)
 				.WithILInstruction(inst);
 		}
 
+		private ExpressionWithResolveResult BuildStringConcat(IMethod method, List<(ILInstruction Instruction, KnownTypeCode TypeCode)> operands)
+		{
+			IType type = typeSystem.FindType(operands[0].TypeCode);
+			ExpressionWithResolveResult result = expressionBuilder.Translate(operands[0].Instruction, type).ConvertTo(type, expressionBuilder);
+			var rr = new MemberResolveResult(null, method);
+
+			for (int i = 1; i < operands.Count; i++)
+			{
+				type = typeSystem.FindType(operands[i].TypeCode);
+				var expr = expressionBuilder.Translate(operands[i].Instruction, type).ConvertTo(type, expressionBuilder);
+				result = new BinaryOperatorExpression(result.Expression, BinaryOperatorType.Add, expr).WithRR(rr);
+			}
+
+			return result;
+		}
+
+		static bool IsSpanBasedStringConcat(CallInstruction call, out List<(ILInstruction, KnownTypeCode)> operands)
+		{
+			operands = null;
+
+			if (!IsSpanBasedStringConcat(call.Method))
+			{
+				return false;
+			}
+
+			int? firstStringArgumentIndex = null;
+			operands = new();
+
+			foreach (var arg in call.Arguments)
+			{
+				if (arg is Call opImplicit && IsStringToReadOnlySpanCharImplicitConversion(opImplicit.Method))
+				{
+					firstStringArgumentIndex ??= arg.ChildIndex;
+					operands.Add((opImplicit.Arguments.Single(), KnownTypeCode.String));
+				}
+				else if (arg is NewObj { Arguments: [AddressOf addressOf] } newObj && ILInlining.IsReadOnlySpanCharCtor(newObj.Method))
+				{
+					operands.Add((addressOf.Value, KnownTypeCode.Char));
+				}
+				else
+				{
+					return false;
+				}
+			}
+
+			return call.Arguments.Count >= 2 && firstStringArgumentIndex <= 1;
+		}
+
+		internal static bool IsSpanBasedStringConcat(IMethod method)
+		{
+			if (method is not { Name: "Concat", IsStatic: true })
+			{
+				return false;
+			}
+			if (!method.DeclaringType.IsKnownType(KnownTypeCode.String))
+			{
+				return false;
+			}
+
+			foreach (var p in method.Parameters)
+			{
+				if (!p.Type.IsKnownType(KnownTypeCode.ReadOnlySpanOfT))
+					return false;
+				if (!p.Type.TypeArguments[0].IsKnownType(KnownTypeCode.Char))
+					return false;
+			}
+
+			return true;
+		}
+
+		internal static bool IsStringToReadOnlySpanCharImplicitConversion(IMethod method)
+		{
+			return method.IsOperator
+				&& method.Name == "op_Implicit"
+				&& method.Parameters.Count == 1
+				&& method.ReturnType.IsKnownType(KnownTypeCode.ReadOnlySpanOfT)
+				&& method.ReturnType.TypeArguments[0].IsKnownType(KnownTypeCode.Char)
+				&& method.Parameters[0].Type.IsKnownType(KnownTypeCode.String);
+		}
+
 		public ExpressionWithResolveResult Build(OpCode callOpCode, IMethod method,
 			IReadOnlyList<ILInstruction> callArguments,
 			IReadOnlyList<int> argumentToParameterMap = null,
@@ -345,69 +428,11 @@ namespace ICSharpCode.Decompiler.CSharp
 						argumentList.GetArgumentResolveResults(), isExpandedForm: argumentList.IsExpandedForm, isDelegateInvocation: true));
 			}
 
-			if (settings.StringInterpolation && IsInterpolatedStringCreation(method, argumentList) &&
-				TryGetStringInterpolationTokens(argumentList, out string format, out var tokens))
+			if (settings.StringInterpolation && IsInterpolatedStringCreation(method, argumentList))
 			{
-				var arguments = argumentList.Arguments;
-				var content = new List<InterpolatedStringContent>();
-
-				bool unpackSingleElementArray = !argumentList.IsExpandedForm && argumentList.Length == 2
-					&& argumentList.Arguments[1].Expression is ArrayCreateExpression ace
-					&& ace.Initializer?.Elements.Count == 1;
-
-				void UnpackSingleElementArray(ref TranslatedExpression argument)
-				{
-					if (!unpackSingleElementArray)
-						return;
-					var arrayCreation = (ArrayCreateExpression)argumentList.Arguments[1].Expression;
-					var arrayCreationRR = (ArrayCreateResolveResult)argumentList.Arguments[1].ResolveResult;
-					var element = arrayCreation.Initializer.Elements.First().Detach();
-					argument = new TranslatedExpression(element, arrayCreationRR.InitializerElements.First());
-				}
-
-				if (tokens.Count > 0)
-				{
-					foreach (var (kind, index, alignment, text) in tokens)
-					{
-						TranslatedExpression argument;
-						switch (kind)
-						{
-							case TokenKind.String:
-								content.Add(new InterpolatedStringText(text));
-								break;
-							case TokenKind.Argument:
-								argument = arguments[index + 1];
-								UnpackSingleElementArray(ref argument);
-								content.Add(new Interpolation(argument));
-								break;
-							case TokenKind.ArgumentWithFormat:
-								argument = arguments[index + 1];
-								UnpackSingleElementArray(ref argument);
-								content.Add(new Interpolation(argument, suffix: text));
-								break;
-							case TokenKind.ArgumentWithAlignment:
-								argument = arguments[index + 1];
-								UnpackSingleElementArray(ref argument);
-								content.Add(new Interpolation(argument, alignment));
-								break;
-							case TokenKind.ArgumentWithAlignmentAndFormat:
-								argument = arguments[index + 1];
-								UnpackSingleElementArray(ref argument);
-								content.Add(new Interpolation(argument, alignment, text));
-								break;
-						}
-					}
-					var formattableStringType = expressionBuilder.compilation.FindType(KnownTypeCode.FormattableString);
-					var isrr = new InterpolatedStringResolveResult(expressionBuilder.compilation.FindType(KnownTypeCode.String),
-						format, argumentList.GetArgumentResolveResults(1).ToArray());
-					var expr = new InterpolatedStringExpression();
-					expr.Content.AddRange(content);
-					if (method.Name == "Format")
-						return expr.WithRR(isrr);
-					return new CastExpression(expressionBuilder.ConvertType(formattableStringType),
-						expr.WithRR(isrr))
-						.WithRR(new ConversionResolveResult(formattableStringType, isrr, Conversion.ImplicitInterpolatedStringConversion));
-				}
+				var result = HandleStringInterpolation(method, argumentList);
+				if (result.Expression != null)
+					return result;
 			}
 
 			int allowedParamCount = (method.ReturnType.IsKnownType(KnownTypeCode.Void) ? 1 : 0);
@@ -431,6 +456,20 @@ namespace ICSharpCode.Decompiler.CSharp
 				return HandleImplicitConversion(method, argumentList.Arguments[0]);
 			}
 
+			if (settings.LiftNullables && method.Name == "GetValueOrDefault"
+				&& method.DeclaringType.IsKnownType(KnownTypeCode.NullableOfT)
+				&& method.DeclaringType.TypeArguments[0].IsKnownType(KnownTypeCode.Boolean)
+				&& argumentList.Length == 0)
+			{
+				argumentList.CheckNoNamedOrOptionalArguments();
+				return new BinaryOperatorExpression(
+					target.Expression,
+					BinaryOperatorType.Equality,
+					new PrimitiveExpression(true))
+					.WithRR(new CSharpInvocationResolveResult(target.ResolveResult, method,
+						argumentList.GetArgumentResolveResults(), isExpandedForm: argumentList.IsExpandedForm));
+			}
+
 			var transform = GetRequiredTransformationsForCall(expectedTargetDetails, method, ref target,
 				ref argumentList, CallTransformation.All, out IParameterizedMember foundMethod);
 
@@ -482,6 +521,75 @@ namespace ICSharpCode.Decompiler.CSharp
 					argumentList.GetArgumentResolveResultsDirect(), isExpandedForm: argumentList.IsExpandedForm));
 		}
 
+		private ExpressionWithResolveResult HandleStringInterpolation(IMethod method, ArgumentList argumentList)
+		{
+			if (!TryGetStringInterpolationTokens(argumentList, out string format, out var tokens))
+				return default;
+
+			var arguments = argumentList.Arguments;
+			var content = new List<InterpolatedStringContent>();
+
+			bool unpackSingleElementArray = !argumentList.IsExpandedForm && argumentList.Length == 2
+				&& argumentList.Arguments[1].Expression is ArrayCreateExpression ace
+				&& ace.Initializer?.Elements.Count == 1;
+
+			void UnpackSingleElementArray(ref TranslatedExpression argument)
+			{
+				if (!unpackSingleElementArray)
+					return;
+				var arrayCreation = (ArrayCreateExpression)argumentList.Arguments[1].Expression;
+				var arrayCreationRR = (ArrayCreateResolveResult)argumentList.Arguments[1].ResolveResult;
+				var element = arrayCreation.Initializer.Elements.First().Detach();
+				argument = new TranslatedExpression(element, arrayCreationRR.InitializerElements.First());
+			}
+
+			if (tokens.Count == 0)
+			{
+				return default;
+			}
+
+			foreach (var (kind, index, alignment, text) in tokens)
+			{
+				TranslatedExpression argument;
+				switch (kind)
+				{
+					case TokenKind.String:
+						content.Add(new InterpolatedStringText(text));
+						break;
+					case TokenKind.Argument:
+						argument = arguments[index + 1];
+						UnpackSingleElementArray(ref argument);
+						content.Add(new Interpolation(argument));
+						break;
+					case TokenKind.ArgumentWithFormat:
+						argument = arguments[index + 1];
+						UnpackSingleElementArray(ref argument);
+						content.Add(new Interpolation(argument, suffix: text));
+						break;
+					case TokenKind.ArgumentWithAlignment:
+						argument = arguments[index + 1];
+						UnpackSingleElementArray(ref argument);
+						content.Add(new Interpolation(argument, alignment));
+						break;
+					case TokenKind.ArgumentWithAlignmentAndFormat:
+						argument = arguments[index + 1];
+						UnpackSingleElementArray(ref argument);
+						content.Add(new Interpolation(argument, alignment, text));
+						break;
+				}
+			}
+			var formattableStringType = expressionBuilder.compilation.FindType(KnownTypeCode.FormattableString);
+			var isrr = new InterpolatedStringResolveResult(expressionBuilder.compilation.FindType(KnownTypeCode.String),
+				format, argumentList.GetArgumentResolveResults(1).ToArray());
+			var expr = new InterpolatedStringExpression();
+			expr.Content.AddRange(content);
+			if (method.Name == "Format")
+				return expr.WithRR(isrr);
+			return new CastExpression(expressionBuilder.ConvertType(formattableStringType),
+				expr.WithRR(isrr))
+				.WithRR(new ConversionResolveResult(formattableStringType, isrr, Conversion.ImplicitInterpolatedStringConversion));
+		}
+
 		/// <summary>
 		/// Converts a call to an Add method to a collection initializer expression.
 		/// </summary>
@@ -843,7 +951,7 @@ namespace ICSharpCode.Decompiler.CSharp
 
 				if (parameter.ReferenceKind != ReferenceKind.None)
 				{
-					arg = ExpressionBuilder.ChangeDirectionExpressionTo(arg, parameter.ReferenceKind);
+					arg = ExpressionBuilder.ChangeDirectionExpressionTo(arg, parameter.ReferenceKind, callArguments[i] is AddressOf);
 				}
 
 				arguments.Add(arg);
@@ -947,7 +1055,11 @@ namespace ICSharpCode.Decompiler.CSharp
 			RequireTarget = 1,
 			RequireTypeArguments = 2,
 			NoOptionalArgumentAllowed = 4,
-			All = 7
+			/// <summary>
+			/// Add calls to AsRefReadOnly for in parameters that did not have an explicit DirectionExpression yet.
+			/// </summary>
+			EnforceExplicitIn = 8,
+			All = 0xf,
 		}
 
 		private CallTransformation GetRequiredTransformationsForCall(ExpectedTargetDetails expectedTargetDetails, IMethod method,
@@ -1098,6 +1210,11 @@ namespace ICSharpCode.Decompiler.CSharp
 							requireTypeArguments = true;
 							typeArguments = method.TypeArguments.ToArray();
 						}
+						else if ((allowedTransforms & CallTransformation.EnforceExplicitIn) != 0)
+						{
+							EnforceExplicitIn(argumentList.Arguments, argumentList.ExpectedParameters);
+							allowedTransforms &= ~CallTransformation.EnforceExplicitIn;
+						}
 						else
 						{
 							break;
@@ -1117,6 +1234,32 @@ namespace ICSharpCode.Decompiler.CSharp
 			return transform;
 		}
 
+		private void EnforceExplicitIn(TranslatedExpression[] arguments, IParameter[] expectedParameters)
+		{
+			for (int i = 0; i < arguments.Length; i++)
+			{
+				if (expectedParameters[i].ReferenceKind != ReferenceKind.In)
+					continue;
+				if (arguments[i].Expression is DirectionExpression)
+					continue;
+
+				arguments[i] = WrapInAsRefReadOnly(arguments[i]);
+				expressionBuilder.statementBuilder.EmitAsRefReadOnly = true;
+			}
+		}
+
+		private TranslatedExpression WrapInAsRefReadOnly(TranslatedExpression arg)
+		{
+			return new DirectionExpression(
+				FieldDirection.In,
+				new InvocationExpression {
+					Target = new IdentifierExpression("ILSpyHelper_AsRefReadOnly"),
+					Arguments = { arg.Expression }
+				}
+			).WithRR(new ByReferenceResolveResult(arg.Type, ReferenceKind.In))
+			.WithoutILInstruction();
+		}
+
 		private bool IsPossibleExtensionMethodCallOnNull(IMethod method, IList<TranslatedExpression> arguments)
 		{
 			return method.IsExtensionMethod && arguments.Count > 0 && arguments[0].Expression is NullReferenceExpression;
@@ -1155,14 +1298,20 @@ namespace ICSharpCode.Decompiler.CSharp
 				}
 				else
 				{
+					IParameter parameter = expectedParameters[i];
 					IType parameterType;
-					if (expectedParameters[i].Type.Kind == TypeKind.Dynamic)
+					if (parameter.Type.Kind == TypeKind.Dynamic)
 					{
 						parameterType = expressionBuilder.compilation.FindType(KnownTypeCode.Object);
 					}
 					else
 					{
-						parameterType = expectedParameters[i].Type;
+						parameterType = parameter.Type;
+					}
+
+					if (parameter.ReferenceKind == ReferenceKind.In && parameterType is ByReferenceType brt && arguments[i].Type is not ByReferenceType)
+					{
+						parameterType = brt.ElementType;
 					}
 
 					arguments[i] = arguments[i].ConvertTo(parameterType, expressionBuilder, allowImplicitConversion: false);
@@ -1271,7 +1420,10 @@ namespace ICSharpCode.Decompiler.CSharp
 						resolver.CurrentTypeDefinition == method.DeclaringTypeDefinition;
 					if (lookup.IsAccessible(ctor, allowProtectedAccess))
 					{
-						or.AddCandidate(ctor);
+						Log.Indent();
+						OverloadResolutionErrors errors = or.AddCandidate(ctor);
+						Log.Unindent();
+						or.LogCandidateAddingResult("  Candidate", ctor, errors);
 					}
 				}
 			}
diff --git a/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs b/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs
index e20b6d818..0f629f38e 100644
--- a/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs
+++ b/ICSharpCode.Decompiler/CSharp/ExpressionBuilder.cs
@@ -1799,7 +1799,17 @@ namespace ICSharpCode.Decompiler.CSharp
 
 		protected internal override TranslatedExpression VisitUserDefinedCompoundAssign(UserDefinedCompoundAssign inst, TranslationContext context)
 		{
-			IType loadType = inst.Method.Parameters[0].Type;
+			IType loadType;
+			bool isSpanBasedStringConcat = CallBuilder.IsSpanBasedStringConcat(inst.Method);
+			if (isSpanBasedStringConcat)
+			{
+				loadType = typeSystem.FindType(KnownTypeCode.String);
+			}
+			else
+			{
+				loadType = inst.Method.Parameters[0].Type;
+			}
+
 			ExpressionWithResolveResult target;
 			if (inst.TargetKind == CompoundTargetKind.Address)
 			{
@@ -1821,11 +1831,24 @@ namespace ICSharpCode.Decompiler.CSharp
 			if (UserDefinedCompoundAssign.IsStringConcat(inst.Method))
 			{
 				Debug.Assert(inst.Method.Parameters.Count == 2);
-				var value = Translate(inst.Value).ConvertTo(inst.Method.Parameters[1].Type, this, allowImplicitConversion: true);
-				var valueExpr = ReplaceMethodCallsWithOperators.RemoveRedundantToStringInConcat(value, inst.Method, isLastArgument: true).Detach();
+				Expression valueExpr;
+				ResolveResult valueResolveResult;
+				if (isSpanBasedStringConcat && inst.Value is NewObj { Arguments: [AddressOf addressOf] })
+				{
+					IType charType = typeSystem.FindType(KnownTypeCode.Char);
+					var value = Translate(addressOf.Value, charType).ConvertTo(charType, this);
+					valueExpr = value.Expression;
+					valueResolveResult = value.ResolveResult;
+				}
+				else
+				{
+					var value = Translate(inst.Value).ConvertTo(inst.Method.Parameters[1].Type, this, allowImplicitConversion: true);
+					valueExpr = ReplaceMethodCallsWithOperators.RemoveRedundantToStringInConcat(value, inst.Method, isLastArgument: true).Detach();
+					valueResolveResult = value.ResolveResult;
+				}
 				return new AssignmentExpression(target, AssignmentOperatorType.Add, valueExpr)
 					.WithILInstruction(inst)
-					.WithRR(new OperatorResolveResult(inst.Method.ReturnType, ExpressionType.AddAssign, inst.Method, inst.IsLifted, new[] { target.ResolveResult, value.ResolveResult }));
+					.WithRR(new OperatorResolveResult(inst.Method.ReturnType, ExpressionType.AddAssign, inst.Method, inst.IsLifted, new[] { target.ResolveResult, valueResolveResult }));
 			}
 			else if (inst.Method.Parameters.Count == 2)
 			{
@@ -2409,7 +2432,7 @@ namespace ICSharpCode.Decompiler.CSharp
 				// C# 10 lambdas can have attributes, but anonymous methods cannot
 				isLambda = true;
 			}
-			else if (settings.UseLambdaSyntax && ame.Parameters.All(p => p.ParameterModifier == ParameterModifier.None))
+			else if (settings.UseLambdaSyntax && ame.Parameters.All(p => p.ParameterModifier == ReferenceKind.None && !p.IsParams))
 			{
 				// otherwise use lambda only if an expression lambda is possible
 				isLambda = (body.Statements.Count == 1 && body.Statements.Single() is ReturnStatement);
@@ -4174,7 +4197,7 @@ namespace ICSharpCode.Decompiler.CSharp
 			}
 			if (info.HasFlag(CSharpArgumentInfoFlags.IsOut))
 			{
-				translatedExpression = ChangeDirectionExpressionTo(translatedExpression, ReferenceKind.Out);
+				translatedExpression = ChangeDirectionExpressionTo(translatedExpression, ReferenceKind.Out, argument is AddressOf);
 			}
 			if (info.HasFlag(CSharpArgumentInfoFlags.NamedArgument) && !string.IsNullOrWhiteSpace(info.Name))
 			{
@@ -4184,11 +4207,21 @@ namespace ICSharpCode.Decompiler.CSharp
 			return translatedExpression;
 		}
 
-		internal static TranslatedExpression ChangeDirectionExpressionTo(TranslatedExpression input, ReferenceKind kind)
+		internal static TranslatedExpression ChangeDirectionExpressionTo(TranslatedExpression input, ReferenceKind kind, bool isAddressOf)
 		{
 			if (!(input.Expression is DirectionExpression dirExpr && input.ResolveResult is ByReferenceResolveResult brrr))
 				return input;
-			dirExpr.FieldDirection = (FieldDirection)kind;
+			if (isAddressOf && kind is ReferenceKind.In or ReferenceKind.RefReadOnly)
+			{
+				return input.UnwrapChild(dirExpr.Expression);
+			}
+			dirExpr.FieldDirection = kind switch {
+				ReferenceKind.Ref => FieldDirection.Ref,
+				ReferenceKind.Out => FieldDirection.Out,
+				ReferenceKind.In => FieldDirection.In,
+				ReferenceKind.RefReadOnly => FieldDirection.In,
+				_ => throw new NotSupportedException("Unsupported reference kind: " + kind)
+			};
 			dirExpr.RemoveAnnotations<ByReferenceResolveResult>();
 			if (brrr.ElementResult == null)
 				brrr = new ByReferenceResolveResult(brrr.ElementType, kind);
@@ -4448,7 +4481,7 @@ namespace ICSharpCode.Decompiler.CSharp
 				var arg = Translate(argInst, typeHint: paramType).ConvertTo(paramType, this, allowImplicitConversion: true);
 				if (paramRefKind != ReferenceKind.None)
 				{
-					arg = ChangeDirectionExpressionTo(arg, paramRefKind);
+					arg = ChangeDirectionExpressionTo(arg, paramRefKind, argInst is AddressOf);
 				}
 				invocation.Arguments.Add(arg);
 			}
diff --git a/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpAmbience.cs b/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpAmbience.cs
index 8e6c5df31..26474c8c8 100644
--- a/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpAmbience.cs
+++ b/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpAmbience.cs
@@ -135,7 +135,9 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor
 				{
 					if ((ConversionFlags & ConversionFlags.ShowParameterModifiers) == 0)
 					{
-						param.ParameterModifier = ParameterModifier.None;
+						param.ParameterModifier = ReferenceKind.None;
+						param.IsScopedRef = false;
+						param.IsParams = false;
 					}
 					if ((ConversionFlags & ConversionFlags.ShowParameterDefaultValues) == 0)
 					{
diff --git a/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpOutputVisitor.cs b/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpOutputVisitor.cs
index 66e6a2acf..e624a8db2 100644
--- a/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpOutputVisitor.cs
+++ b/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpOutputVisitor.cs
@@ -1077,7 +1077,7 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor
 				return true;
 			}
 			var p = lambdaExpression.Parameters.Single();
-			return !(p.Type.IsNull && p.ParameterModifier == ParameterModifier.None);
+			return !(p.Type.IsNull && p.ParameterModifier == ReferenceKind.None && !p.IsParams);
 		}
 
 		public virtual void VisitMemberReferenceExpression(MemberReferenceExpression memberReferenceExpression)
@@ -2608,6 +2608,11 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor
 				WriteKeyword(ParameterDeclaration.ThisModifierRole);
 				Space();
 			}
+			if (parameterDeclaration.IsParams)
+			{
+				WriteKeyword(ParameterDeclaration.ParamsModifierRole);
+				Space();
+			}
 			if (parameterDeclaration.IsScopedRef)
 			{
 				WriteKeyword(ParameterDeclaration.ScopedRefRole);
@@ -2615,19 +2620,20 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor
 			}
 			switch (parameterDeclaration.ParameterModifier)
 			{
-				case ParameterModifier.Ref:
+				case ReferenceKind.Ref:
 					WriteKeyword(ParameterDeclaration.RefModifierRole);
 					Space();
 					break;
-				case ParameterModifier.Out:
-					WriteKeyword(ParameterDeclaration.OutModifierRole);
+				case ReferenceKind.RefReadOnly:
+					WriteKeyword(ParameterDeclaration.RefModifierRole);
+					WriteKeyword(ParameterDeclaration.ReadonlyModifierRole);
 					Space();
 					break;
-				case ParameterModifier.Params:
-					WriteKeyword(ParameterDeclaration.ParamsModifierRole);
+				case ReferenceKind.Out:
+					WriteKeyword(ParameterDeclaration.OutModifierRole);
 					Space();
 					break;
-				case ParameterModifier.In:
+				case ReferenceKind.In:
 					WriteKeyword(ParameterDeclaration.InModifierRole);
 					Space();
 					break;
diff --git a/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs b/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs
index 8abe102f2..15d6a6d34 100644
--- a/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs
+++ b/ICSharpCode.Decompiler/CSharp/RecordDecompiler.cs
@@ -196,7 +196,7 @@ namespace ICSharpCode.Decompiler.CSharp
 						return false;
 					if (!target.MatchLdThis())
 						return false;
-					if (method.Parameters[i].IsIn)
+					if (method.Parameters[i].ReferenceKind is ReferenceKind.In or ReferenceKind.RefReadOnly)
 					{
 						if (!valueInst.MatchLdObj(out valueInst, out _))
 							return false;
@@ -1012,11 +1012,11 @@ namespace ICSharpCode.Decompiler.CSharp
 				var deconstruct = method.Parameters[i];
 				var ctor = primaryCtor.Parameters[i];
 
-				if (!deconstruct.IsOut)
+				if (deconstruct.ReferenceKind != ReferenceKind.Out)
 					return false;
 
 				IType ctorType = ctor.Type;
-				if (ctor.IsIn)
+				if (ctor.ReferenceKind is ReferenceKind.In or ReferenceKind.RefReadOnly)
 					ctorType = ((ByReferenceType)ctorType).ElementType;
 				if (!ctorType.Equals(((ByReferenceType)deconstruct.Type).ElementType))
 					return false;
diff --git a/ICSharpCode.Decompiler/CSharp/Resolver/CSharpConversions.cs b/ICSharpCode.Decompiler/CSharp/Resolver/CSharpConversions.cs
index 86be39f38..51ea71144 100644
--- a/ICSharpCode.Decompiler/CSharp/Resolver/CSharpConversions.cs
+++ b/ICSharpCode.Decompiler/CSharp/Resolver/CSharpConversions.cs
@@ -1128,7 +1128,7 @@ namespace ICSharpCode.Decompiler.CSharp.Resolver
 			foreach (IMethod op in operators)
 			{
 				IType sourceType = op.Parameters[0].Type;
-				if (sourceType.Kind == TypeKind.ByReference && op.Parameters[0].IsIn && fromType.Kind != TypeKind.ByReference)
+				if (sourceType.Kind == TypeKind.ByReference && op.Parameters[0].ReferenceKind == ReferenceKind.In && fromType.Kind != TypeKind.ByReference)
 				{
 					sourceType = ((ByReferenceType)sourceType).ElementType;
 				}
@@ -1235,7 +1235,7 @@ namespace ICSharpCode.Decompiler.CSharp.Resolver
 				// type, as long as no parameter of D has the out parameter modifier.
 				foreach (IParameter p in d.Parameters)
 				{
-					if (p.IsOut)
+					if (p.ReferenceKind == ReferenceKind.Out)
 						return Conversion.None;
 				}
 			}
diff --git a/ICSharpCode.Decompiler/CSharp/Resolver/OverloadResolution.cs b/ICSharpCode.Decompiler/CSharp/Resolver/OverloadResolution.cs
index 09fd186f7..bfeb1bbd2 100644
--- a/ICSharpCode.Decompiler/CSharp/Resolver/OverloadResolution.cs
+++ b/ICSharpCode.Decompiler/CSharp/Resolver/OverloadResolution.cs
@@ -671,8 +671,8 @@ namespace ICSharpCode.Decompiler.CSharp.Resolver
 				{
 					// AllowImplicitIn: `in` parameters can be filled implicitly without `in` DirectionExpression
 					// IsExtensionMethodInvocation: `this ref` and `this in` parameters can be filled implicitly
-					if (((paramRefKind == ReferenceKind.In && AllowImplicitIn)
-						|| (IsExtensionMethodInvocation && parameterIndex == 0 && (paramRefKind == ReferenceKind.In || paramRefKind == ReferenceKind.Ref))
+					if (((paramRefKind is ReferenceKind.In or ReferenceKind.RefReadOnly && AllowImplicitIn)
+						|| (IsExtensionMethodInvocation && parameterIndex == 0 && (paramRefKind is ReferenceKind.In or ReferenceKind.Ref or ReferenceKind.RefReadOnly))
 						) && candidate.ParameterTypes[parameterIndex].SkipModifiers() is ByReferenceType brt)
 					{
 						// Treat the parameter as if it was not declared "in" for the following steps
diff --git a/ICSharpCode.Decompiler/CSharp/StatementBuilder.cs b/ICSharpCode.Decompiler/CSharp/StatementBuilder.cs
index daa2cbe7f..4dfc5f658 100644
--- a/ICSharpCode.Decompiler/CSharp/StatementBuilder.cs
+++ b/ICSharpCode.Decompiler/CSharp/StatementBuilder.cs
@@ -47,6 +47,8 @@ namespace ICSharpCode.Decompiler.CSharp
 		internal IType currentResultType;
 		internal bool currentIsIterator;
 
+		internal bool EmitAsRefReadOnly;
+
 		public StatementBuilder(IDecompilerTypeSystem typeSystem, ITypeResolveContext decompilationContext,
 			ILFunction currentFunction, DecompilerSettings settings, DecompileRun decompileRun,
 			CancellationToken cancellationToken)
@@ -1367,6 +1369,32 @@ namespace ICSharpCode.Decompiler.CSharp
 		{
 			var blockStatement = ConvertBlockContainer(new BlockStatement(), container, container.Blocks, isLoop);
 			DeclareLocalFunctions(currentFunction, container, blockStatement);
+			if (currentFunction.Body == container)
+			{
+				if (EmitAsRefReadOnly)
+				{
+					var methodDecl = new MethodDeclaration();
+					if (settings.StaticLocalFunctions)
+					{
+						methodDecl.Modifiers = Modifiers.Static;
+					}
+
+					methodDecl.ReturnType = new ComposedType() { HasReadOnlySpecifier = true, HasRefSpecifier = true, BaseType = new SimpleType("T") };
+					methodDecl.Name = "ILSpyHelper_AsRefReadOnly";
+					methodDecl.TypeParameters.Add(new TypeParameterDeclaration("T"));
+					methodDecl.Parameters.Add(new ParameterDeclaration { ParameterModifier = ReferenceKind.In, Type = new SimpleType("T"), Name = "temp" });
+
+					methodDecl.Body = new BlockStatement();
+					methodDecl.Body.AddChild(new Comment(
+						"ILSpy generated this function to help ensure overload resolution can pick the overload using 'in'"),
+											 Roles.Comment);
+					methodDecl.Body.Add(new ReturnStatement(new DirectionExpression(FieldDirection.Ref, new IdentifierExpression("temp"))));
+
+					blockStatement.Statements.Add(
+						new LocalFunctionDeclarationStatement(methodDecl)
+					);
+				}
+			}
 			return blockStatement;
 		}
 
diff --git a/ICSharpCode.Decompiler/CSharp/Syntax/TypeMembers/ParameterDeclaration.cs b/ICSharpCode.Decompiler/CSharp/Syntax/TypeMembers/ParameterDeclaration.cs
index a86945d66..617f0427a 100644
--- a/ICSharpCode.Decompiler/CSharp/Syntax/TypeMembers/ParameterDeclaration.cs
+++ b/ICSharpCode.Decompiler/CSharp/Syntax/TypeMembers/ParameterDeclaration.cs
@@ -26,32 +26,19 @@
 
 #nullable enable
 
-using System;
+using ICSharpCode.Decompiler.TypeSystem;
 
 namespace ICSharpCode.Decompiler.CSharp.Syntax
 {
-	public enum ParameterModifier
-	{
-		None,
-		Ref,
-		Out,
-		Params,
-		In,
-		Scoped
-	}
-
 	public class ParameterDeclaration : AstNode
 	{
 		public static readonly Role<AttributeSection> AttributeRole = EntityDeclaration.AttributeRole;
 		public static readonly TokenRole ThisModifierRole = new TokenRole("this");
 		public static readonly TokenRole ScopedRefRole = new TokenRole("scoped");
-		[Obsolete("Renamed to ScopedRefRole")]
-		public static readonly TokenRole RefScopedRole = ScopedRefRole;
 		public static readonly TokenRole RefModifierRole = new TokenRole("ref");
+		public static readonly TokenRole ReadonlyModifierRole = ComposedType.ReadonlyRole;
 		public static readonly TokenRole OutModifierRole = new TokenRole("out");
 		public static readonly TokenRole InModifierRole = new TokenRole("in");
-		[Obsolete("C# 11 preview: \"ref scoped\" no longer supported")]
-		public static readonly TokenRole ValueScopedRole = new TokenRole("scoped");
 		public static readonly TokenRole ParamsModifierRole = new TokenRole("params");
 
 		#region PatternPlaceholder
@@ -107,6 +94,7 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
 		}
 
 		bool hasThisModifier;
+		bool isParams;
 		bool isScopedRef;
 
 		public CSharpTokenNode ThisKeyword {
@@ -127,16 +115,15 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
 			}
 		}
 
-		public bool IsScopedRef {
-			get { return isScopedRef; }
+		public bool IsParams {
+			get { return isParams; }
 			set {
 				ThrowIfFrozen();
-				isScopedRef = value;
+				isParams = value;
 			}
 		}
 
-		[Obsolete("Renamed to IsScopedRef")]
-		public bool IsRefScoped {
+		public bool IsScopedRef {
 			get { return isScopedRef; }
 			set {
 				ThrowIfFrozen();
@@ -144,15 +131,9 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
 			}
 		}
 
-		[Obsolete("C# 11 preview: \"ref scoped\" no longer supported")]
-		public bool IsValueScoped {
-			get { return false; }
-			set { }
-		}
+		ReferenceKind parameterModifier;
 
-		ParameterModifier parameterModifier;
-
-		public ParameterModifier ParameterModifier {
+		public ReferenceKind ParameterModifier {
 			get { return parameterModifier; }
 			set {
 				ThrowIfFrozen();
@@ -240,19 +221,6 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
 		{
 		}
 
-		public ParameterDeclaration(AstType type, string name, ParameterModifier modifier = ParameterModifier.None)
-		{
-			Type = type;
-			Name = name;
-			ParameterModifier = modifier;
-		}
-
-		public ParameterDeclaration(string name, ParameterModifier modifier = ParameterModifier.None)
-		{
-			Name = name;
-			ParameterModifier = modifier;
-		}
-
 		public new ParameterDeclaration Clone()
 		{
 			return (ParameterDeclaration)base.Clone();
diff --git a/ICSharpCode.Decompiler/CSharp/Syntax/TypeSystemAstBuilder.cs b/ICSharpCode.Decompiler/CSharp/Syntax/TypeSystemAstBuilder.cs
index 799e79035..c26998303 100644
--- a/ICSharpCode.Decompiler/CSharp/Syntax/TypeSystemAstBuilder.cs
+++ b/ICSharpCode.Decompiler/CSharp/Syntax/TypeSystemAstBuilder.cs
@@ -372,14 +372,9 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
 				for (int i = 0; i < fpt.ParameterTypes.Length; i++)
 				{
 					var paramDecl = new ParameterDeclaration();
-					paramDecl.ParameterModifier = fpt.ParameterReferenceKinds[i] switch {
-						ReferenceKind.In => ParameterModifier.In,
-						ReferenceKind.Ref => ParameterModifier.Ref,
-						ReferenceKind.Out => ParameterModifier.Out,
-						_ => ParameterModifier.None,
-					};
+					paramDecl.ParameterModifier = fpt.ParameterReferenceKinds[i];
 					IType parameterType = fpt.ParameterTypes[i];
-					if (paramDecl.ParameterModifier != ParameterModifier.None && parameterType is ByReferenceType brt)
+					if (paramDecl.ParameterModifier != ReferenceKind.None && parameterType is ByReferenceType brt)
 					{
 						paramDecl.Type = ConvertType(brt.ElementType);
 					}
@@ -1649,22 +1644,8 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
 			if (parameter == null)
 				throw new ArgumentNullException(nameof(parameter));
 			ParameterDeclaration decl = new ParameterDeclaration();
-			if (parameter.IsRef)
-			{
-				decl.ParameterModifier = ParameterModifier.Ref;
-			}
-			else if (parameter.IsOut)
-			{
-				decl.ParameterModifier = ParameterModifier.Out;
-			}
-			else if (parameter.IsIn)
-			{
-				decl.ParameterModifier = ParameterModifier.In;
-			}
-			else if (parameter.IsParams)
-			{
-				decl.ParameterModifier = ParameterModifier.Params;
-			}
+			decl.ParameterModifier = parameter.ReferenceKind;
+			decl.IsParams = parameter.IsParams;
 			decl.IsScopedRef = parameter.Lifetime.ScopedRef;
 			if (ShowAttributes)
 			{
@@ -1685,7 +1666,8 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
 			{
 				decl.Name = parameter.Name;
 			}
-			if (parameter.IsOptional && decl.ParameterModifier is ParameterModifier.None or ParameterModifier.In && parameter.HasConstantValueInSignature && this.ShowConstantValues)
+			if (parameter.IsOptional && decl.ParameterModifier is ReferenceKind.None or ReferenceKind.In or ReferenceKind.RefReadOnly
+				&& parameter.HasConstantValueInSignature && this.ShowConstantValues)
 			{
 				try
 				{
diff --git a/ICSharpCode.Decompiler/CSharp/Transforms/DecimalConstantTransform.cs b/ICSharpCode.Decompiler/CSharp/Transforms/DecimalConstantTransform.cs
deleted file mode 100644
index ea9d78ff9..000000000
--- a/ICSharpCode.Decompiler/CSharp/Transforms/DecimalConstantTransform.cs
+++ /dev/null
@@ -1,62 +0,0 @@
-// Copyright (c) 2011 AlphaSierraPapa for the SharpDevelop Team
-// 
-// 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.
-
-using ICSharpCode.Decompiler.CSharp.Syntax;
-using ICSharpCode.Decompiler.CSharp.Syntax.PatternMatching;
-using ICSharpCode.Decompiler.TypeSystem;
-
-namespace ICSharpCode.Decompiler.CSharp.Transforms
-{
-	/// <summary>
-	/// Transforms decimal constant fields.
-	/// </summary>
-	public class DecimalConstantTransform : DepthFirstAstVisitor, IAstTransform
-	{
-		static readonly PrimitiveType decimalType = new PrimitiveType("decimal");
-
-		public override void VisitFieldDeclaration(FieldDeclaration fieldDeclaration)
-		{
-			const Modifiers staticReadOnly = Modifiers.Static | Modifiers.Readonly;
-			if ((fieldDeclaration.Modifiers & staticReadOnly) == staticReadOnly && decimalType.IsMatch(fieldDeclaration.ReturnType))
-			{
-				foreach (var attributeSection in fieldDeclaration.Attributes)
-				{
-					foreach (var attribute in attributeSection.Attributes)
-					{
-						var t = attribute.Type.GetSymbol() as IType;
-						if (t != null && t.Name == "DecimalConstantAttribute" && t.Namespace == "System.Runtime.CompilerServices")
-						{
-							attribute.Remove();
-							if (attributeSection.Attributes.Count == 0)
-								attributeSection.Remove();
-							fieldDeclaration.Modifiers = (fieldDeclaration.Modifiers & ~staticReadOnly) | Modifiers.Const;
-							return;
-						}
-					}
-				}
-			}
-		}
-
-		public void Run(AstNode rootNode, TransformContext context)
-		{
-			if (!context.Settings.DecimalConstants)
-				return;
-			rootNode.AcceptVisitor(this);
-		}
-	}
-}
diff --git a/ICSharpCode.Decompiler/CSharp/Transforms/DeclareVariables.cs b/ICSharpCode.Decompiler/CSharp/Transforms/DeclareVariables.cs
index b17960e36..c84d9e81a 100644
--- a/ICSharpCode.Decompiler/CSharp/Transforms/DeclareVariables.cs
+++ b/ICSharpCode.Decompiler/CSharp/Transforms/DeclareVariables.cs
@@ -205,7 +205,11 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
 		{
 			foreach (var stmt in rootNode.DescendantsAndSelf.OfType<ExpressionStatement>())
 			{
-				if (!IsValidInStatementExpression(stmt.Expression))
+				if (stmt.Expression is DirectionExpression dir && IsValidInStatementExpression(dir.Expression))
+				{
+					stmt.Expression = dir.Expression.Detach();
+				}
+				else if (!IsValidInStatementExpression(stmt.Expression))
 				{
 					// fetch ILFunction
 					var function = stmt.Ancestors.SelectMany(a => a.Annotations.OfType<ILFunction>()).First(f => f.Parent == null);
diff --git a/ICSharpCode.Decompiler/CSharp/Transforms/EscapeInvalidIdentifiers.cs b/ICSharpCode.Decompiler/CSharp/Transforms/EscapeInvalidIdentifiers.cs
index 2b1dc7f1d..828d220af 100644
--- a/ICSharpCode.Decompiler/CSharp/Transforms/EscapeInvalidIdentifiers.cs
+++ b/ICSharpCode.Decompiler/CSharp/Transforms/EscapeInvalidIdentifiers.cs
@@ -168,6 +168,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
 			"System.Runtime.CompilerServices.NativeIntegerAttribute",
 			"System.Runtime.CompilerServices.RefSafetyRulesAttribute",
 			"System.Runtime.CompilerServices.ScopedRefAttribute",
+			"System.Runtime.CompilerServices.RequiresLocationAttribute",
 			"Microsoft.CodeAnalysis.EmbeddedAttribute",
 		};
 
diff --git a/ICSharpCode.Decompiler/CSharp/Transforms/IntroduceQueryExpressions.cs b/ICSharpCode.Decompiler/CSharp/Transforms/IntroduceQueryExpressions.cs
index 6d804cf09..8e5e55368 100644
--- a/ICSharpCode.Decompiler/CSharp/Transforms/IntroduceQueryExpressions.cs
+++ b/ICSharpCode.Decompiler/CSharp/Transforms/IntroduceQueryExpressions.cs
@@ -389,7 +389,7 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
 
 		private static bool ValidateParameter(ParameterDeclaration p)
 		{
-			return p.ParameterModifier == ParameterModifier.None && p.Attributes.Count == 0;
+			return p.ParameterModifier == Decompiler.TypeSystem.ReferenceKind.None && p.Attributes.Count == 0;
 		}
 	}
 }
diff --git a/ICSharpCode.Decompiler/CSharp/Transforms/PatternStatementTransform.cs b/ICSharpCode.Decompiler/CSharp/Transforms/PatternStatementTransform.cs
index f9a605012..3605d968d 100644
--- a/ICSharpCode.Decompiler/CSharp/Transforms/PatternStatementTransform.cs
+++ b/ICSharpCode.Decompiler/CSharp/Transforms/PatternStatementTransform.cs
@@ -1167,6 +1167,17 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
 			}
 			return base.VisitBinaryOperatorExpression(expr);
 		}
+
+		public override AstNode VisitUnaryOperatorExpression(UnaryOperatorExpression expr)
+		{
+			if (expr.Operator == UnaryOperatorType.Not && expr.Expression is BinaryOperatorExpression { Operator: BinaryOperatorType.Equality } binary)
+			{
+				binary.Operator = BinaryOperatorType.InEquality;
+				expr.ReplaceWith(binary.Detach());
+				return VisitBinaryOperatorExpression(binary);
+			}
+			return base.VisitUnaryOperatorExpression(expr);
+		}
 		#endregion
 
 		#region C# 7.3 pattern based fixed (for value types)
diff --git a/ICSharpCode.Decompiler/CSharp/Transforms/ReplaceMethodCallsWithOperators.cs b/ICSharpCode.Decompiler/CSharp/Transforms/ReplaceMethodCallsWithOperators.cs
index cf9970232..36624535b 100644
--- a/ICSharpCode.Decompiler/CSharp/Transforms/ReplaceMethodCallsWithOperators.cs
+++ b/ICSharpCode.Decompiler/CSharp/Transforms/ReplaceMethodCallsWithOperators.cs
@@ -274,7 +274,8 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
 			}
 			foreach (var arg in arguments)
 			{
-				if (arg.GetResolveResult() is InvocationResolveResult rr && IsStringConcat(rr.Member))
+				var rr = arg.GetResolveResult();
+				if (rr is InvocationResolveResult irr && IsStringConcat(irr.Member))
 				{
 					// Roslyn + mcs also flatten nested string.Concat() invocations within a operator+ use,
 					// which causes it to use the incorrect evaluation order despite the code using an
@@ -282,6 +283,11 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms
 					// This problem is avoided if the outer call remains string.Concat() as well.
 					return false;
 				}
+				if (rr.Type.IsByRefLike)
+				{
+					// ref structs cannot be converted to object for use with +
+					return false;
+				}
 			}
 
 			// One of the first two arguments must be string, otherwise the + operator
diff --git a/ICSharpCode.Decompiler/DecompilerSettings.cs b/ICSharpCode.Decompiler/DecompilerSettings.cs
index 1032093b3..c9d2e77ab 100644
--- a/ICSharpCode.Decompiler/DecompilerSettings.cs
+++ b/ICSharpCode.Decompiler/DecompilerSettings.cs
@@ -152,7 +152,6 @@ namespace ICSharpCode.Decompiler
 			}
 			if (languageVersion < CSharp.LanguageVersion.CSharp11_0)
 			{
-				parameterNullCheck = false;
 				scopedRef = false;
 				requiredMembers = false;
 				numericIntPtr = false;
@@ -160,11 +159,17 @@ namespace ICSharpCode.Decompiler
 				unsignedRightShift = false;
 				checkedOperators = false;
 			}
+			if (languageVersion < CSharp.LanguageVersion.CSharp12_0)
+			{
+				refReadOnlyParameters = false;
+			}
 		}
 
 		public CSharp.LanguageVersion GetMinimumRequiredVersion()
 		{
-			if (parameterNullCheck || scopedRef || requiredMembers || numericIntPtr || utf8StringLiterals || unsignedRightShift || checkedOperators)
+			if (refReadOnlyParameters)
+				return CSharp.LanguageVersion.CSharp12_0;
+			if (scopedRef || requiredMembers || numericIntPtr || utf8StringLiterals || unsignedRightShift || checkedOperators)
 				return CSharp.LanguageVersion.CSharp11_0;
 			if (fileScopedNamespaces || recordStructs)
 				return CSharp.LanguageVersion.CSharp10_0;
@@ -443,26 +448,6 @@ namespace ICSharpCode.Decompiler
 			}
 		}
 
-		bool parameterNullCheck = false;
-
-		/// <summary>
-		/// Use C# 11 preview parameter null-checking (<code>string param!!</code>).
-		/// </summary>
-		[Category("C# 11.0 / VS 2022.4")]
-		[Description("DecompilerSettings.ParameterNullCheck")]
-		[Browsable(false)]
-		[Obsolete("This feature did not make it into C# 11, and may be removed in a future version of the decompiler.")]
-		public bool ParameterNullCheck {
-			get { return parameterNullCheck; }
-			set {
-				if (parameterNullCheck != value)
-				{
-					parameterNullCheck = value;
-					OnPropertyChanged();
-				}
-			}
-		}
-
 		bool anonymousMethods = true;
 
 		/// <summary>
@@ -2012,6 +1997,24 @@ namespace ICSharpCode.Decompiler
 			}
 		}
 
+		bool refReadOnlyParameters = true;
+
+		/// <summary>
+		/// Gets/sets whether RequiresLocationAttribute on parameters should be replaced with 'ref readonly' modifiers.
+		/// </summary>
+		[Category("C# 12.0 / VS 2022.8")]
+		[Description("DecompilerSettings.RefReadOnlyParameters")]
+		public bool RefReadOnlyParameters {
+			get { return refReadOnlyParameters; }
+			set {
+				if (refReadOnlyParameters != value)
+				{
+					refReadOnlyParameters = value;
+					OnPropertyChanged();
+				}
+			}
+		}
+
 		bool separateLocalVariableDeclarations = false;
 
 		/// <summary>
diff --git a/ICSharpCode.Decompiler/FlowAnalysis/DefiniteAssignmentVisitor.cs b/ICSharpCode.Decompiler/FlowAnalysis/DefiniteAssignmentVisitor.cs
index c12175f0b..c12aa9579 100644
--- a/ICSharpCode.Decompiler/FlowAnalysis/DefiniteAssignmentVisitor.cs
+++ b/ICSharpCode.Decompiler/FlowAnalysis/DefiniteAssignmentVisitor.cs
@@ -262,7 +262,7 @@ namespace ICSharpCode.Decompiler.FlowAnalysis
 			bool hasOutArgs = false;
 			foreach (var arg in call.Arguments)
 			{
-				if (arg.MatchLdLoca(out var v) && call.GetParameter(arg.ChildIndex)?.IsOut == true)
+				if (arg.MatchLdLoca(out var v) && call.GetParameter(arg.ChildIndex)?.ReferenceKind == ReferenceKind.Out)
 				{
 					// Visiting ldloca would require the variable to be initialized,
 					// but we don't need out arguments to be initialized.
@@ -278,7 +278,7 @@ namespace ICSharpCode.Decompiler.FlowAnalysis
 			{
 				foreach (var arg in call.Arguments)
 				{
-					if (arg.MatchLdLoca(out var v) && call.GetParameter(arg.ChildIndex)?.IsOut == true)
+					if (arg.MatchLdLoca(out var v) && call.GetParameter(arg.ChildIndex)?.ReferenceKind == ReferenceKind.Out)
 					{
 						HandleStore(v);
 					}
diff --git a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj
index 7fe6df907..ba3a308e8 100644
--- a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj
+++ b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj
@@ -97,7 +97,6 @@
     <Compile Include="DecompilationProgress.cs" />
     <Compile Include="Disassembler\IEntityProcessor.cs" />
     <Compile Include="Disassembler\SortByNameProcessor.cs" />
-    <Compile Include="IL\ApplyPdbLocalTypeInfoTypeVisitor.cs" />
     <Compile Include="Metadata\MetadataFile.cs" />
     <Compile Include="Metadata\ModuleReferenceMetadata.cs" />
     <Compile Include="NRTAttributes.cs" />
@@ -135,7 +134,6 @@
     <Compile Include="IL\Transforms\InterpolatedStringTransform.cs" />
     <Compile Include="IL\Transforms\IntroduceNativeIntTypeOnLocals.cs" />
     <Compile Include="IL\Transforms\LdLocaDupInitObjTransform.cs" />
-    <Compile Include="IL\Transforms\ParameterNullCheckTransform.cs" />
     <Compile Include="IL\Transforms\PatternMatchingTransform.cs" />
     <Compile Include="IL\Transforms\RemoveInfeasiblePathTransform.cs" />
     <Compile Include="Instrumentation\DecompilerEventSource.cs" />
@@ -336,7 +334,6 @@
     <Compile Include="DecompilerException.cs" />
     <Compile Include="DecompilerSettings.cs" />
     <Compile Include="CSharp\Transforms\ContextTrackingVisitor.cs" />
-    <Compile Include="CSharp\Transforms\DecimalConstantTransform.cs" />
     <Compile Include="CSharp\Transforms\DeclareVariables.cs" />
     <Compile Include="CSharp\Transforms\EscapeInvalidIdentifiers.cs" />
     <Compile Include="CSharp\Transforms\FixNameCollisions.cs" />
diff --git a/ICSharpCode.Decompiler/IL/ApplyPdbLocalTypeInfoTypeVisitor.cs b/ICSharpCode.Decompiler/IL/ApplyPdbLocalTypeInfoTypeVisitor.cs
deleted file mode 100644
index 1d8796c1e..000000000
--- a/ICSharpCode.Decompiler/IL/ApplyPdbLocalTypeInfoTypeVisitor.cs
+++ /dev/null
@@ -1,159 +0,0 @@
-using System;
-using System.Collections.Immutable;
-
-using ICSharpCode.Decompiler.DebugInfo;
-using ICSharpCode.Decompiler.TypeSystem;
-using ICSharpCode.Decompiler.TypeSystem.Implementation;
-
-namespace ICSharpCode.Decompiler.IL
-{
-	/// <summary>
-	/// Heavily based on <see cref="ApplyAttributeTypeVisitor"/>
-	/// </summary>
-	sealed class ApplyPdbLocalTypeInfoTypeVisitor : TypeVisitor
-	{
-		private readonly bool[] dynamicData;
-		private readonly string[] tupleElementNames;
-		private int dynamicTypeIndex = 0;
-		private int tupleTypeIndex = 0;
-
-		private ApplyPdbLocalTypeInfoTypeVisitor(bool[] dynamicData, string[] tupleElementNames)
-		{
-			this.dynamicData = dynamicData;
-			this.tupleElementNames = tupleElementNames;
-		}
-
-		public static IType Apply(IType type, PdbExtraTypeInfo pdbExtraTypeInfo)
-		{
-			if (pdbExtraTypeInfo.DynamicFlags is null && pdbExtraTypeInfo.TupleElementNames is null)
-				return type;
-			return type.AcceptVisitor(new ApplyPdbLocalTypeInfoTypeVisitor(pdbExtraTypeInfo.DynamicFlags, pdbExtraTypeInfo.TupleElementNames));
-		}
-
-		public override IType VisitModOpt(ModifiedType type)
-		{
-			dynamicTypeIndex++;
-			return base.VisitModOpt(type);
-		}
-
-		public override IType VisitModReq(ModifiedType type)
-		{
-			dynamicTypeIndex++;
-			return base.VisitModReq(type);
-		}
-
-		public override IType VisitPointerType(PointerType type)
-		{
-			dynamicTypeIndex++;
-			return base.VisitPointerType(type);
-		}
-
-		public override IType VisitArrayType(ArrayType type)
-		{
-			dynamicTypeIndex++;
-			return base.VisitArrayType(type);
-		}
-
-		public override IType VisitByReferenceType(ByReferenceType type)
-		{
-			dynamicTypeIndex++;
-			return base.VisitByReferenceType(type);
-		}
-
-		public override IType VisitTupleType(TupleType type)
-		{
-			if (tupleElementNames != null && tupleTypeIndex < tupleElementNames.Length)
-			{
-				int tupleCardinality = type.Cardinality;
-				string[] extractedValues = new string[tupleCardinality];
-				Array.Copy(tupleElementNames, tupleTypeIndex, extractedValues, 0,
-					Math.Min(tupleCardinality, tupleElementNames.Length - tupleTypeIndex));
-				var elementNames = ImmutableArray.CreateRange(extractedValues);
-				tupleTypeIndex += tupleCardinality;
-
-				int level = 0;
-				var elementTypes = new IType[type.ElementTypes.Length];
-				for (int i = 0; i < type.ElementTypes.Length; i++)
-				{
-					dynamicTypeIndex++;
-					IType elementType = type.ElementTypes[i];
-					if (i != 0 && (i - level) % TupleType.RestPosition == 0 && elementType is TupleType tuple)
-					{
-						tupleTypeIndex += tuple.Cardinality;
-						level++;
-					}
-					elementTypes[i] = elementType.AcceptVisitor(this);
-				}
-
-				return new TupleType(
-					type.Compilation,
-					elementTypes.ToImmutableArray(),
-					elementNames,
-					type.GetDefinition()?.ParentModule
-				);
-			}
-			return base.VisitTupleType(type);
-		}
-
-		public override IType VisitParameterizedType(ParameterizedType type)
-		{
-			if (TupleType.IsTupleCompatible(type, out var tupleCardinality))
-				tupleTypeIndex += tupleCardinality;
-			// Visit generic type and type arguments.
-			// Like base implementation, except that it increments dynamicTypeIndex.
-			var genericType = type.GenericType.AcceptVisitor(this);
-			bool changed = type.GenericType != genericType;
-			var arguments = new IType[type.TypeArguments.Count];
-			for (int i = 0; i < type.TypeArguments.Count; i++)
-			{
-				dynamicTypeIndex++;
-				arguments[i] = type.TypeArguments[i].AcceptVisitor(this);
-				changed = changed || arguments[i] != type.TypeArguments[i];
-			}
-			if (!changed)
-				return type;
-			return new ParameterizedType(genericType, arguments);
-		}
-
-		public override IType VisitFunctionPointerType(FunctionPointerType type)
-		{
-			dynamicTypeIndex++;
-			if (type.ReturnIsRefReadOnly)
-			{
-				dynamicTypeIndex++;
-			}
-			var returnType = type.ReturnType.AcceptVisitor(this);
-			bool changed = type.ReturnType != returnType;
-			var parameters = new IType[type.ParameterTypes.Length];
-			for (int i = 0; i < parameters.Length; i++)
-			{
-				dynamicTypeIndex += type.ParameterReferenceKinds[i] switch {
-					ReferenceKind.None => 1,
-					ReferenceKind.Ref => 1,
-					ReferenceKind.Out => 2, // in/out also count the modreq
-					ReferenceKind.In => 2,
-					_ => throw new NotSupportedException()
-				};
-				parameters[i] = type.ParameterTypes[i].AcceptVisitor(this);
-				changed = changed || parameters[i] != type.ParameterTypes[i];
-			}
-			if (!changed)
-				return type;
-			return type.WithSignature(returnType, parameters.ToImmutableArray());
-		}
-
-		public override IType VisitTypeDefinition(ITypeDefinition type)
-		{
-			IType newType = type;
-			var ktc = type.KnownTypeCode;
-			if (ktc == KnownTypeCode.Object && dynamicData is not null)
-			{
-				if (dynamicTypeIndex >= dynamicData.Length)
-					newType = SpecialType.Dynamic;
-				else if (dynamicData[dynamicTypeIndex])
-					newType = SpecialType.Dynamic;
-			}
-			return newType;
-		}
-	}
-}
diff --git a/ICSharpCode.Decompiler/IL/ILReader.cs b/ICSharpCode.Decompiler/IL/ILReader.cs
index ca1ed07c1..2ec9da40a 100644
--- a/ICSharpCode.Decompiler/IL/ILReader.cs
+++ b/ICSharpCode.Decompiler/IL/ILReader.cs
@@ -285,7 +285,7 @@ namespace ICSharpCode.Decompiler.IL
 			{
 				IParameter parameter = method.Parameters[paramIndex - offset];
 				ILVariable ilVar = CreateILVariable(paramIndex - offset, parameter.Type, parameter.Name);
-				ilVar.IsRefReadOnly = parameter.IsIn;
+				ilVar.IsRefReadOnly = parameter.ReferenceKind is ReferenceKind.In or ReferenceKind.RefReadOnly;
 				parameterVariables[paramIndex] = ilVar;
 				paramIndex++;
 			}
@@ -308,7 +308,7 @@ namespace ICSharpCode.Decompiler.IL
 			if (UseDebugSymbols && DebugInfo is not null &&
 				DebugInfo.TryGetExtraTypeInfo((MethodDefinitionHandle)method.MetadataToken, index, out var pdbExtraTypeInfo))
 			{
-				type = ApplyPdbLocalTypeInfoTypeVisitor.Apply(type, pdbExtraTypeInfo);
+				type = ApplyAttributeTypeVisitor.ApplyAttributesToType(type, compilation, module.TypeSystemOptions, pdbExtraTypeInfo);
 			}
 
 			ILVariable ilVar = new ILVariable(kind, type, index);
diff --git a/ICSharpCode.Decompiler/IL/Instructions/MatchInstruction.cs b/ICSharpCode.Decompiler/IL/Instructions/MatchInstruction.cs
index af9074f12..6ed9b3760 100644
--- a/ICSharpCode.Decompiler/IL/Instructions/MatchInstruction.cs
+++ b/ICSharpCode.Decompiler/IL/Instructions/MatchInstruction.cs
@@ -267,7 +267,7 @@ namespace ICSharpCode.Decompiler.IL
 
 			for (int i = firstOutParam; i < method.Parameters.Count; i++)
 			{
-				if (!method.Parameters[i].IsOut)
+				if (method.Parameters[i].ReferenceKind != ReferenceKind.Out)
 					return false;
 			}
 
diff --git a/ICSharpCode.Decompiler/IL/Transforms/ExpressionTransforms.cs b/ICSharpCode.Decompiler/IL/Transforms/ExpressionTransforms.cs
index 5385bbaae..c3b464399 100644
--- a/ICSharpCode.Decompiler/IL/Transforms/ExpressionTransforms.cs
+++ b/ICSharpCode.Decompiler/IL/Transforms/ExpressionTransforms.cs
@@ -96,8 +96,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms
 				return;
 			}
 			else if (inst.Kind == ComparisonKind.Inequality && inst.LiftingKind == ComparisonLiftingKind.None
-			  && inst.Right.MatchLdcI4(0) && (IfInstruction.IsInConditionSlot(inst) || inst.Left is Comp)
-		  )
+				&& inst.Right.MatchLdcI4(0) && (IfInstruction.IsInConditionSlot(inst) || inst.Left is Comp))
 			{
 				// if (comp(x != 0)) ==> if (x)
 				// comp(comp(...) != 0) => comp(...)
@@ -107,6 +106,10 @@ namespace ICSharpCode.Decompiler.IL.Transforms
 				inst.Left.AcceptVisitor(this);
 				return;
 			}
+			if (context.Settings.LiftNullables)
+			{
+				new NullableLiftingTransform(context).Run(inst);
+			}
 
 			base.VisitComp(inst);
 			if (inst.IsLifted)
diff --git a/ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs b/ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs
index fc15ebc3a..7b38b2e90 100644
--- a/ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs
+++ b/ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs
@@ -244,7 +244,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms
 				var loadInst = r.LoadInst;
 				if (loadInst.OpCode == OpCode.LdLoca)
 				{
-					if (!IsGeneratedValueTypeTemporary((LdLoca)loadInst, v, inlinedExpression, options))
+					if (!IsGeneratedTemporaryForAddressOf((LdLoca)loadInst, v, inlinedExpression, options))
 						return false;
 				}
 				else
@@ -289,7 +289,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms
 		/// </summary>
 		/// <param name="loadInst">The load instruction (a descendant within 'next')</param>
 		/// <param name="v">The variable being inlined.</param>
-		static bool IsGeneratedValueTypeTemporary(LdLoca loadInst, ILVariable v, ILInstruction inlinedExpression, InliningOptions options)
+		static bool IsGeneratedTemporaryForAddressOf(LdLoca loadInst, ILVariable v, ILInstruction inlinedExpression, InliningOptions options)
 		{
 			Debug.Assert(loadInst.Variable == v);
 			if (!options.HasFlag(InliningOptions.AllowInliningOfLdloca))
@@ -326,6 +326,39 @@ namespace ICSharpCode.Decompiler.IL.Transforms
 						throw new InvalidOperationException("invalid expression classification");
 				}
 			}
+			else if (IsPassedToReadOnlySpanOfCharCtor(loadInst))
+			{
+				// Always inlining is possible here, because it's an 'in' or 'ref readonly' parameter
+				// and the C# compiler allows calling it with an rvalue, even though that might produce
+				// a warning. Note that we don't need to check the expression classification, because
+				// expressionBuilder.VisitAddressOf will handle creating the copy for us.
+				// This is necessary, because there are compiler-generated uses of this ctor when
+				// concatenating a string to a char and our following transforms assume the char is
+				// already inlined.
+				return true;
+			}
+			else if (IsPassedToInParameter(loadInst))
+			{
+				if (options.HasFlag(InliningOptions.Aggressive))
+				{
+					// Inlining might be required in ctor initializers (see #2714).
+					// expressionBuilder.VisitAddressOf will handle creating the copy for us.
+					return true;
+				}
+
+				switch (ClassifyExpression(inlinedExpression))
+				{
+					case ExpressionClassification.RValue:
+						// For rvalues passed to in parameters, the C# compiler generates a temporary.
+						return true;
+					case ExpressionClassification.MutableLValue:
+					case ExpressionClassification.ReadonlyLValue:
+						// For lvalues passed to in parameters, the C# compiler never generates temporaries.
+						return false;
+					default:
+						throw new InvalidOperationException("invalid expression classification");
+				}
+			}
 			else if (IsUsedAsThisPointerInFieldRead(loadInst))
 			{
 				// mcs generated temporaries for field reads on rvalues (#1555)
@@ -415,6 +448,34 @@ namespace ICSharpCode.Decompiler.IL.Transforms
 			return inst != ldloca && inst.Parent is LdObj;
 		}
 
+		static bool IsPassedToInParameter(LdLoca ldloca)
+		{
+			if (ldloca.Parent is not CallInstruction call)
+			{
+				return false;
+			}
+			return call.GetParameter(ldloca.ChildIndex)?.ReferenceKind is ReferenceKind.In;
+		}
+
+		static bool IsPassedToReadOnlySpanOfCharCtor(LdLoca ldloca)
+		{
+			if (ldloca.Parent is not NewObj call)
+			{
+				return false;
+			}
+			return IsReadOnlySpanCharCtor(call.Method);
+		}
+
+		internal static bool IsReadOnlySpanCharCtor(IMethod method)
+		{
+			return method.IsConstructor
+				&& method.Parameters.Count == 1
+				&& method.DeclaringType.IsKnownType(KnownTypeCode.ReadOnlySpanOfT)
+				&& method.DeclaringType.TypeArguments[0].IsKnownType(KnownTypeCode.Char)
+				&& method.Parameters[0].Type is ByReferenceType brt
+				&& brt.ElementType.IsKnownType(KnownTypeCode.Char);
+		}
+
 		/// <summary>
 		/// Gets whether the instruction, when converted into C#, turns into an l-value that can
 		/// be used to mutate a value-type.
@@ -476,6 +537,9 @@ namespace ICSharpCode.Decompiler.IL.Transforms
 			}
 		}
 
+		/// <summary>
+		/// Gets whether the ILInstruction will turn into a C# expresion that is considered readonly by the C# compiler.
+		/// </summary>
 		internal static bool IsReadonlyReference(ILInstruction addr)
 		{
 			switch (addr)
diff --git a/ICSharpCode.Decompiler/IL/Transforms/IntroduceRefReadOnlyModifierOnLocals.cs b/ICSharpCode.Decompiler/IL/Transforms/IntroduceRefReadOnlyModifierOnLocals.cs
index 946f15363..aba272b1f 100644
--- a/ICSharpCode.Decompiler/IL/Transforms/IntroduceRefReadOnlyModifierOnLocals.cs
+++ b/ICSharpCode.Decompiler/IL/Transforms/IntroduceRefReadOnlyModifierOnLocals.cs
@@ -52,8 +52,18 @@ namespace ICSharpCode.Decompiler.IL
 		{
 			foreach (var store in variable.StoreInstructions.OfType<StLoc>())
 			{
+				// Check if C# requires that the local is ref-readonly in order to allow the store:
 				if (ILInlining.IsReadonlyReference(store.Value))
 					return true;
+				// Check whether the local needs to be ref-readonly to avoid changing the semantics of
+				// a readonly.ldelema:
+				ILInstruction val = store.Value;
+				while (val is LdFlda ldflda)
+				{
+					val = ldflda.Target;
+				}
+				if (val is LdElema { IsReadOnly: true })
+					return true;
 			}
 			return false;
 		}
diff --git a/ICSharpCode.Decompiler/IL/Transforms/LocalFunctionDecompiler.cs b/ICSharpCode.Decompiler/IL/Transforms/LocalFunctionDecompiler.cs
index bc0e209c4..f00fc48b0 100644
--- a/ICSharpCode.Decompiler/IL/Transforms/LocalFunctionDecompiler.cs
+++ b/ICSharpCode.Decompiler/IL/Transforms/LocalFunctionDecompiler.cs
@@ -556,9 +556,9 @@ namespace ICSharpCode.Decompiler.IL.Transforms
 
 		internal static bool IsClosureParameter(IParameter parameter, ITypeResolveContext context)
 		{
-			if (!parameter.IsRef)
+			if (parameter.Type is not ByReferenceType brt)
 				return false;
-			var type = ((ByReferenceType)parameter.Type).ElementType.GetDefinition();
+			var type = brt.ElementType.GetDefinition();
 			return type != null
 				&& type.Kind == TypeKind.Struct
 				&& TransformDisplayClassUsage.IsPotentialClosure(context.CurrentTypeDefinition, type);
diff --git a/ICSharpCode.Decompiler/IL/Transforms/NullableLiftingTransform.cs b/ICSharpCode.Decompiler/IL/Transforms/NullableLiftingTransform.cs
index 4e297b00d..111cac3ce 100644
--- a/ICSharpCode.Decompiler/IL/Transforms/NullableLiftingTransform.cs
+++ b/ICSharpCode.Decompiler/IL/Transforms/NullableLiftingTransform.cs
@@ -85,6 +85,33 @@ namespace ICSharpCode.Decompiler.IL.Transforms
 			return false;
 		}
 
+		/// <summary>
+		/// VS2022.10 / Roslyn 4.10.0 adds an optimization that turns
+		/// a == 42 into a.GetValueOrDefault() == 42 without any HasValue check.
+		/// </summary>
+		public void Run(Comp comp)
+		{
+			if (!comp.IsLifted && comp.Kind.IsEqualityOrInequality())
+			{
+				var left = comp.Left;
+				var right = comp.Right;
+				if (MatchGetValueOrDefault(left, out ILInstruction arg)
+					&& right.MatchLdcI(out var value) && value != 0)
+				{
+					context.Step("comp(a.GetValueOrDefault() == const) -> comp.lifted(a == const)", comp);
+					comp.LiftingKind = ComparisonLiftingKind.CSharp;
+					comp.Left = new LdObj(arg, ((Call)left).Method.DeclaringType);
+				}
+				else if (MatchGetValueOrDefault(right, out arg)
+					&& left.MatchLdcI(out value) && value != 0)
+				{
+					context.Step("comp(const == a.GetValueOrDefault()) -> comp.lifted(const == a)", comp);
+					comp.LiftingKind = ComparisonLiftingKind.CSharp;
+					comp.Right = new LdObj(arg, ((Call)right).Method.DeclaringType);
+				}
+			}
+		}
+
 		public bool RunStatements(Block block, int pos)
 		{
 			// e.g.:
@@ -198,14 +225,14 @@ namespace ICSharpCode.Decompiler.IL.Transforms
 						return LiftCSharpEqualityComparison(comp, ComparisonKind.Inequality, trueInst)
 							?? LiftCSharpUserEqualityComparison(comp, ComparisonKind.Inequality, trueInst);
 					}
-					else if (IsGenericNewPattern(comp.Left, comp.Right, trueInst, falseInst))
+					else if (!comp.IsLifted && IsGenericNewPattern(comp.Left, comp.Right, trueInst, falseInst))
 					{
 						// (default(T) == null) ? Activator.CreateInstance<T>() : default(T)
 						// => Activator.CreateInstance<T>()
 						return trueInst;
 					}
 				}
-				else
+				else if (!comp.IsLifted)
 				{
 					// Not (in)equality, but one of < <= > >=.
 					// Returns false unless all HasValue bits are true.
@@ -374,11 +401,12 @@ namespace ICSharpCode.Decompiler.IL.Transforms
 		{
 			result = default(CompOrDecimal);
 			result.Instruction = inst;
-			if (inst is Comp comp && !comp.IsLifted)
+			if (inst is Comp comp)
 			{
 				result.Kind = comp.Kind;
 				result.Left = comp.Left;
 				result.Right = comp.Right;
+				result.IsLifted = comp.IsLifted;
 				return true;
 			}
 			else if (inst is Call call && call.Method.IsOperator && call.Arguments.Count == 2 && !call.IsLifted)
@@ -422,6 +450,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms
 			public ComparisonKind Kind;
 			public ILInstruction Left;
 			public ILInstruction Right;
+			public bool IsLifted;
 
 			public IType LeftExpectedType {
 				get {
@@ -501,6 +530,8 @@ namespace ICSharpCode.Decompiler.IL.Transforms
 			// The HasValue comparison must be the same operator as the Value comparison.
 			if (hasValueTest is Comp hasValueComp)
 			{
+				if (valueComp.IsLifted || hasValueComp.IsLifted)
+					return null;
 				// Comparing two nullables: HasValue comparison must be the same operator as the Value comparison
 				if ((hasValueTestNegated ? hasValueComp.Kind.Negate() : hasValueComp.Kind) != newComparisonKind)
 					return null;
@@ -549,6 +580,24 @@ namespace ICSharpCode.Decompiler.IL.Transforms
 		/// </summary>
 		ILInstruction LiftCSharpComparison(CompOrDecimal comp, ComparisonKind newComparisonKind)
 		{
+			if (comp.IsLifted)
+			{
+				// Happens when legacy csc generates 'num.GetValueOrDefault() == const && num.HasValue',
+				// checking HasValue is redundant here, modern Roslyn versions optimize it and our
+				// NullableLifting transform on Comp will already lift the lhs of the logic.and.
+				// Treat this case as if the transform had not undone the optimization yet.
+				if (nullableVars.Count != 1)
+				{
+					return null;
+				}
+
+				if (comp.Left.MatchLdLoc(nullableVars[0]) || comp.Right.MatchLdLoc(nullableVars[0]))
+				{
+					return comp.MakeLifted(newComparisonKind, comp.Left.Clone(), comp.Right.Clone());
+				}
+
+				return null;
+			}
 			var (left, right, bits) = DoLiftBinary(comp.Left, comp.Right, comp.LeftExpectedType, comp.RightExpectedType);
 			// due to the restrictions on side effects, we only allow instructions that are pure after lifting.
 			// (we can't check this before lifting due to the calls to GetValueOrDefault())
@@ -584,7 +633,8 @@ namespace ICSharpCode.Decompiler.IL.Transforms
 			//         call op_Inequality(call GetValueOrDefault(ldloca nullable1), call GetValueOrDefault(ldloca nullable2))
 			//      else
 			//         ldc.i4 0
-
+			if (hasValueComp.IsLifted)
+				return null;
 			if (!MatchHasValueCall(hasValueComp.Left, out ILVariable nullable1))
 				return null;
 			if (!MatchHasValueCall(hasValueComp.Right, out ILVariable nullable2))
diff --git a/ICSharpCode.Decompiler/IL/Transforms/ParameterNullCheckTransform.cs b/ICSharpCode.Decompiler/IL/Transforms/ParameterNullCheckTransform.cs
deleted file mode 100644
index 7deb3d9ea..000000000
--- a/ICSharpCode.Decompiler/IL/Transforms/ParameterNullCheckTransform.cs
+++ /dev/null
@@ -1,90 +0,0 @@
-// Copyright (c) 2017 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.Diagnostics.CodeAnalysis;
-
-using ICSharpCode.Decompiler.TypeSystem;
-
-namespace ICSharpCode.Decompiler.IL.Transforms
-{
-	/// <summary>
-	/// Implements transforming &lt;PrivateImplementationDetails&gt;.ThrowIfNull(name, "name"); 
-	/// </summary>
-	class ParameterNullCheckTransform : IILTransform
-	{
-		void IILTransform.Run(ILFunction function, ILTransformContext context)
-		{
-#pragma warning disable 618 // ParameterNullCheck is obsolete
-			if (!context.Settings.ParameterNullCheck)
-				return;
-#pragma warning restore 618
-			// we only need to look at the entry-point as parameter null-checks
-			// do not produce any IL control-flow instructions
-			Block entryPoint = ((BlockContainer)function.Body).EntryPoint;
-			int index = 0;
-			// Early versions of this pattern produced call ThrowIfNull instructions after
-			// state-machine initialization instead of right at the start of the method.
-			// In order to support both patterns, we scan all instructions,
-			// if the current function is decorated with a state-machine attribute.
-			bool scanFullBlock = function.Method != null
-				&& (function.Method.HasAttribute(KnownAttribute.IteratorStateMachine)
-					|| function.Method.HasAttribute(KnownAttribute.AsyncIteratorStateMachine)
-					|| function.Method.HasAttribute(KnownAttribute.AsyncStateMachine));
-			// loop over all instructions
-			while (index < entryPoint.Instructions.Count)
-			{
-				// The pattern does not match for the current instruction
-				if (!MatchThrowIfNullCall(entryPoint.Instructions[index], out ILVariable? parameterVariable))
-				{
-					if (scanFullBlock)
-					{
-						// continue scanning
-						index++;
-						continue;
-					}
-					else
-					{
-						// abort
-						break;
-					}
-				}
-				// remove the call to ThrowIfNull
-				entryPoint.Instructions.RemoveAt(index);
-				// remember to generate !! when producing the final output.
-				parameterVariable.HasNullCheck = true;
-			}
-		}
-
-		// call <PrivateImplementationDetails>.ThrowIfNull(ldloc parameterVariable, ldstr "parameterVariable")
-		private bool MatchThrowIfNullCall(ILInstruction instruction, [NotNullWhen(true)] out ILVariable? parameterVariable)
-		{
-			parameterVariable = null;
-			if (instruction is not Call call)
-				return false;
-			if (call.Arguments.Count != 2)
-				return false;
-			if (!call.Method.IsStatic || !call.Method.FullNameIs("<PrivateImplementationDetails>", "ThrowIfNull"))
-				return false;
-			if (!(call.Arguments[0].MatchLdLoc(out parameterVariable) && parameterVariable.Kind == VariableKind.Parameter))
-				return false;
-			return true;
-		}
-	}
-}
diff --git a/ICSharpCode.Decompiler/IL/Transforms/TransformArrayInitializers.cs b/ICSharpCode.Decompiler/IL/Transforms/TransformArrayInitializers.cs
index cc0027b0a..7ddec2e56 100644
--- a/ICSharpCode.Decompiler/IL/Transforms/TransformArrayInitializers.cs
+++ b/ICSharpCode.Decompiler/IL/Transforms/TransformArrayInitializers.cs
@@ -243,7 +243,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms
 
 					while (blob.RemainingBytes > 0)
 					{
-						block.Instructions.Add(StElemPtr(tempStore, blob.Offset, new LdcI4(blob.ReadByte()), elementType));
+						block.Instructions.Add(StElemPtr(tempStore, blob.Offset, ReadElement(ref blob, elementType), elementType));
 					}
 
 					block.FinalInstruction = new LdLoc(tempStore);
@@ -271,13 +271,30 @@ namespace ICSharpCode.Decompiler.IL.Transforms
 			return false;
 		}
 
+		private ILInstruction ReadElement(ref BlobReader blob, IType elementType)
+		{
+			switch (elementType.GetSize())
+			{
+				case 1:
+					return new LdcI4(blob.ReadByte());
+				case 2:
+					return new LdcI4(blob.ReadInt16());
+				case 4:
+					return new LdcI4(blob.ReadInt32());
+				case 8:
+					return new LdcI8(blob.ReadInt64());
+				default:
+					throw new NotSupportedException();
+			}
+		}
+
 		bool HandleCpblkInitializer(Block block, int pos, ILVariable v, long length, out BlobReader blob, out IType elementType)
 		{
 			blob = default;
 			elementType = null;
 			if (!block.Instructions[pos].MatchCpblk(out var dest, out var src, out var size))
 				return false;
-			if (!dest.MatchLdLoc(v) || !src.MatchLdsFlda(out var field) || !size.MatchLdcI4((int)length))
+			if (!dest.MatchLdLoc(v) || !MatchGetStaticFieldAddress(src, out var field) || !size.MatchLdcI4((int)length))
 				return false;
 			if (!(v.IsSingleDefinition && v.LoadCount == 2))
 				return false;
@@ -303,9 +320,11 @@ namespace ICSharpCode.Decompiler.IL.Transforms
 			else if (value is NewObj { Arguments: { Count: 2 } } newObj
 				  && newObj.Method.DeclaringType.IsKnownType(KnownTypeCode.SpanOfT)
 				  && newObj.Arguments[0].MatchLdLoc(v)
-				  && newObj.Arguments[1].MatchLdcI4((int)length))
+				  && newObj.Arguments[1].MatchLdcI4(out var elementCount))
 			{
 				elementType = ((ParameterizedType)newObj.Method.DeclaringType).TypeArguments[0];
+				if (elementCount != length / elementType.GetSize())
+					return false;
 			}
 			else
 			{
@@ -315,7 +334,32 @@ namespace ICSharpCode.Decompiler.IL.Transforms
 			return true;
 		}
 
-		bool HandleSequentialLocAllocInitializer(Block block, int pos, ILVariable store, ILInstruction locAllocInstruction, out IType elementType, out StObj[] values, out int instructionsToRemove)
+		bool MatchGetStaticFieldAddress(ILInstruction input, out IField field)
+		{
+			if (input.MatchLdsFlda(out field))
+				return true;
+			// call get_Item(addressof System.ReadOnlySpan`1[[T]](call CreateSpan(ldmembertoken field)), ldc.i4 0)
+			if (input is not Call { Method.Name: "get_Item", Arguments.Count: 2 } call)
+				return false;
+			if (!call.Method.DeclaringType.IsKnownType(KnownTypeCode.ReadOnlySpanOfT))
+				return false;
+			if (!call.Arguments[1].MatchLdcI4(0))
+				return false;
+			if (call.Arguments[0] is not AddressOf addressOf)
+				return false;
+			if (addressOf.Value is not Call { Method.Name: "CreateSpan", Arguments.Count: 1 } createSpanCall)
+				return false;
+			if (!IsRuntimeHelpers(createSpanCall.Method.DeclaringType))
+				return false;
+			if (!createSpanCall.Arguments[0].MatchLdMemberToken(out var member))
+				return false;
+			field = member as IField;
+			return field != null;
+		}
+
+		static bool IsRuntimeHelpers(IType type) => type is { Name: "RuntimeHelpers", Namespace: "System.Runtime.CompilerServices" };
+
+		unsafe bool HandleSequentialLocAllocInitializer(Block block, int pos, ILVariable store, ILInstruction locAllocInstruction, out IType elementType, out StObj[] values, out int instructionsToRemove)
 		{
 			int elementCount = 0;
 			long minExpectedOffset = 0;
@@ -326,24 +370,60 @@ namespace ICSharpCode.Decompiler.IL.Transforms
 			if (!locAllocInstruction.MatchLocAlloc(out var lengthInstruction))
 				return false;
 
-			if (block.Instructions[pos].MatchInitblk(out var dest, out var value, out var size)
-				&& lengthInstruction.MatchLdcI(out long byteCount))
+			BlobReader blob = default;
+
+			if (lengthInstruction.MatchLdcI(out long byteCount))
 			{
-				if (!dest.MatchLdLoc(store) || !size.MatchLdcI(byteCount))
-					return false;
-				instructionsToRemove++;
-				pos++;
+				if (block.Instructions[pos].MatchInitblk(out var dest, out var value, out var size))
+				{
+					if (!dest.MatchLdLoc(store) || !size.MatchLdcI(byteCount))
+						return false;
+					instructionsToRemove++;
+					pos++;
+				}
+				else if (block.Instructions[pos].MatchCpblk(out dest, out var src, out size))
+				{
+					if (!dest.MatchLdLoc(store) || !size.MatchLdcI(byteCount))
+						return false;
+					if (!MatchGetStaticFieldAddress(src, out var field))
+						return false;
+					var fd = context.PEFile.Metadata.GetFieldDefinition((FieldDefinitionHandle)field.MetadataToken);
+					if (!fd.HasFlag(System.Reflection.FieldAttributes.HasFieldRVA))
+						return false;
+					blob = fd.GetInitialValue(context.PEFile, context.TypeSystem);
+					instructionsToRemove++;
+					pos++;
+				}
 			}
 
 			for (int i = pos; i < block.Instructions.Count; i++)
 			{
 				// match the basic stobj pattern
-				if (!block.Instructions[i].MatchStObj(out ILInstruction target, out value, out var currentType)
+				if (!block.Instructions[i].MatchStObj(out ILInstruction target, out var value, out var currentType)
 					|| value.Descendants.OfType<IInstructionWithVariableOperand>().Any(inst => inst.Variable == store))
 					break;
-				if (elementType != null && !currentType.Equals(elementType))
+				// first
+				if (elementType == null)
+				{
+					elementType = currentType;
+					if (blob.StartPointer != null)
+					{
+						var countInstruction = PointerArithmeticOffset.Detect(lengthInstruction, elementType, checkForOverflow: true);
+						if (countInstruction == null || !countInstruction.MatchLdcI(out long valuesLength) || valuesLength < 1)
+							return false;
+						values = new StObj[(int)valuesLength];
+						int valueIndex = 0;
+						while (blob.RemainingBytes > 0 && valueIndex < values.Length)
+						{
+							values[valueIndex] = StElemPtr(store, blob.Offset, ReadElement(ref blob, elementType), elementType);
+							valueIndex++;
+						}
+					}
+				}
+				else if (!currentType.Equals(elementType))
+				{
 					break;
-				elementType = currentType;
+				}
 				// match the target
 				// should be either ldloc store (at offset 0)
 				// or binary.add(ldloc store, offset) where offset is either 'elementSize' or 'i * elementSize'
@@ -403,7 +483,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms
 			return new StObj(targetInst, storeInstruction.Value, storeInstruction.Type);
 		}
 
-		ILInstruction StElemPtr(ILVariable target, int offset, LdcI4 value, IType type)
+		StObj StElemPtr(ILVariable target, int offset, ILInstruction value, IType type)
 		{
 			var targetInst = offset == 0 ? (ILInstruction)new LdLoc(target) : new BinaryNumericInstruction(
 				BinaryNumericOperator.Add,
@@ -698,12 +778,8 @@ namespace ICSharpCode.Decompiler.IL.Transforms
 			IMethod method = call.Method;
 			if (!method.IsStatic || method.Name != "InitializeArray" || method.DeclaringTypeDefinition == null)
 				return false;
-			var declaringType = method.DeclaringTypeDefinition;
-			if (declaringType.DeclaringType != null || declaringType.Name != "RuntimeHelpers"
-				|| declaringType.Namespace != "System.Runtime.CompilerServices")
-			{
+			if (!IsRuntimeHelpers(method.DeclaringType))
 				return false;
-			}
 			array = call.Arguments[0];
 			if (!call.Arguments[1].MatchLdMemberToken(out var member))
 				return false;
diff --git a/ICSharpCode.Decompiler/IL/Transforms/TransformAssignment.cs b/ICSharpCode.Decompiler/IL/Transforms/TransformAssignment.cs
index a24783d21..b5530fb70 100644
--- a/ICSharpCode.Decompiler/IL/Transforms/TransformAssignment.cs
+++ b/ICSharpCode.Decompiler/IL/Transforms/TransformAssignment.cs
@@ -21,6 +21,7 @@ using System.Diagnostics;
 using System.Linq;
 using System.Linq.Expressions;
 
+using ICSharpCode.Decompiler.CSharp;
 using ICSharpCode.Decompiler.TypeSystem;
 using ICSharpCode.Decompiler.Util;
 
@@ -452,7 +453,17 @@ namespace ICSharpCode.Decompiler.IL.Transforms
 					return false; // for now we only support binary compound assignments
 				if (!targetType.IsKnownType(KnownTypeCode.String))
 					return false;
-				if (!IsMatchingCompoundLoad(concatCall.Arguments[0], compoundStore, out var target, out var targetKind, out var finalizeMatch, forbiddenVariable: storeInSetter?.Variable))
+				var arg = concatCall.Arguments[0];
+				if (arg is Call call && CallBuilder.IsStringToReadOnlySpanCharImplicitConversion(call.Method))
+				{
+					arg = call.Arguments[0];
+					if (!(concatCall.Arguments[1] is NewObj { Arguments: [AddressOf addressOf] } newObj) || !ILInlining.IsReadOnlySpanCharCtor(newObj.Method))
+					{
+						return false;
+					}
+				}
+
+				if (!IsMatchingCompoundLoad(arg, compoundStore, out var target, out var targetKind, out var finalizeMatch, forbiddenVariable: storeInSetter?.Variable))
 					return false;
 				context.Step($"Compound assignment (string concatenation)", compoundStore);
 				finalizeMatch?.Invoke(context);
diff --git a/ICSharpCode.Decompiler/IL/Transforms/UsingTransform.cs b/ICSharpCode.Decompiler/IL/Transforms/UsingTransform.cs
index 8a61d7931..96fe3d13d 100644
--- a/ICSharpCode.Decompiler/IL/Transforms/UsingTransform.cs
+++ b/ICSharpCode.Decompiler/IL/Transforms/UsingTransform.cs
@@ -367,8 +367,19 @@ namespace ICSharpCode.Decompiler.IL.Transforms
 					}
 					disposeCall = cv;
 				}
-				if (disposeCall.Method.FullName != disposeMethodFullName)
+				if (disposeCall.Method.IsStatic)
 					return false;
+				if (disposeTypeCode == KnownTypeCode.IAsyncDisposable)
+				{
+					if (disposeCall.Method.Name != "DisposeAsync")
+						return false;
+				}
+				else
+				{
+					if (disposeCall.Method.FullName != disposeMethodFullName)
+						return false;
+				}
+
 				if (disposeCall.Method.Parameters.Count > 0)
 					return false;
 				if (disposeCall.Arguments.Count != 1)
@@ -505,9 +516,14 @@ namespace ICSharpCode.Decompiler.IL.Transforms
 				return false;
 			if (!awaitInstruction.MatchAwait(out var arg))
 				return false;
-			if (!arg.MatchAddressOf(out awaitInstruction, out var type))
-				return false;
-			// TODO check type: does it match the structural 'Awaitable' pattern?
+			if (arg.MatchAddressOf(out var awaitInstructionInAddressOf, out var type))
+			{
+				awaitInstruction = awaitInstructionInAddressOf;
+			}
+			else
+			{
+				awaitInstruction = arg;
+			}
 			return true;
 		}
 	}
diff --git a/ICSharpCode.Decompiler/Semantics/ByReferenceResolveResult.cs b/ICSharpCode.Decompiler/Semantics/ByReferenceResolveResult.cs
index 215aaa369..3c4828d4f 100644
--- a/ICSharpCode.Decompiler/Semantics/ByReferenceResolveResult.cs
+++ b/ICSharpCode.Decompiler/Semantics/ByReferenceResolveResult.cs
@@ -30,9 +30,6 @@ namespace ICSharpCode.Decompiler.Semantics
 	public class ByReferenceResolveResult : ResolveResult
 	{
 		public ReferenceKind ReferenceKind { get; }
-		public bool IsOut => ReferenceKind == ReferenceKind.Out;
-		public bool IsRef => ReferenceKind == ReferenceKind.Ref;
-		public bool IsIn => ReferenceKind == ReferenceKind.In;
 
 		public readonly ResolveResult ElementResult;
 
diff --git a/ICSharpCode.Decompiler/TypeSystem/ApplyAttributeTypeVisitor.cs b/ICSharpCode.Decompiler/TypeSystem/ApplyAttributeTypeVisitor.cs
index 0ad298513..76418f3b2 100644
--- a/ICSharpCode.Decompiler/TypeSystem/ApplyAttributeTypeVisitor.cs
+++ b/ICSharpCode.Decompiler/TypeSystem/ApplyAttributeTypeVisitor.cs
@@ -21,6 +21,7 @@ using System.Collections.Immutable;
 using System.Diagnostics;
 using System.Linq;
 
+using ICSharpCode.Decompiler.DebugInfo;
 using ICSharpCode.Decompiler.TypeSystem.Implementation;
 using ICSharpCode.Decompiler.Util;
 
@@ -163,6 +164,13 @@ namespace ICSharpCode.Decompiler.TypeSystem
 			}
 		}
 
+		public static IType ApplyAttributesToType(IType inputType, ICompilation compilation, TypeSystemOptions options, PdbExtraTypeInfo pdbExtraTypeInfo)
+		{
+			if (pdbExtraTypeInfo.DynamicFlags is null && pdbExtraTypeInfo.TupleElementNames is null)
+				return inputType;
+			return inputType.AcceptVisitor(new ApplyAttributeTypeVisitor(compilation, pdbExtraTypeInfo.DynamicFlags != null, pdbExtraTypeInfo.DynamicFlags, false, null, options, pdbExtraTypeInfo.TupleElementNames, Nullability.Oblivious, null));
+		}
+
 		readonly ICompilation compilation;
 		readonly bool hasDynamicAttribute;
 		readonly bool[] dynamicAttributeData;
@@ -343,6 +351,7 @@ namespace ICSharpCode.Decompiler.TypeSystem
 					ReferenceKind.Ref => 1,
 					ReferenceKind.Out => 2, // in/out also count the modreq
 					ReferenceKind.In => 2,
+					ReferenceKind.RefReadOnly => 2, // counts the modopt
 					_ => throw new NotSupportedException()
 				};
 				parameters[i] = type.ParameterTypes[i].AcceptVisitor(this);
diff --git a/ICSharpCode.Decompiler/TypeSystem/DecompilerTypeSystem.cs b/ICSharpCode.Decompiler/TypeSystem/DecompilerTypeSystem.cs
index 536b00cac..896f9e802 100644
--- a/ICSharpCode.Decompiler/TypeSystem/DecompilerTypeSystem.cs
+++ b/ICSharpCode.Decompiler/TypeSystem/DecompilerTypeSystem.cs
@@ -124,8 +124,6 @@ namespace ICSharpCode.Decompiler.TypeSystem
 		/// will be reported as custom attribute.
 		/// </summary>
 		ScopedRef = 0x4000,
-		[Obsolete("Use ScopedRef instead")]
-		LifetimeAnnotations = ScopedRef,
 		/// <summary>
 		/// Replace 'IntPtr' types with the 'nint' type even in absence of [NativeIntegerAttribute].
 		/// Note: DecompilerTypeSystem constructor removes this setting from the options if
@@ -133,11 +131,19 @@ namespace ICSharpCode.Decompiler.TypeSystem
 		/// </summary>
 		NativeIntegersWithoutAttribute = 0x8000,
 		/// <summary>
+		/// If this option is active, [RequiresLocationAttribute] on parameters is removed
+		/// and parameters are marked as ref readonly.
+		/// Otherwise, the attribute is preserved but the parameters are not marked
+		/// as if it was a ref parameter without any attributes.
+		/// </summary>
+		RefReadOnlyParameters = 0x10000,
+		/// <summary>
 		/// Default settings: typical options for the decompiler, with all C# languages features enabled.
 		/// </summary>
 		Default = Dynamic | Tuple | ExtensionMethods | DecimalConstants | ReadOnlyStructsAndParameters
 			| RefStructs | UnmanagedConstraints | NullabilityAnnotations | ReadOnlyMethods
 			| NativeIntegers | FunctionPointers | ScopedRef | NativeIntegersWithoutAttribute
+			| RefReadOnlyParameters
 	}
 
 	/// <summary>
@@ -177,6 +183,8 @@ namespace ICSharpCode.Decompiler.TypeSystem
 				typeSystemOptions |= TypeSystemOptions.ScopedRef;
 			if (settings.NumericIntPtr)
 				typeSystemOptions |= TypeSystemOptions.NativeIntegersWithoutAttribute;
+			if (settings.RefReadOnlyParameters)
+				typeSystemOptions |= TypeSystemOptions.RefReadOnlyParameters;
 			return typeSystemOptions;
 		}
 
diff --git a/ICSharpCode.Decompiler/TypeSystem/FunctionPointerType.cs b/ICSharpCode.Decompiler/TypeSystem/FunctionPointerType.cs
index c2589db4d..5d71478fd 100644
--- a/ICSharpCode.Decompiler/TypeSystem/FunctionPointerType.cs
+++ b/ICSharpCode.Decompiler/TypeSystem/FunctionPointerType.cs
@@ -58,17 +58,22 @@ namespace ICSharpCode.Decompiler.TypeSystem
 			{
 				IType paramType = p;
 				ReferenceKind kind = ReferenceKind.None;
-				if (p is ModifiedType modreq)
+				if (p is ModifiedType mod)
 				{
-					if (modreq.Modifier.IsKnownType(KnownAttribute.In))
+					if (mod.Modifier.IsKnownType(KnownAttribute.In))
 					{
 						kind = ReferenceKind.In;
-						paramType = modreq.ElementType;
+						paramType = mod.ElementType;
 					}
-					else if (modreq.Modifier.IsKnownType(KnownAttribute.Out))
+					else if (mod.Modifier.IsKnownType(KnownAttribute.Out))
 					{
 						kind = ReferenceKind.Out;
-						paramType = modreq.ElementType;
+						paramType = mod.ElementType;
+					}
+					else if (mod.Modifier.IsKnownType(KnownAttribute.RequiresLocation))
+					{
+						kind = ReferenceKind.RefReadOnly;
+						paramType = mod.ElementType;
 					}
 				}
 				if (paramType.Kind == TypeKind.ByReference)
diff --git a/ICSharpCode.Decompiler/TypeSystem/IParameter.cs b/ICSharpCode.Decompiler/TypeSystem/IParameter.cs
index aee0078a9..481613318 100644
--- a/ICSharpCode.Decompiler/TypeSystem/IParameter.cs
+++ b/ICSharpCode.Decompiler/TypeSystem/IParameter.cs
@@ -32,7 +32,8 @@ namespace ICSharpCode.Decompiler.TypeSystem
 		None,
 		Out,
 		Ref,
-		In
+		In,
+		RefReadOnly,
 	}
 
 	public struct LifetimeAnnotation
@@ -71,21 +72,6 @@ namespace ICSharpCode.Decompiler.TypeSystem
 		/// </summary>
 		LifetimeAnnotation Lifetime { get; }
 
-		/// <summary>
-		/// Gets whether this parameter is a C# 'ref' parameter.
-		/// </summary>
-		bool IsRef { get; }
-
-		/// <summary>
-		/// Gets whether this parameter is a C# 'out' parameter.
-		/// </summary>
-		bool IsOut { get; }
-
-		/// <summary>
-		/// Gets whether this parameter is a C# 'in' parameter.
-		/// </summary>
-		bool IsIn { get; }
-
 		/// <summary>
 		/// Gets whether this parameter is a C# 'params' parameter.
 		/// </summary>
diff --git a/ICSharpCode.Decompiler/TypeSystem/Implementation/AttributeListBuilder.cs b/ICSharpCode.Decompiler/TypeSystem/Implementation/AttributeListBuilder.cs
index cea813250..66d77a735 100644
--- a/ICSharpCode.Decompiler/TypeSystem/Implementation/AttributeListBuilder.cs
+++ b/ICSharpCode.Decompiler/TypeSystem/Implementation/AttributeListBuilder.cs
@@ -255,6 +255,9 @@ namespace ICSharpCode.Decompiler.TypeSystem.Implementation
 						case "ScopedRefAttribute":
 							return (options & TypeSystemOptions.ScopedRef) != 0
 								&& (target == SymbolKind.Parameter);
+						case "RequiresLocationAttribute":
+							return (options & TypeSystemOptions.RefReadOnlyParameters) != 0
+								&& (target == SymbolKind.Parameter);
 						default:
 							return false;
 					}
diff --git a/ICSharpCode.Decompiler/TypeSystem/Implementation/DefaultParameter.cs b/ICSharpCode.Decompiler/TypeSystem/Implementation/DefaultParameter.cs
index e3a1a8a25..4fa30a818 100644
--- a/ICSharpCode.Decompiler/TypeSystem/Implementation/DefaultParameter.cs
+++ b/ICSharpCode.Decompiler/TypeSystem/Implementation/DefaultParameter.cs
@@ -76,9 +76,6 @@ namespace ICSharpCode.Decompiler.TypeSystem.Implementation
 		public IEnumerable<IAttribute> GetAttributes() => attributes;
 
 		public ReferenceKind ReferenceKind => referenceKind;
-		public bool IsRef => referenceKind == ReferenceKind.Ref;
-		public bool IsOut => referenceKind == ReferenceKind.Out;
-		public bool IsIn => referenceKind == ReferenceKind.In;
 
 		public bool IsParams => isParams;
 
@@ -115,12 +112,14 @@ namespace ICSharpCode.Decompiler.TypeSystem.Implementation
 		public static string ToString(IParameter parameter)
 		{
 			StringBuilder b = new StringBuilder();
-			if (parameter.IsRef)
-				b.Append("ref ");
-			if (parameter.IsOut)
-				b.Append("out ");
-			if (parameter.IsIn)
-				b.Append("in ");
+			b.Append(parameter.ReferenceKind switch {
+				ReferenceKind.None => "",
+				ReferenceKind.Ref => "ref ",
+				ReferenceKind.Out => "out ",
+				ReferenceKind.In => "in ",
+				ReferenceKind.RefReadOnly => "ref readonly ",
+				_ => throw new NotSupportedException()
+			});
 			if (parameter.IsParams)
 				b.Append("params ");
 			b.Append(parameter.Name);
diff --git a/ICSharpCode.Decompiler/TypeSystem/Implementation/KnownAttributes.cs b/ICSharpCode.Decompiler/TypeSystem/Implementation/KnownAttributes.cs
index 59dfc5863..59d2c19eb 100644
--- a/ICSharpCode.Decompiler/TypeSystem/Implementation/KnownAttributes.cs
+++ b/ICSharpCode.Decompiler/TypeSystem/Implementation/KnownAttributes.cs
@@ -95,6 +95,7 @@ namespace ICSharpCode.Decompiler.TypeSystem
 		CallerFilePath,
 		CallerLineNumber,
 		ScopedRef,
+		RequiresLocation,
 
 		// Type parameter attributes:
 		IsUnmanaged,
@@ -173,6 +174,7 @@ namespace ICSharpCode.Decompiler.TypeSystem
 			new TopLevelTypeName("System.Runtime.CompilerServices", nameof(CallerFilePathAttribute)),
 			new TopLevelTypeName("System.Runtime.CompilerServices", nameof(CallerLineNumberAttribute)),
 			new TopLevelTypeName("System.Runtime.CompilerServices", "ScopedRefAttribute"),
+			new TopLevelTypeName("System.Runtime.CompilerServices", "RequiresLocationAttribute"),
 			// Type parameter attributes:
 			new TopLevelTypeName("System.Runtime.CompilerServices", "IsUnmanagedAttribute"),
 			// Marshalling attributes:
diff --git a/ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataParameter.cs b/ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataParameter.cs
index c2cba9824..14bee08dc 100644
--- a/ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataParameter.cs
+++ b/ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataParameter.cs
@@ -21,7 +21,6 @@ using System.Collections.Generic;
 using System.Reflection;
 using System.Reflection.Metadata;
 using System.Reflection.Metadata.Ecma335;
-using System.Text;
 
 using ICSharpCode.Decompiler.Util;
 
@@ -64,7 +63,7 @@ namespace ICSharpCode.Decompiler.TypeSystem.Implementation
 			var metadata = module.metadata;
 			var parameter = metadata.GetParameter(handle);
 
-			bool defaultValueAssignmentAllowed = ReferenceKind is ReferenceKind.None or ReferenceKind.In;
+			bool defaultValueAssignmentAllowed = ReferenceKind is ReferenceKind.None or ReferenceKind.In or ReferenceKind.RefReadOnly;
 
 			if (IsOptional && (!defaultValueAssignmentAllowed || !HasConstantValueInSignature))
 			{
@@ -76,13 +75,10 @@ namespace ICSharpCode.Decompiler.TypeSystem.Implementation
 				b.Add(KnownAttribute.DefaultParameterValue, KnownTypeCode.Object, GetConstantValue(throwOnInvalidMetadata: false));
 			}
 
-			if (!IsOut && !IsIn)
-			{
-				if ((attributes & ParameterAttributes.In) == ParameterAttributes.In)
-					b.Add(KnownAttribute.In);
-				if ((attributes & ParameterAttributes.Out) == ParameterAttributes.Out)
-					b.Add(KnownAttribute.Out);
-			}
+			if ((attributes & ParameterAttributes.In) == ParameterAttributes.In && ReferenceKind is not (ReferenceKind.In or ReferenceKind.RefReadOnly))
+				b.Add(KnownAttribute.In);
+			if ((attributes & ParameterAttributes.Out) == ParameterAttributes.Out && ReferenceKind != ReferenceKind.Out)
+				b.Add(KnownAttribute.Out);
 			b.Add(parameter.GetCustomAttributes(), SymbolKind.Parameter);
 			b.AddMarshalInfo(parameter.GetMarshallingDescriptor());
 
@@ -93,9 +89,6 @@ namespace ICSharpCode.Decompiler.TypeSystem.Implementation
 		const ParameterAttributes inOut = ParameterAttributes.In | ParameterAttributes.Out;
 
 		public ReferenceKind ReferenceKind => DetectRefKind();
-		public bool IsRef => DetectRefKind() == ReferenceKind.Ref;
-		public bool IsOut => Type.Kind == TypeKind.ByReference && (attributes & inOut) == ParameterAttributes.Out;
-		public bool IsIn => DetectRefKind() == ReferenceKind.In;
 
 		public bool IsOptional => (attributes & ParameterAttributes.Optional) != 0;
 
@@ -112,6 +105,14 @@ namespace ICSharpCode.Decompiler.TypeSystem.Implementation
 				if (parameterDef.GetCustomAttributes().HasKnownAttribute(metadata, KnownAttribute.IsReadOnly))
 					return ReferenceKind.In;
 			}
+			if ((module.TypeSystemOptions & TypeSystemOptions.RefReadOnlyParameters) != 0
+				&& (attributes & inOut) == ParameterAttributes.In)
+			{
+				var metadata = module.metadata;
+				var parameterDef = metadata.GetParameter(handle);
+				if (parameterDef.GetCustomAttributes().HasKnownAttribute(metadata, KnownAttribute.RequiresLocation))
+					return ReferenceKind.RefReadOnly;
+			}
 			return ReferenceKind.Ref;
 		}
 
diff --git a/ICSharpCode.Decompiler/TypeSystem/Implementation/SpecializedParameter.cs b/ICSharpCode.Decompiler/TypeSystem/Implementation/SpecializedParameter.cs
index a05340d3a..0a68894a6 100644
--- a/ICSharpCode.Decompiler/TypeSystem/Implementation/SpecializedParameter.cs
+++ b/ICSharpCode.Decompiler/TypeSystem/Implementation/SpecializedParameter.cs
@@ -37,9 +37,6 @@ namespace ICSharpCode.Decompiler.TypeSystem.Implementation
 
 		IEnumerable<IAttribute> IParameter.GetAttributes() => baseParameter.GetAttributes();
 		ReferenceKind IParameter.ReferenceKind => baseParameter.ReferenceKind;
-		bool IParameter.IsRef => baseParameter.IsRef;
-		bool IParameter.IsOut => baseParameter.IsOut;
-		bool IParameter.IsIn => baseParameter.IsIn;
 		bool IParameter.IsParams => baseParameter.IsParams;
 		bool IParameter.IsOptional => baseParameter.IsOptional;
 		bool IParameter.HasConstantValueInSignature => baseParameter.HasConstantValueInSignature;
diff --git a/ICSharpCode.ILSpyX/Analyzers/AnalyzerContext.cs b/ICSharpCode.ILSpyX/Analyzers/AnalyzerContext.cs
index 09112056f..1c339ef0a 100644
--- a/ICSharpCode.ILSpyX/Analyzers/AnalyzerContext.cs
+++ b/ICSharpCode.ILSpyX/Analyzers/AnalyzerContext.cs
@@ -34,49 +34,49 @@ namespace ICSharpCode.ILSpyX.Analyzers
 	{
 		public required AssemblyList AssemblyList { get; init; }
 
-	/// <summary>
-	/// CancellationToken. Currently Analyzers do not support cancellation from the UI, but it should be checked nonetheless.
-	/// </summary>
-	public CancellationToken CancellationToken { get; init; }
+		/// <summary>
+		/// CancellationToken. Currently Analyzers do not support cancellation from the UI, but it should be checked nonetheless.
+		/// </summary>
+		public CancellationToken CancellationToken { get; init; }
 
-	/// <summary>
-	/// Currently used language.
-	/// </summary>
-	public required ILanguage Language { get; init; }
+		/// <summary>
+		/// Currently used language.
+		/// </summary>
+		public required ILanguage Language { get; init; }
 
-/// <summary>
-/// Allows the analyzer to control whether the tree nodes will be sorted.
-/// Must be set within <see cref="IAnalyzer.Analyze(ISymbol, AnalyzerContext)"/>
-/// before the results are enumerated.
-/// </summary>
-public bool SortResults { get; set; }
+		/// <summary>
+		/// Allows the analyzer to control whether the tree nodes will be sorted.
+		/// Must be set within <see cref="IAnalyzer.Analyze(ISymbol, AnalyzerContext)"/>
+		/// before the results are enumerated.
+		/// </summary>
+		public bool SortResults { get; set; }
 
-public MethodBodyBlock? GetMethodBody(IMethod method)
-{
-	if (!method.HasBody || method.MetadataToken.IsNil || method.ParentModule?.MetadataFile == null)
-		return null;
-	var module = method.ParentModule.MetadataFile;
-	var md = module.Metadata.GetMethodDefinition((MethodDefinitionHandle)method.MetadataToken);
-	try
-	{
-		return module.GetMethodBody(md.RelativeVirtualAddress);
-	}
-	catch (BadImageFormatException)
-	{
-		return null;
-	}
-}
+		public MethodBodyBlock? GetMethodBody(IMethod method)
+		{
+			if (!method.HasBody || method.MetadataToken.IsNil || method.ParentModule?.MetadataFile == null)
+				return null;
+			var module = method.ParentModule.MetadataFile;
+			var md = module.Metadata.GetMethodDefinition((MethodDefinitionHandle)method.MetadataToken);
+			try
+			{
+				return module.GetMethodBody(md.RelativeVirtualAddress);
+			}
+			catch (BadImageFormatException)
+			{
+				return null;
+			}
+		}
 
-public AnalyzerScope GetScopeOf(IEntity entity)
-{
-	return new AnalyzerScope(AssemblyList, entity);
-}
+		public AnalyzerScope GetScopeOf(IEntity entity)
+		{
+			return new AnalyzerScope(AssemblyList, entity);
+		}
 
-readonly ConcurrentDictionary<MetadataFile, DecompilerTypeSystem> typeSystemCache = new();
+		readonly ConcurrentDictionary<MetadataFile, DecompilerTypeSystem> typeSystemCache = new();
 
-public DecompilerTypeSystem GetOrCreateTypeSystem(MetadataFile module)
-{
-	return typeSystemCache.GetOrAdd(module, m => new DecompilerTypeSystem(m, m.GetAssemblyResolver()));
-}
+		public DecompilerTypeSystem GetOrCreateTypeSystem(MetadataFile module)
+		{
+			return typeSystemCache.GetOrAdd(module, m => new DecompilerTypeSystem(m, m.GetAssemblyResolver()));
+		}
 	}
 }
diff --git a/ICSharpCode.ILSpyX/Analyzers/ExportAnalyzerAttribute.cs b/ICSharpCode.ILSpyX/Analyzers/ExportAnalyzerAttribute.cs
index fbd4b3a72..628b41bc8 100644
--- a/ICSharpCode.ILSpyX/Analyzers/ExportAnalyzerAttribute.cs
+++ b/ICSharpCode.ILSpyX/Analyzers/ExportAnalyzerAttribute.cs
@@ -17,7 +17,10 @@
 // DEALINGS IN THE SOFTWARE.
 
 using System;
+using System.Collections.Generic;
 using System.ComponentModel.Composition;
+using System.Linq;
+using System.Reflection;
 
 namespace ICSharpCode.ILSpyX.Analyzers
 {
@@ -31,5 +34,16 @@ namespace ICSharpCode.ILSpyX.Analyzers
 		public required string Header { get; init; }
 
 		public int Order { get; set; }
+
+		public static IEnumerable<(ExportAnalyzerAttribute AttributeData, Type AnalyzerType)> GetAnnotatedAnalyzers()
+		{
+			foreach (var type in typeof(ExportAnalyzerAttribute).Assembly.GetTypes())
+			{
+				if (type.GetCustomAttribute(typeof(ExportAnalyzerAttribute), false) is ExportAnalyzerAttribute exportAnalyzerAttribute)
+				{
+					yield return (exportAnalyzerAttribute, type);
+				}
+			}
+		}
 	}
 }
diff --git a/ILSpy.Tests/Analyzers/ExportAnalyzerAttributeTests.cs b/ILSpy.Tests/Analyzers/ExportAnalyzerAttributeTests.cs
new file mode 100644
index 000000000..c8e3af599
--- /dev/null
+++ b/ILSpy.Tests/Analyzers/ExportAnalyzerAttributeTests.cs
@@ -0,0 +1,43 @@
+// Copyright (c) 2024 Andreas Weizel
+// 
+// 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.
+
+using System.Linq;
+
+using ICSharpCode.ILSpyX.Analyzers;
+
+using NUnit.Framework;
+
+namespace ICSharpCode.ILSpy.Tests.Analyzers
+{
+	[TestFixture]
+	public class ExportAnalyzerAttributeTests
+	{
+		[Test]
+		public void CollectAnalyzers()
+		{
+			var analyzerNames = ExportAnalyzerAttribute.GetAnnotatedAnalyzers()
+				.Select(analyzer => analyzer.AnalyzerType.Name)
+				.ToArray();
+			Assert.That(analyzerNames.Contains("AttributeAppliedToAnalyzer"));
+			Assert.That(analyzerNames.Contains("EventImplementedByAnalyzer"));
+			Assert.That(analyzerNames.Contains("MethodUsedByAnalyzer"));
+			Assert.That(analyzerNames.Contains("PropertyOverriddenByAnalyzer"));
+			Assert.That(analyzerNames.Contains("TypeInstantiatedByAnalyzer"));
+		}
+	}
+}
diff --git a/ILSpy.Tests/ILSpy.Tests.csproj b/ILSpy.Tests/ILSpy.Tests.csproj
index 17337e7b2..0f44621f6 100644
--- a/ILSpy.Tests/ILSpy.Tests.csproj
+++ b/ILSpy.Tests/ILSpy.Tests.csproj
@@ -34,6 +34,7 @@
   </PropertyGroup>
 
   <ItemGroup>
+    <Compile Include="Analyzers\ExportAnalyzerAttributeTests.cs" />
     <Compile Include="Analyzers\AnalyzerScopeTests.cs" />
     <Compile Include="Analyzers\MemberImplementsInterfaceAnalyzerTests.cs" />
     <Compile Include="Analyzers\MethodUsesAnalyzerTests.cs" />
diff --git a/ILSpy/Languages/CSharpLanguage.cs b/ILSpy/Languages/CSharpLanguage.cs
index a7fc1cb55..212d4f870 100644
--- a/ILSpy/Languages/CSharpLanguage.cs
+++ b/ILSpy/Languages/CSharpLanguage.cs
@@ -114,6 +114,7 @@ namespace ICSharpCode.ILSpy
 						new LanguageVersion(Decompiler.CSharp.LanguageVersion.CSharp9_0.ToString(), "C# 9.0 / VS 2019.8"),
 						new LanguageVersion(Decompiler.CSharp.LanguageVersion.CSharp10_0.ToString(), "C# 10.0 / VS 2022"),
 						new LanguageVersion(Decompiler.CSharp.LanguageVersion.CSharp11_0.ToString(), "C# 11.0 / VS 2022.4"),
+						new LanguageVersion(Decompiler.CSharp.LanguageVersion.CSharp12_0.ToString(), "C# 12.0 / VS 2022.8"),
 					};
 				}
 				return versions;
diff --git a/ILSpy/Properties/Resources.Designer.cs b/ILSpy/Properties/Resources.Designer.cs
index 1866a7b29..54ec14ad5 100644
--- a/ILSpy/Properties/Resources.Designer.cs
+++ b/ILSpy/Properties/Resources.Designer.cs
@@ -1154,15 +1154,6 @@ namespace ICSharpCode.ILSpy.Properties {
             }
         }
         
-        /// <summary>
-        ///   Looks up a localized string similar to Use parameter null checking.
-        /// </summary>
-        public static string DecompilerSettings_ParameterNullCheck {
-            get {
-                return ResourceManager.GetString("DecompilerSettings.ParameterNullCheck", resourceCulture);
-            }
-        }
-        
         /// <summary>
         ///   Looks up a localized string similar to Pattern combinators (and, or, not).
         /// </summary>
@@ -1235,6 +1226,15 @@ namespace ICSharpCode.ILSpy.Properties {
             }
         }
         
+        /// <summary>
+        ///   Looks up a localized string similar to &apos;ref readonly&apos; parameters.
+        /// </summary>
+        public static string DecompilerSettings_RefReadOnlyParameters {
+            get {
+                return ResourceManager.GetString("DecompilerSettings.RefReadOnlyParameters", resourceCulture);
+            }
+        }
+        
         /// <summary>
         ///   Looks up a localized string similar to Relational patterns.
         /// </summary>
diff --git a/ILSpy/Properties/Resources.resx b/ILSpy/Properties/Resources.resx
index cc08fca35..096c9d74b 100644
--- a/ILSpy/Properties/Resources.resx
+++ b/ILSpy/Properties/Resources.resx
@@ -408,9 +408,6 @@ Are you sure you want to continue?</value>
   <data name="DecompilerSettings.Other" xml:space="preserve">
     <value>Other</value>
   </data>
-  <data name="DecompilerSettings.ParameterNullCheck" xml:space="preserve">
-    <value>Use parameter null checking</value>
-  </data>
   <data name="DecompilerSettings.PatternCombinators" xml:space="preserve">
     <value>Pattern combinators (and, or, not)</value>
   </data>
@@ -435,6 +432,9 @@ Are you sure you want to continue?</value>
   <data name="DecompilerSettings.RecursivePatternMatching" xml:space="preserve">
     <value>Recursive pattern matching</value>
   </data>
+  <data name="DecompilerSettings.RefReadOnlyParameters" xml:space="preserve">
+    <value>'ref readonly' parameters</value>
+  </data>
   <data name="DecompilerSettings.RelationalPatterns" xml:space="preserve">
     <value>Relational patterns</value>
   </data>
diff --git a/ILSpy/Properties/Resources.zh-Hans.resx b/ILSpy/Properties/Resources.zh-Hans.resx
index 0304ce446..8e46de32e 100644
--- a/ILSpy/Properties/Resources.zh-Hans.resx
+++ b/ILSpy/Properties/Resources.zh-Hans.resx
@@ -399,9 +399,6 @@
   <data name="DecompilerSettings.Other" xml:space="preserve">
     <value>其他</value>
   </data>
-  <data name="DecompilerSettings.ParameterNullCheck" xml:space="preserve">
-    <value>使用方法参数非空校验</value>
-  </data>
   <data name="DecompilerSettings.PatternMatching" xml:space="preserve">
     <value>使用模式匹配表达式</value>
   </data>