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.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/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; + } + } + } +} 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.