From 284a68fcb7f7ce9abc5a0b5883c4c3a80eb1dbca Mon Sep 17 00:00:00 2001 From: Sebastien Lebreton Date: Sat, 18 Jul 2026 22:59:40 +0200 Subject: [PATCH 1/2] Model scoped parameter lifetimes ScopedRefAttribute only records explicit syntax. Effective lifetime also depends on UnscopedRefAttribute, params collections, out parameters, and the defining module's RefSafetyRules version. Model those distinctions in the type system without changing decompiler output. Assisted-by: Copilot:gpt-5.6-sol:GitHub Copilot CLI Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 86d2918e-5a24-48b4-9a86-41d331ec3720 --- .../TypeSystem/TypeSystemLoaderTests.cs | 81 +++++++++++++++++++ .../TypeSystem/TypeSystemTestCase.cs | 29 +++++++ .../TypeSystem/IParameter.cs | 43 +++++++++- .../Implementation/KnownAttributes.cs | 9 ++- .../Implementation/MetadataParameter.cs | 30 ++++--- .../TypeSystem/MetadataModule.cs | 38 +++++++++ .../TypeSystem/TypeSystemExtensions.cs | 59 ++++++++++++++ 7 files changed, 275 insertions(+), 14 deletions(-) diff --git a/ICSharpCode.Decompiler.Tests/TypeSystem/TypeSystemLoaderTests.cs b/ICSharpCode.Decompiler.Tests/TypeSystem/TypeSystemLoaderTests.cs index e98991a378..1554bb069f 100644 --- a/ICSharpCode.Decompiler.Tests/TypeSystem/TypeSystemLoaderTests.cs +++ b/ICSharpCode.Decompiler.Tests/TypeSystem/TypeSystemLoaderTests.cs @@ -196,6 +196,87 @@ public void DynamicParameterHasNoAttributes() Assert.That(m1.Parameters[0].GetAttributes().Count(), Is.EqualTo(0)); } + [Test] + public void LifetimeAnnotations() + { + ITypeDefinition type = GetTypeDefinition(typeof(LifetimeAnnotationTests)); + + IParameter explicitScopedRef = type.Methods.Single(m => m.Name == "ExplicitScopedRef").Parameters.Single(); + Assert.That(explicitScopedRef.Lifetime.DeclaredScope, Is.EqualTo(ScopedKind.ScopedRef)); + Assert.That(explicitScopedRef.GetEffectiveScope(), Is.EqualTo(ScopedKind.ScopedRef)); + Assert.That(explicitScopedRef.Lifetime.HasUnscopedRefAttribute, Is.False); + Assert.That(explicitScopedRef.Lifetime.ScopedRef, Is.True); + + IParameter explicitScopedValue = type.Methods.Single(m => m.Name == "ExplicitScopedValue").Parameters.Single(); + Assert.That(explicitScopedValue.Lifetime.DeclaredScope, Is.EqualTo(ScopedKind.ScopedValue)); + Assert.That(explicitScopedValue.GetEffectiveScope(), Is.EqualTo(ScopedKind.ScopedValue)); + Assert.That(explicitScopedValue.Lifetime.HasUnscopedRefAttribute, Is.False); + Assert.That(explicitScopedValue.Lifetime.ScopedRef, Is.True); + + IParameter implicitScopedRef = type.Methods.Single(m => m.Name == "ImplicitScopedRef").Parameters.Single(); + Assert.That(implicitScopedRef.Lifetime.DeclaredScope, Is.EqualTo(ScopedKind.None)); + Assert.That(implicitScopedRef.GetEffectiveScope(), Is.EqualTo(ScopedKind.ScopedRef)); + Assert.That(implicitScopedRef.Lifetime.HasUnscopedRefAttribute, Is.False); + Assert.That(implicitScopedRef.Lifetime.ScopedRef, Is.False); + + IParameter unscopedRef = type.Methods.Single(m => m.Name == "UnscopedRef").Parameters.Single(); + Assert.That(unscopedRef.Lifetime.DeclaredScope, Is.EqualTo(ScopedKind.None)); + Assert.That(unscopedRef.GetEffectiveScope(), Is.EqualTo(ScopedKind.None)); + Assert.That(unscopedRef.Lifetime.HasUnscopedRefAttribute, Is.True); + Assert.That(unscopedRef.GetAttributes().Any(a => + a.AttributeType.FullName == "System.Diagnostics.CodeAnalysis.UnscopedRefAttribute"), Is.True); + + var paramsRefStruct = new DefaultParameter(type, "values", isParams: true); + Assert.That(paramsRefStruct.GetEffectiveScope(), Is.EqualTo(ScopedKind.ScopedValue)); + + IMethod instanceMethod = type.Methods.Single(m => m.Name == "InstanceMethod"); + Assert.That(instanceMethod.GetThisParameterScope(), Is.EqualTo(ScopedKind.ScopedRef)); + Assert.That(instanceMethod.HasAttribute(KnownAttribute.UnscopedRef), Is.False); + + IMethod unscopedMethod = type.Methods.Single(m => m.Name == "UnscopedMethod"); + Assert.That(unscopedMethod.GetThisParameterScope(), Is.EqualTo(ScopedKind.None)); + Assert.That(unscopedMethod.HasAttribute(KnownAttribute.UnscopedRef), Is.True); + + IProperty unscopedProperty = type.Properties.Single(p => p.Name == "UnscopedProperty"); + Assert.That(unscopedProperty.Getter.GetThisParameterScope(), Is.EqualTo(ScopedKind.None)); + Assert.That(unscopedProperty.HasAttribute(KnownAttribute.UnscopedRef), Is.True); + + IProperty accessorUnscopedProperty = type.Properties.Single(p => p.Name == "AccessorUnscopedProperty"); + Assert.That(accessorUnscopedProperty.Getter.GetThisParameterScope(), Is.EqualTo(ScopedKind.None)); + Assert.That(accessorUnscopedProperty.Getter.HasAttribute(KnownAttribute.UnscopedRef), Is.True); + + IMethod staticMethod = type.Methods.Single(m => m.Name == "ExplicitScopedRef"); + Assert.That(staticMethod.GetThisParameterScope(), Is.EqualTo(ScopedKind.None)); + + IMethod classMethod = GetTypeDefinition(typeof(SimplePublicClass)).Methods.Single(m => m.Name == "Method"); + Assert.That(classMethod.GetThisParameterScope(), Is.EqualTo(ScopedKind.None)); + + // The legacy mscorlib fixture has no RefSafetyRulesAttribute, so out is not implicitly scoped. + ITypeDefinition int32 = compilation.FindType(KnownTypeCode.Int32).GetDefinition(); + IMethod legacyTryParse = int32.Methods.Single(m => m.Name == "TryParse" + && m.Parameters.Count == 2 + && m.Parameters[1].ReferenceKind == ReferenceKind.Out); + Assert.That(legacyTryParse.Parameters[1].GetEffectiveScope(), Is.EqualTo(ScopedKind.None)); + } + + [Test] + public void LifetimeAnnotationsCanBeDisabled() + { + var compilationWithoutLifetimeAnnotations = new SimpleCompilation( + TestAssembly.WithOptions(TypeSystemOptions.Default & ~TypeSystemOptions.ScopedRef), + Mscorlib.WithOptions(TypeSystemOptions.Default | TypeSystemOptions.OnlyPublicAPI)); + ITypeDefinition type = compilationWithoutLifetimeAnnotations.FindType(typeof(LifetimeAnnotationTests)).GetDefinition(); + + IParameter explicitScopedRef = type.Methods.Single(m => m.Name == "ExplicitScopedRef").Parameters.Single(); + Assert.That(explicitScopedRef.Lifetime.DeclaredScope, Is.EqualTo(ScopedKind.None)); + Assert.That(explicitScopedRef.GetEffectiveScope(), Is.EqualTo(ScopedKind.None)); + Assert.That(explicitScopedRef.GetAttributes().Any(a => + a.AttributeType.FullName == "System.Runtime.CompilerServices.ScopedRefAttribute"), Is.True); + + IMethod instanceMethod = type.Methods.Single(m => m.Name == "InstanceMethod"); + Assert.That(instanceMethod.GetThisParameterScope(), Is.EqualTo(ScopedKind.None)); + } + [Test] public void AssemblyAttribute() { diff --git a/ICSharpCode.Decompiler.Tests/TypeSystem/TypeSystemTestCase.cs b/ICSharpCode.Decompiler.Tests/TypeSystem/TypeSystemTestCase.cs index e3135e43e4..5213cb5084 100644 --- a/ICSharpCode.Decompiler.Tests/TypeSystem/TypeSystemTestCase.cs +++ b/ICSharpCode.Decompiler.Tests/TypeSystem/TypeSystemTestCase.cs @@ -18,6 +18,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -219,6 +220,34 @@ public void MethodWithOptionalDecimalParameter(decimal x = 1) { } public void VarArgsMethod(__arglist) { } } + public ref struct LifetimeAnnotationTests + { + private int value; + + public static void ExplicitScopedRef(scoped ref int value) { } + public static void ExplicitScopedValue(scoped LifetimeAnnotationTests value) { } + public static void ImplicitScopedRef(out int value) { value = 0; } + public static void UnscopedRef([UnscopedRef] out int value) { value = 0; } + + public void InstanceMethod() { } + + [UnscopedRef] + public ref int UnscopedMethod() + { + return ref value; + } + + [UnscopedRef] + public ref int UnscopedProperty => ref value; + + public ref int AccessorUnscopedProperty { + [UnscopedRef] + get { + return ref value; + } + } + } + public class VarArgsCtor { public VarArgsCtor(__arglist) { } diff --git a/ICSharpCode.Decompiler/TypeSystem/IParameter.cs b/ICSharpCode.Decompiler/TypeSystem/IParameter.cs index 8ab231679d..441da2e030 100644 --- a/ICSharpCode.Decompiler/TypeSystem/IParameter.cs +++ b/ICSharpCode.Decompiler/TypeSystem/IParameter.cs @@ -36,16 +36,46 @@ public enum ReferenceKind : byte RefReadOnly, } + /// + /// Describes which part of a parameter or local is restricted to the current scope. + /// + public enum ScopedKind : byte + { + None, + ScopedRef, + ScopedValue, + } + public struct LifetimeAnnotation { /// - /// C# 11 scoped annotation: "scoped ref" (ScopedRefAttribute) + /// Gets or sets the scope explicitly encoded by ScopedRefAttribute. + /// Implicit and redundant scopedness is excluded. /// - public bool ScopedRef { + public ScopedKind DeclaredScope { + get { +#pragma warning disable 618 + if (ValueScoped) + return ScopedKind.ScopedValue; + if (RefScoped) + return ScopedKind.ScopedRef; +#pragma warning restore 618 + return ScopedKind.None; + } + set { #pragma warning disable 618 - get { return RefScoped; } - set { RefScoped = value; } + RefScoped = value != ScopedKind.None; + ValueScoped = value == ScopedKind.ScopedValue; #pragma warning restore 618 + } + } + + /// + /// C# 11 scoped annotation: "scoped ref" (ScopedRefAttribute) + /// + public bool ScopedRef { + get { return DeclaredScope != ScopedKind.None; } + set { DeclaredScope = value ? ScopedKind.ScopedRef : ScopedKind.None; } } [Obsolete("Use ScopedRef property instead of directly accessing this field")] @@ -53,6 +83,11 @@ public bool ScopedRef { [Obsolete("C# 11 preview: \"ref scoped\" no longer supported")] public bool ValueScoped; + + /// + /// Gets or sets whether UnscopedRefAttribute is present. + /// + public bool HasUnscopedRefAttribute { get; set; } } public interface IParameter : IVariable diff --git a/ICSharpCode.Decompiler/TypeSystem/Implementation/KnownAttributes.cs b/ICSharpCode.Decompiler/TypeSystem/Implementation/KnownAttributes.cs index 52a20c3375..d346096331 100644 --- a/ICSharpCode.Decompiler/TypeSystem/Implementation/KnownAttributes.cs +++ b/ICSharpCode.Decompiler/TypeSystem/Implementation/KnownAttributes.cs @@ -120,11 +120,15 @@ public enum KnownAttribute // C# 14 attributes: ExtensionMarker, + + // Lifetime attributes: + UnscopedRef, + RefSafetyRules, } public static class KnownAttributes { - internal const int Count = (int)KnownAttribute.ExtensionMarker + 1; + internal const int Count = (int)KnownAttribute.RefSafetyRules + 1; static readonly TopLevelTypeName[] typeNames = new TopLevelTypeName[Count]{ default, @@ -200,6 +204,9 @@ public static class KnownAttributes new TopLevelTypeName("System.Runtime.CompilerServices", "InlineArrayAttribute"), // C# 14 attributes: new TopLevelTypeName("System.Runtime.CompilerServices", "ExtensionMarkerAttribute"), + // Lifetime attributes: + new TopLevelTypeName("System.Diagnostics.CodeAnalysis", "UnscopedRefAttribute"), + new TopLevelTypeName("System.Runtime.CompilerServices", "RefSafetyRulesAttribute"), }; public static ref readonly TopLevelTypeName GetTypeName(this KnownAttribute attr) diff --git a/ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataParameter.cs b/ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataParameter.cs index c62438cb98..238c9ed2e5 100644 --- a/ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataParameter.cs +++ b/ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataParameter.cs @@ -125,17 +125,29 @@ public LifetimeAnnotation Lifetime { var metadata = module.metadata; var parameterDef = metadata.GetParameter(handle); - if ((module.TypeSystemOptions & TypeSystemOptions.ParamsCollections) != 0 - && parameterDef.GetCustomAttributes().HasKnownAttribute(metadata, KnownAttribute.ParamCollection)) + var attributes = parameterDef.GetCustomAttributes(); + bool hasUnscopedRefAttribute = attributes.HasKnownAttribute(metadata, KnownAttribute.UnscopedRef); + bool isParamsCollection = (module.TypeSystemOptions & TypeSystemOptions.ParamsCollections) != 0 + && attributes.HasKnownAttribute(metadata, KnownAttribute.ParamCollection); + ScopedKind declaredScope = ScopedKind.None; + if (!hasUnscopedRefAttribute + && !isParamsCollection + && attributes.HasKnownAttribute(metadata, KnownAttribute.ScopedRef)) { - // params collections are implicitly scoped - return default; - } - if (parameterDef.GetCustomAttributes().HasKnownAttribute(metadata, KnownAttribute.ScopedRef)) - { - return new LifetimeAnnotation { ScopedRef = true }; + if (ReferenceKind != ReferenceKind.None) + { + declaredScope = ScopedKind.ScopedRef; + } + else if (Type.IsRefLikeOrAllowsRefLikeType()) + { + declaredScope = ScopedKind.ScopedValue; + } } - return default; + + return new LifetimeAnnotation { + DeclaredScope = declaredScope, + HasUnscopedRefAttribute = hasUnscopedRefAttribute + }; } } diff --git a/ICSharpCode.Decompiler/TypeSystem/MetadataModule.cs b/ICSharpCode.Decompiler/TypeSystem/MetadataModule.cs index 9701bba081..0002b5d0a0 100644 --- a/ICSharpCode.Decompiler/TypeSystem/MetadataModule.cs +++ b/ICSharpCode.Decompiler/TypeSystem/MetadataModule.cs @@ -91,6 +91,7 @@ internal MetadataModule(ICompilation compilation, MetadataFile peFile, TypeSyste var customAttrs = metadata.GetModuleDefinition().GetCustomAttributes(); this.NullableContext = customAttrs.GetNullableContext(metadata) ?? Nullability.Oblivious; this.minAccessibilityForNRT = FindMinimumAccessibilityForNRT(metadata, customAttrs); + this.UseUpdatedEscapeRules = HasRefSafetyRulesVersion(customAttrs, 11); this.rootNamespace = new MetadataNamespace(this, null, string.Empty, metadata.GetNamespaceDefinitionRoot()); if (!options.HasFlag(TypeSystemOptions.Uncached)) @@ -112,6 +113,43 @@ internal string GetString(StringHandle name) public TypeSystemOptions TypeSystemOptions => options; + /// + /// Gets whether the module declares the C# 11 ref-safety rules. + /// + internal bool UseUpdatedEscapeRules { get; } + + bool HasRefSafetyRulesVersion(CustomAttributeHandleCollection attributes, int expectedVersion) + { + foreach (var attributeHandle in attributes) + { + var attribute = metadata.GetCustomAttribute(attributeHandle); + if (!attribute.IsKnownAttribute(metadata, KnownAttribute.RefSafetyRules)) + continue; + + CustomAttributeValue value; + try + { + value = attribute.DecodeValue(Metadata.MetadataExtensions.MinimalAttributeTypeProvider); + } + catch (BadImageFormatException) + { + continue; + } + catch (Metadata.EnumUnderlyingTypeResolveException) + { + continue; + } + if (value.FixedArguments.Length == 1 + && value.FixedArguments[0].Value is int version) + { + // Roslyn recognizes exactly version 11; missing and unknown versions use legacy rules. + return version == expectedVersion; + } + } + + return false; + } + #region IModule interface public MetadataFile MetadataFile { get; } diff --git a/ICSharpCode.Decompiler/TypeSystem/TypeSystemExtensions.cs b/ICSharpCode.Decompiler/TypeSystem/TypeSystemExtensions.cs index 90bf02ce64..a42b17d2b1 100644 --- a/ICSharpCode.Decompiler/TypeSystem/TypeSystemExtensions.cs +++ b/ICSharpCode.Decompiler/TypeSystem/TypeSystemExtensions.cs @@ -658,6 +658,65 @@ public static IAttribute GetAttribute(this IParameter parameter, KnownAttribute } #endregion + #region Lifetime annotations + internal static bool IsRefLikeOrAllowsRefLikeType(this IType type) + { + return type.IsByRefLike + || type is ITypeParameter { AllowsRefLikeType: true }; + } + + /// + /// Gets the parameter's effective scope after applying implicit scope rules and + /// UnscopedRefAttribute. + /// + internal static ScopedKind GetEffectiveScope(this IParameter parameter) + { + LifetimeAnnotation lifetime = parameter.Lifetime; + if (lifetime.HasUnscopedRefAttribute) + return ScopedKind.None; + + if (lifetime.DeclaredScope != ScopedKind.None) + return lifetime.DeclaredScope; + + if (parameter.Owner?.ParentModule is MetadataModule module) + { + if ((module.TypeSystemOptions & TypeSystemOptions.ScopedRef) == 0) + return ScopedKind.None; + + if (module.UseUpdatedEscapeRules && parameter.ReferenceKind == ReferenceKind.Out) + return ScopedKind.ScopedRef; + } + + if (parameter.IsParams && parameter.Type.IsRefLikeOrAllowsRefLikeType()) + return ScopedKind.ScopedValue; + + return ScopedKind.None; + } + + /// + /// Gets the effective scope of the implicit this parameter. + /// + internal static ScopedKind GetThisParameterScope(this IMethod method) + { + if (method.IsStatic || method.DeclaringTypeDefinition?.Kind != TypeKind.Struct) + return ScopedKind.None; + + if (method.ParentModule is MetadataModule module) + { + if ((module.TypeSystemOptions & TypeSystemOptions.ScopedRef) == 0) + return ScopedKind.None; + + bool hasUnscopedRefAttribute = method.HasAttribute(KnownAttribute.UnscopedRef) + || method.AccessorOwner is IProperty property + && property.HasAttribute(KnownAttribute.UnscopedRef); + if (hasUnscopedRefAttribute && module.UseUpdatedEscapeRules) + return ScopedKind.None; + } + + return ScopedKind.ScopedRef; + } + #endregion + #region IParameter.IsDefaultValueAssignmentAllowed /// /// Checks if the parameter is allowed to be assigned a default value. From 857fe55003380f77440735a0046ea0623b416ea7 Mon Sep 17 00:00:00 2001 From: Sebastien Lebreton Date: Sun, 19 Jul 2026 03:12:16 +0200 Subject: [PATCH 2/2] Fix #3853: infer scoped locals from ref safety Local scopedness is erased from IL and PDBs, so it can only be recovered from the body. Compare each declaration initializer with later assignments, field stores, and receiver captures using the C# 11 ref/value escape rules, and emit scoped only when a later operation is strictly narrower. Assisted-by: Copilot:gpt-5.6-sol:GitHub Copilot CLI Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 86d2918e-5a24-48b4-9a86-41d331ec3720 --- .../TestCases/Pretty/RefFields.cs | 319 ++++++++- .../CSharp/CSharpDecompiler.cs | 1 + .../CSharp/Syntax/Modifiers.cs | 4 + .../CSharp/Transforms/DeclareVariables.cs | 8 + ICSharpCode.Decompiler/IL/ILVariable.cs | 14 + .../IntroduceScopedModifierOnLocals.cs | 670 ++++++++++++++++++ 6 files changed, 1014 insertions(+), 2 deletions(-) create mode 100644 ICSharpCode.Decompiler/IL/Transforms/IntroduceScopedModifierOnLocals.cs diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/RefFields.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/RefFields.cs index 7bbb377a6b..a14343bbf9 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/RefFields.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/RefFields.cs @@ -3,6 +3,45 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty { + internal struct Buffer + { + private int value; + + [UnscopedRef] + public ref int GetRef() + { + return ref value; + } + } + + internal ref struct Holder + { + public Span Inner; + + public void Set(Span inner) + { + Inner = inner; + } + + public readonly void ReadonlyUse(Span inner) + { + _ = inner.Length; + } + } + + internal static class HolderExtensions + { + public static void SetExtension(this ref Holder holder, Span inner) + { + holder.Inner = inner; + } + + public static void ReadonlyExtension(this in Holder holder, Span inner) + { + _ = inner.Length; + } + } + internal class LifetimeTests { private static int staticField; @@ -35,15 +74,277 @@ public void OutSpan(out Span span) span = default(Span); } + public Span CaptureOut([UnscopedRef] out int value) + { + value = 0; + return new Span(ref value); + } + + public Span NoCaptureOut(out int value) + { + value = 0; + return default(Span); + } + + public Span Identity(Span span) + { + return span; + } + public void Calls() { int value = 0; - Span span = CreateWithoutCapture(ref value); - //span = CreateAndCapture(ref value); -- would need scoped local, not yet implemented + scoped Span span = CreateWithoutCapture(ref value); + span = CreateAndCapture(ref value); span = ScopedRefSpan(ref span); span = ScopedSpan(span); OutSpan(out span); } + + public int ReassignScopedRefToLocal(bool b, ref int x) + { + int num = 42; + scoped ref int reference = ref x; + if (b) + { + reference = ref num; + } + return reference; + } + + public int ReassignScopedSpanFromOut(bool b) + { + int value = 0; + scoped Span span = default(Span); + if (b) + { + span = CaptureOut(out value); + } + return span[0] + value; + } + + public int ImplicitScopedOutDoesNotCapture() + { + Span span = default(Span); + span = NoCaptureOut(out var value); + Console.WriteLine(span.Length); + return span.Length + value; + } + + public int ReassignScopedSpanFromReceiver(bool b) + { + UnscopedRefStruct unscopedRefStruct = default(UnscopedRefStruct); + scoped Span span = default(Span); + if (b) + { + span = unscopedRefStruct.AsSpan(); + } + return span[0]; + } + + public int ReassignScopedSpanFromRefParameter(bool b, ref int value) + { + scoped Span span = default(Span); + if (b) + { + span = CreateAndCapture(ref value); + } + return span[0]; + } + + public int ReassignScopedSpanFromStackAlloc(bool b) + { + scoped Span span = default(Span); + if (b) + { + span = stackalloc int[1]; + } + return span[0]; + } + + public int ReassignScopedSpanFromScopedValue(bool b, scoped Span value) + { + scoped Span span = default(Span); + if (b) + { + span = value; + } + return span[0]; + } + + public int ReassignScopedSpanFromNestedCall(bool b) + { + int value = 0; + scoped Span span = default(Span); + if (b) + { + span = Identity(CreateAndCapture(ref value)); + } + return span[0]; + } + + public int ReassignScopedSpanFromLocalCopy(bool b) + { + Span span = stackalloc int[1]; + scoped Span span2 = default(Span); + if (b) + { + span2 = span; + } + return span2[0]; + } + + public int ReassignScopedSpanFromScopedRefValue(bool b) + { + Span span = stackalloc int[1]; + scoped Span span2 = default(Span); + if (b) + { + span2 = ScopedRefSpan(ref span); + } + return span2[0]; + } + + public int NarrowInitializerDoesNotNeedScoped(bool b) + { + int num = 1; + int num2 = 2; + ref int reference = ref num; + if (b) + { + reference = ref num2; + } + return reference; + } + + public int NarrowThenWideDoesNotNeedScoped(bool b, ref int value) + { + int num = 1; + ref int reference = ref num; + if (b) + { + reference = ref value; + } + return reference; + } + + public int ClassParameterReceiverDoesNotNarrow(SpanProvider provider, bool b) + { + Span span = default(Span); + if (b) + { + span = provider.GetBuffer(); + } + return span.Length; + } + + public int ClassLocalReceiverDoesNotNarrow(bool b) + { + SpanProvider spanProvider = new SpanProvider(); + Span span = default(Span); + if (b) + { + span = spanProvider.GetBuffer(); + } + return span.Length; + } + + public ref int PlainStructReceiverDoesNotForceScoped(bool b, ref Buffer buffer, ref int other) + { + ref int result = ref buffer.GetRef(); + if (b) + { + result = ref other; + } + return ref result; + } + + public int FieldStoreRequiresScoped() + { + scoped Holder holder = default(Holder); + Span inner = stackalloc int[4]; + holder.Inner = inner; + return holder.Inner.Length; + } + + public int ReceiverCallRequiresScoped() + { + scoped Holder holder = default(Holder); + Span inner = stackalloc int[4]; + holder.Set(inner); + return holder.Inner.Length; + } + + public int ExtensionReceiverCallRequiresScoped() + { + scoped Holder holder = default(Holder); + Span inner = stackalloc int[4]; + holder.SetExtension(inner); + return holder.Inner.Length; + } + + public Holder ReadonlyReceiverDoesNotRequireScoped(bool b) + { + Holder result = default(Holder); + if (b) + { + Span inner = stackalloc int[4]; + result.ReadonlyUse(inner); + } + return result; + } + + public Holder ReadonlyExtensionReceiverDoesNotRequireScoped(bool b) + { + Holder holder = default(Holder); + if (b) + { + Span inner = stackalloc int[4]; + holder.ReadonlyExtension(inner); + } + return holder; + } + + public ReadOnlyHolder ReadonlyRefStructReceiverDoesNotRequireScoped(bool b, Span wide) + { + ReadOnlyHolder result = new ReadOnlyHolder(wide); + if (b) + { + Span other = stackalloc int[1]; + result.Compare(other); + } + return result; + } + + public int TryInitThenReassign(bool condition, Span wide) + { + scoped Span span; + try + { + span = stackalloc int[4]; + } + finally + { + Console.WriteLine("cleanup"); + } + if (condition) + { + span = wide; + } + return span[0]; + } + + } + + internal readonly ref struct ReadOnlyHolder(Span inner) + { + private readonly Span inner = inner; + + public void Compare(Span other) + { + _ = inner.Length; + _ = other.Length; + } } internal ref struct RefFields @@ -86,6 +387,14 @@ public RefFields(ref int v) } } + internal sealed class SpanProvider + { + public Span GetBuffer() + { + return default(Span); + } + } + internal ref struct UnscopedRefStruct { private int val; @@ -98,5 +407,11 @@ public ref int ByRefAccess() { return ref val; } + + [UnscopedRef] + public Span AsSpan() + { + return new Span(ref val); + } } } diff --git a/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs b/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs index c828ad48ae..794c95977a 100644 --- a/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs +++ b/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs @@ -172,6 +172,7 @@ public static List GetILTransforms() new RemoveRedundantReturn(), new IntroduceDynamicTypeOnLocals(), new IntroduceNativeIntTypeOnLocals(), + new IntroduceScopedModifierOnLocals(), new AssignVariableNames(), }; } diff --git a/ICSharpCode.Decompiler/CSharp/Syntax/Modifiers.cs b/ICSharpCode.Decompiler/CSharp/Syntax/Modifiers.cs index d286f21cf8..ffa6501770 100644 --- a/ICSharpCode.Decompiler/CSharp/Syntax/Modifiers.cs +++ b/ICSharpCode.Decompiler/CSharp/Syntax/Modifiers.cs @@ -59,6 +59,7 @@ public enum Modifiers Async = 0x10000, Ref = 0x20000, Required = 0x40000, + Scoped = 0x80000, VisibilityMask = Private | Internal | Protected | Public, @@ -78,6 +79,7 @@ public static class CSharpModifiers Modifiers.Unsafe, Modifiers.Static, Modifiers.Abstract, Modifiers.Virtual, Modifiers.Sealed, Modifiers.Override, Modifiers.Required, Modifiers.Readonly, Modifiers.Volatile, + Modifiers.Scoped, Modifiers.Ref, Modifiers.Extern, Modifiers.Partial, Modifiers.Const, Modifiers.Async, @@ -124,6 +126,8 @@ public static string GetModifierName(Modifiers modifier) return "async"; case Modifiers.Ref: return "ref"; + case Modifiers.Scoped: + return "scoped"; case Modifiers.Required: return "required"; case Modifiers.Any: diff --git a/ICSharpCode.Decompiler/CSharp/Transforms/DeclareVariables.cs b/ICSharpCode.Decompiler/CSharp/Transforms/DeclareVariables.cs index fe3c2e70eb..2200391bbc 100644 --- a/ICSharpCode.Decompiler/CSharp/Transforms/DeclareVariables.cs +++ b/ICSharpCode.Decompiler/CSharp/Transforms/DeclareVariables.cs @@ -638,6 +638,10 @@ void InsertVariableDeclarations(TransformContext context) init.AddAnnotation(annotation); } } + if (context.Settings.ScopedRef && v.ILVariable.IsScoped) + { + vds.Modifiers |= Modifiers.Scoped; + } return vds; }, "Combine variable declaration with initializer")); } @@ -696,6 +700,10 @@ void InsertVariableDeclarations(TransformContext context) initializer = new DefaultValueExpression(type.Clone()); } var vds = new VariableDeclarationStatement(type, v.Name, initializer); + if (context.Settings.ScopedRef && v.ILVariable.IsScopedWithoutInitializer) + { + vds.Modifiers |= Modifiers.Scoped; + } vds.Variables.Single().AddAnnotation(new ILVariableResolveResult(ilVariable)); context.Step("Insert variable declaration", v.InsertionPoint.nextNode); if (v.InsertionPoint.nextNode.Parent is LambdaExpression lambda) diff --git a/ICSharpCode.Decompiler/IL/ILVariable.cs b/ICSharpCode.Decompiler/IL/ILVariable.cs index f67a0260b7..8d65e86700 100644 --- a/ICSharpCode.Decompiler/IL/ILVariable.cs +++ b/ICSharpCode.Decompiler/IL/ILVariable.cs @@ -154,6 +154,16 @@ internal set { /// public bool IsRefReadOnly { get; internal set; } + /// + /// This variable must be declared with the C# scoped modifier. + /// + public bool IsScoped { get; internal set; } + + /// + /// This variable must be declared with scoped when its declaration has no initializer. + /// + internal bool IsScopedWithoutInitializer { get; set; } + /// /// The index of the local variable or parameter (depending on Kind) /// @@ -477,6 +487,10 @@ public ILVariable(VariableKind kind, IType type, StackType stackType, int? index internal void WriteDefinitionTo(ITextOutput output) { + if (IsScoped) + { + output.Write("scoped "); + } if (IsRefReadOnly) { output.Write("readonly "); diff --git a/ICSharpCode.Decompiler/IL/Transforms/IntroduceScopedModifierOnLocals.cs b/ICSharpCode.Decompiler/IL/Transforms/IntroduceScopedModifierOnLocals.cs new file mode 100644 index 0000000000..90e81f9a26 --- /dev/null +++ b/ICSharpCode.Decompiler/IL/Transforms/IntroduceScopedModifierOnLocals.cs @@ -0,0 +1,670 @@ +// Copyright (c) 2026 Sebastien Lebreton +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +#nullable enable + +using System.Collections.Generic; +using System.Linq; + +using ICSharpCode.Decompiler.IL.Transforms; +using ICSharpCode.Decompiler.TypeSystem; + +namespace ICSharpCode.Decompiler.IL +{ + /// + /// Infers the C# scoped modifier on locals whose initializer has a wider + /// ref-safety context than a later assignment. + /// + /// + /// The modifier is erased from IL and PDBs, so recovery compares the ref-safe-context + /// (for ref locals) or safe-context (for ref-struct locals) of the declaration initializer + /// with each later store. The three contexts are ordered from narrowest to widest: + /// current method, return-only, calling method. A narrower later store proves that the + /// original declaration required scoped. + /// + class IntroduceScopedModifierOnLocals : IILTransform + { + public void Run(ILFunction function, ILTransformContext context) + { + if (!context.Settings.ScopedRef) + return; + + foreach (var nestedFunction in function.Descendants.OfType()) + { + if (nestedFunction.Method is not { + ParentModule: MetadataModule { UseUpdatedEscapeRules: true } + } method) + { + continue; + } + + new Analyzer(nestedFunction, method, context).Run(); + } + } + + enum SafeContext : byte + { + CurrentMethod, + ReturnOnly, + CallingMethod, + } + + readonly struct LocalScopes( + SafeContext? refEscape, + SafeContext? valEscape, + bool isScoped, + bool isScopedWithoutInitializer) + { + public SafeContext? RefEscape { get; } = refEscape; + public SafeContext? ValEscape { get; } = valEscape; + public bool IsScoped { get; } = isScoped; + public bool IsScopedWithoutInitializer { get; } = isScopedWithoutInitializer; + } + + sealed class Analyzer(ILFunction function, IMethod method, ILTransformContext context) + { + readonly Dictionary _stores = []; + readonly Dictionary> _fieldStores = []; + readonly Dictionary> _receiverCalls = []; + Dictionary _scopes = []; + + public void Run() + { + foreach (var variable in function.Variables) + { + variable.IsScoped = false; + variable.IsScopedWithoutInitializer = false; + if (variable.Kind is not (VariableKind.Local or VariableKind.StackSlot) + || !variable.Type.IsRefLikeOrAllowsRefLikeType() + || variable.UsesInitialValue) + { + continue; + } + + var stores = variable.StoreInstructions.OfType().ToArray(); + if (stores.Length == 0 || stores.Any(store => store.Parent == null)) + continue; + + _stores.Add(variable, stores); + _scopes.Add(variable, default); + } + + foreach (var store in function.Descendants.OfType()) + { + if (!TryGetStorageRoot(store.Target, out var variable) + || !_stores.ContainsKey(variable)) + { + continue; + } + + if (!_fieldStores.TryGetValue(variable, out var stores)) + { + stores = []; + _fieldStores.Add(variable, stores); + } + stores.Add(store); + } + + foreach (var call in function.Descendants.OfType()) + { + bool isRefStructReceiver = call.IsInstanceCall + && call.Method.DeclaringType.IsRefLikeOrAllowsRefLikeType() + && !call.Method.ThisIsRefReadOnly; + if (!(isRefStructReceiver || IsRefStructExtensionReceiver(call)) + || call.Arguments.Count == 0 + || !TryGetStorageRoot(call.Arguments[0], out var variable) + || !_stores.ContainsKey(variable)) + { + continue; + } + + if (!_receiverCalls.TryGetValue(variable, out var calls)) + { + calls = []; + _receiverCalls.Add(variable, calls); + } + calls.Add(call); + } + + for (int iteration = 0; iteration <= _stores.Count * 3; iteration++) + { + context.CancellationToken.ThrowIfCancellationRequested(); + bool changed = false; + var nextScopes = new Dictionary(_scopes); + foreach (var pair in _stores) + { + ILVariable variable = pair.Key; + StLoc[] stores = pair.Value; + var initializer = GetFirstStore(stores); + LocalScopes scopes = CalculateLocalScopes(variable, initializer, stores); + LocalScopes previous = _scopes[variable]; + if (scopes.RefEscape != previous.RefEscape + || scopes.ValEscape != previous.ValEscape + || scopes.IsScoped != previous.IsScoped + || scopes.IsScopedWithoutInitializer != previous.IsScopedWithoutInitializer) + { + nextScopes[variable] = scopes; + changed = true; + } + } + + _scopes = nextScopes; + if (!changed) + break; + } + + foreach (var pair in _scopes) + { + pair.Key.IsScoped = pair.Value.IsScoped; + pair.Key.IsScopedWithoutInitializer = pair.Value.IsScopedWithoutInitializer; + } + } + + LocalScopes CalculateLocalScopes(ILVariable variable, StLoc initializer, IReadOnlyList stores) + { + bool isRefLocal = variable.Type.Kind == TypeKind.ByReference; + SafeContext? initializerRefEscape = isRefLocal + ? GetRefEscape(initializer.Value) + : SafeContext.CurrentMethod; + SafeContext? initializerValEscape = GetVariableValueType(variable).IsRefLikeOrAllowsRefLikeType() + ? GetValEscape(initializer.Value) + : SafeContext.CallingMethod; + + if (initializerRefEscape == null || initializerValEscape == null) + return default; + + bool requiresScoped = false; + SafeContext initializerEscape = isRefLocal + ? initializerRefEscape.Value + : initializerValEscape.Value; + bool requiresScopedWithoutInitializer = initializerEscape < SafeContext.CallingMethod; + foreach (var store in stores) + { + if (store == initializer) + continue; + + SafeContext? storeEscape = isRefLocal + ? GetRefEscape(store.Value) + : GetValEscape(store.Value); + if (storeEscape is not { } scope) + continue; + + if (scope < SafeContext.CallingMethod) + requiresScopedWithoutInitializer = true; + if (scope < initializerEscape) + { + requiresScoped = true; + } + } + + if (!requiresScoped && _fieldStores.TryGetValue(variable, out var fieldStores)) + { + foreach (var store in fieldStores) + { + if (!initializer.IsBefore(store)) + continue; + + SafeContext? storeEscape = store.Type.Kind == TypeKind.ByReference + ? GetRefEscape(store.Value) + : GetValEscape(store.Value); + if (storeEscape is not { } scope) + continue; + + if (scope < SafeContext.CallingMethod) + requiresScopedWithoutInitializer = true; + if (scope < initializerValEscape.Value) + { + requiresScoped = true; + } + } + } + + if (!requiresScoped && _receiverCalls.TryGetValue(variable, out var receiverCalls)) + { + foreach (var call in receiverCalls) + { + if (!initializer.IsBefore(call)) + continue; + + SafeContext? callEscape = GetInvocationEscapeToReceiver(call); + if (callEscape is not { } scope) + continue; + + if (scope < SafeContext.CallingMethod) + requiresScopedWithoutInitializer = true; + if (scope < initializerValEscape.Value) + { + requiresScoped = true; + } + } + } + + if (!requiresScoped) + { + return new LocalScopes( + initializerRefEscape, + initializerValEscape, + isScoped: false, + isScopedWithoutInitializer: requiresScopedWithoutInitializer); + } + + return isRefLocal + ? new LocalScopes( + SafeContext.CurrentMethod, + SafeContext.CallingMethod, + isScoped: true, + isScopedWithoutInitializer: true) + : new LocalScopes( + SafeContext.CurrentMethod, + SafeContext.CurrentMethod, + isScoped: true, + isScopedWithoutInitializer: true); + } + + /// + /// Gets the declaration-initializer candidate: the first assignment in the final + /// ILAst traversal order. + /// + static StLoc GetFirstStore(IReadOnlyList stores) + { + StLoc first = stores[0]; + for (int i = 1; i < stores.Count; i++) + { + if (stores[i].IsBefore(first)) + first = stores[i]; + } + + return first; + } + + static bool IsRefStructExtensionReceiver(CallInstruction call) + { + if (!call.Method.IsExtensionMethod || call.Arguments.Count == 0) + return false; + + IParameter? receiver = call.GetParameter(0); + return receiver is { ReferenceKind: ReferenceKind.Ref } + && GetValueType(receiver.Type).IsRefLikeOrAllowsRefLikeType(); + } + + static bool TryGetStorageRoot(ILInstruction target, out ILVariable variable) + { + while (target is LdFlda fieldAddress) + { + target = fieldAddress.Target; + } + + if (target is LdLoca ldloca) + { + variable = ldloca.Variable; + return true; + } + + variable = null!; + return false; + } + + SafeContext? GetRefEscape(ILInstruction value) + { + switch (value) + { + case LdLoc ldloc: + return GetVariableScopes(ldloc.Variable).RefEscape; + case LdLoca: + case LocAlloc: + case LocAllocSpan: + return SafeContext.CurrentMethod; + case LdsFlda: + case LdElema: + return SafeContext.CallingMethod; + case LdElemaInlineArray inlineArray: + return GetRefEscape(inlineArray.Array); + case LdFlda fieldAddress: + return GetFieldRefEscape(fieldAddress); + case LdObjIfRef load: + return GetRefEscape(load.Target); + case CallInstruction call: + return GetInvocationEscape(call, isRefEscape: true); + case IfInstruction conditional: + return Intersect(GetRefEscape(conditional.TrueInst), GetRefEscape(conditional.FalseInst)); + case NullCoalescingInstruction coalescing: + return Intersect(GetRefEscape(coalescing.ValueInst), GetRefEscape(coalescing.FallbackInst)); + case Block block: + return block.Kind == BlockKind.StackAllocInitializer + ? SafeContext.CurrentMethod + : GetRefEscape(block.FinalInstruction); + default: + return null; + } + } + + SafeContext? GetValEscape(ILInstruction value) + { + switch (value) + { + case DefaultValue: + case LdNull: + case NewArr: + return SafeContext.CallingMethod; + case LocAlloc: + case LocAllocSpan: + return SafeContext.CurrentMethod; + case LdLoc ldloc: + return GetVariableScopes(ldloc.Variable).ValEscape; + case LdLoca ldloca: + return GetVariableScopes(ldloca.Variable).ValEscape; + case LdsFlda: + case LdElema: + return SafeContext.CallingMethod; + case LdElemaInlineArray inlineArray: + return GetValEscape(inlineArray.Array); + case LdFlda fieldAddress: + return GetFieldValEscape(fieldAddress); + case LdObj load: + return GetValEscape(load.Target); + case LdObjIfRef load: + return GetValEscape(load.Target); + case CallInstruction call: + return GetInvocationEscape(call, isRefEscape: false); + case IfInstruction conditional: + return Intersect(GetValEscape(conditional.TrueInst), GetValEscape(conditional.FalseInst)); + case NullCoalescingInstruction coalescing: + return Intersect(GetValEscape(coalescing.ValueInst), GetValEscape(coalescing.FallbackInst)); + case Block block: + return block.Kind == BlockKind.StackAllocInitializer + ? SafeContext.CurrentMethod + : GetValEscape(block.FinalInstruction); + default: + return null; + } + } + + SafeContext? GetFieldRefEscape(LdFlda fieldAddress) + { + if (fieldAddress.Field.IsStatic || fieldAddress.Field.DeclaringType.IsReferenceType == true) + return SafeContext.CallingMethod; + + if (fieldAddress.Field.ReturnType.Kind == TypeKind.ByReference) + return GetValEscape(fieldAddress.Target); + + return GetRefEscape(fieldAddress.Target); + } + + SafeContext? GetFieldValEscape(LdFlda fieldAddress) + { + if (fieldAddress.Field.IsStatic + || !fieldAddress.Field.DeclaringType.IsRefLikeOrAllowsRefLikeType()) + { + return SafeContext.CallingMethod; + } + + return GetValEscape(fieldAddress.Target); + } + + SafeContext? GetInvocationEscape(CallInstruction call, bool isRefEscape) + { + IType returnType = call is NewObj ? call.Method.DeclaringType : call.Method.ReturnType; + bool hasRefLikeReturn = GetValueType(returnType).IsRefLikeOrAllowsRefLikeType(); + if (!isRefEscape && !hasRefLikeReturn) + return SafeContext.CallingMethod; + + if (returnType.Kind == TypeKind.Unknown) + return null; + + if (call.Method.ParentModule is not MetadataModule module) + return null; + + return module.UseUpdatedEscapeRules + ? GetInvocationEscapeWithUpdatedRules(call, isRefEscape, returnType) + : GetInvocationEscapeWithOldRules(call, isRefEscape); + } + + SafeContext? GetInvocationEscapeWithUpdatedRules( + CallInstruction call, bool isRefEscape, IType returnType) + { + SafeContext? result = SafeContext.CallingMethod; + bool returnsRefToRefStruct = returnType is ByReferenceType { ElementType: var elementType } + && elementType.IsRefLikeOrAllowsRefLikeType(); + + for (int i = 0; i < call.Arguments.Count; i++) + { + if (i == 0 && call.IsInstanceCall) + { + IType receiverType = call.Method.DeclaringType; + if (receiverType.IsRefLikeOrAllowsRefLikeType()) + { + result = AddInvocationEscape( + result, GetValEscape(call.Arguments[i]), false, + isRefEscape, returnsRefToRefStruct, isRefToRefStruct: true); + } + + if (receiverType.IsReferenceType == false + && call.Method.GetThisParameterScope() != ScopedKind.ScopedRef) + { + result = AddInvocationEscape( + result, GetRefEscape(call.Arguments[i]), true, + isRefEscape, returnsRefToRefStruct, + isRefToRefStruct: receiverType.IsRefLikeOrAllowsRefLikeType()); + } + continue; + } + + IParameter? parameter = call.GetParameter(i); + if (parameter == null) + return null; + + IType parameterType = GetValueType(parameter.Type); + bool isRefToRefStruct = parameter.ReferenceKind != ReferenceKind.None + && parameterType.IsRefLikeOrAllowsRefLikeType(); + if (parameterType.IsRefLikeOrAllowsRefLikeType() + && parameter.ReferenceKind != ReferenceKind.Out + && GetParameterValEscape(parameter) != SafeContext.CurrentMethod) + { + result = AddInvocationEscape( + result, GetValEscape(call.Arguments[i]), false, + isRefEscape, returnsRefToRefStruct, isRefToRefStruct); + } + + if (parameter.ReferenceKind != ReferenceKind.None + && GetParameterRefEscape(parameter) != SafeContext.CurrentMethod) + { + result = AddInvocationEscape( + result, GetRefEscape(call.Arguments[i]), true, + isRefEscape, returnsRefToRefStruct, isRefToRefStruct); + } + } + + return result; + } + + SafeContext? GetInvocationEscapeWithOldRules(CallInstruction call, bool isRefEscape) + { + SafeContext? result = SafeContext.CallingMethod; + for (int i = 0; i < call.Arguments.Count; i++) + { + if (i == 0 && call.IsInstanceCall) + { + if (call.Method.DeclaringType.IsRefLikeOrAllowsRefLikeType()) + result = Intersect(result, GetValEscape(call.Arguments[i])); + continue; + } + + IParameter? parameter = call.GetParameter(i); + if (parameter == null) + return null; + + if (!isRefEscape && GetValueType(parameter.Type).IsRefLikeOrAllowsRefLikeType()) + result = Intersect(result, GetValEscape(call.Arguments[i])); + + if (isRefEscape && parameter.ReferenceKind != ReferenceKind.None) + result = Intersect(result, GetRefEscape(call.Arguments[i])); + } + + return result; + } + + SafeContext? GetInvocationEscapeToReceiver(CallInstruction call) + { + if (call.Method.ParentModule is not MetadataModule { UseUpdatedEscapeRules: true }) + return null; + + SafeContext? result = SafeContext.CallingMethod; + for (int i = 1; i < call.Arguments.Count; i++) + { + IParameter? parameter = call.GetParameter(i); + if (parameter == null) + return null; + + IType parameterType = GetValueType(parameter.Type); + if (parameterType.IsRefLikeOrAllowsRefLikeType() + && parameter.ReferenceKind != ReferenceKind.Out + && GetParameterValEscape(parameter) == SafeContext.CallingMethod) + { + result = Intersect(result, GetValEscape(call.Arguments[i])); + } + + if (parameter.ReferenceKind != ReferenceKind.None + && GetParameterRefEscape(parameter) == SafeContext.CallingMethod) + { + result = Intersect(result, GetRefEscape(call.Arguments[i])); + } + } + + return result; + } + + static SafeContext? AddInvocationEscape( + SafeContext? current, + SafeContext? argument, + bool argumentIsRefEscape, + bool invocationIsRefEscape, + bool returnsRefToRefStruct, + bool isRefToRefStruct) + { + if (returnsRefToRefStruct + && (!isRefToRefStruct || argumentIsRefEscape != invocationIsRefEscape)) + { + return current; + } + + return Intersect(current, argument); + } + + LocalScopes GetVariableScopes(ILVariable variable) + { + if (_scopes.TryGetValue(variable, out var scopes)) + return scopes; + + if (variable.Kind != VariableKind.Parameter) + return default; + + if (variable.Index is not int index) + return default; + + if (index < 0) + return GetThisParameterScopes(); + + if (index >= method.Parameters.Count) + return default; + + IParameter parameter = method.Parameters[index]; + return new LocalScopes( + GetParameterRefEscape(parameter), + GetParameterValEscape(parameter), + isScoped: false, + isScopedWithoutInitializer: false); + } + + LocalScopes GetThisParameterScopes() + { + if (method.DeclaringTypeDefinition?.Kind != TypeKind.Struct) + { + return new LocalScopes( + SafeContext.CallingMethod, + SafeContext.CallingMethod, + isScoped: false, + isScopedWithoutInitializer: false); + } + + SafeContext refEscape = method.GetThisParameterScope() == ScopedKind.ScopedRef + ? SafeContext.CurrentMethod + : SafeContext.ReturnOnly; + SafeContext valEscape = method.IsConstructor + ? SafeContext.ReturnOnly + : SafeContext.CallingMethod; + return new LocalScopes( + refEscape, + valEscape, + isScoped: false, + isScopedWithoutInitializer: false); + } + + static SafeContext GetParameterRefEscape(IParameter parameter) + { + if (parameter.ReferenceKind == ReferenceKind.None) + return SafeContext.CurrentMethod; + if (parameter.GetEffectiveScope() == ScopedKind.ScopedRef) + return SafeContext.CurrentMethod; + + bool useUpdatedEscapeRules = parameter.Owner?.ParentModule is MetadataModule { + UseUpdatedEscapeRules: true + }; + if (parameter.Lifetime.HasUnscopedRefAttribute + && useUpdatedEscapeRules + && parameter.ReferenceKind == ReferenceKind.Out) + { + return SafeContext.ReturnOnly; + } + if (parameter.Lifetime.HasUnscopedRefAttribute && useUpdatedEscapeRules) + return SafeContext.CallingMethod; + + return SafeContext.ReturnOnly; + } + + static SafeContext GetParameterValEscape(IParameter parameter) + { + if (parameter.GetEffectiveScope() == ScopedKind.ScopedValue) + return SafeContext.CurrentMethod; + if (parameter.ReferenceKind == ReferenceKind.Out + && parameter.Owner?.ParentModule is MetadataModule { UseUpdatedEscapeRules: true }) + { + return SafeContext.ReturnOnly; + } + + return SafeContext.CallingMethod; + } + + static IType GetVariableValueType(ILVariable variable) + { + return GetValueType(variable.Type); + } + + static IType GetValueType(IType type) + { + return type is ByReferenceType byReference ? byReference.ElementType : type; + } + + static SafeContext? Intersect(SafeContext? left, SafeContext? right) + { + if (left == null || right == null) + return null; + return left < right ? left : right; + } + } + } +}