Browse Source

Merge pull request #3887 from sailro/scoped-ref-type-system

Model scoped and UnscopedRef lifetimes in the type system
pull/3888/head
Daniel Grunwald 1 month ago committed by GitHub
parent
commit
042f57be5a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 81
      ICSharpCode.Decompiler.Tests/TypeSystem/TypeSystemLoaderTests.cs
  2. 29
      ICSharpCode.Decompiler.Tests/TypeSystem/TypeSystemTestCase.cs
  3. 32
      ICSharpCode.Decompiler/TypeSystem/IParameter.cs
  4. 4
      ICSharpCode.Decompiler/TypeSystem/Implementation/KnownAttributes.cs
  5. 30
      ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataParameter.cs
  6. 38
      ICSharpCode.Decompiler/TypeSystem/MetadataModule.cs
  7. 59
      ICSharpCode.Decompiler/TypeSystem/TypeSystemExtensions.cs

81
ICSharpCode.Decompiler.Tests/TypeSystem/TypeSystemLoaderTests.cs

@ -196,6 +196,87 @@ namespace ICSharpCode.Decompiler.Tests.TypeSystem @@ -196,6 +196,87 @@ namespace ICSharpCode.Decompiler.Tests.TypeSystem
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()
{

29
ICSharpCode.Decompiler.Tests/TypeSystem/TypeSystemTestCase.cs

@ -19,6 +19,7 @@ @@ -19,6 +19,7 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@ -220,6 +221,34 @@ namespace ICSharpCode.Decompiler.Tests.TypeSystem @@ -220,6 +221,34 @@ namespace ICSharpCode.Decompiler.Tests.TypeSystem
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) { }

32
ICSharpCode.Decompiler/TypeSystem/IParameter.cs

@ -18,7 +18,6 @@ @@ -18,7 +18,6 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
@ -36,23 +35,36 @@ namespace ICSharpCode.Decompiler.TypeSystem @@ -36,23 +35,36 @@ namespace ICSharpCode.Decompiler.TypeSystem
RefReadOnly,
}
/// <summary>
/// Describes which part of a parameter or local is restricted to the current scope.
/// </summary>
public enum ScopedKind : byte
{
None,
ScopedRef,
ScopedValue,
}
public struct LifetimeAnnotation
{
/// <summary>
/// Gets or sets the scope explicitly encoded by <c>ScopedRefAttribute</c>.
/// Implicit and redundant scopedness is excluded.
/// </summary>
public ScopedKind DeclaredScope { get; set; }
/// <summary>
/// C# 11 scoped annotation: "scoped ref" (ScopedRefAttribute)
/// </summary>
public bool ScopedRef {
#pragma warning disable 618
get { return RefScoped; }
set { RefScoped = value; }
#pragma warning restore 618
get { return DeclaredScope != ScopedKind.None; }
set { DeclaredScope = value ? ScopedKind.ScopedRef : ScopedKind.None; }
}
[Obsolete("Use ScopedRef property instead of directly accessing this field")]
public bool RefScoped;
[Obsolete("C# 11 preview: \"ref scoped\" no longer supported")]
public bool ValueScoped;
/// <summary>
/// Gets or sets whether <c>UnscopedRefAttribute</c> is present.
/// </summary>
public bool HasUnscopedRefAttribute { get; set; }
}
public interface IParameter : IVariable

4
ICSharpCode.Decompiler/TypeSystem/Implementation/KnownAttributes.cs

@ -114,6 +114,8 @@ namespace ICSharpCode.Decompiler.TypeSystem @@ -114,6 +114,8 @@ namespace ICSharpCode.Decompiler.TypeSystem
// C# 11 attributes:
Required,
UnscopedRef,
RefSafetyRules,
// C# 12 attributes:
InlineArray,
@ -196,6 +198,8 @@ namespace ICSharpCode.Decompiler.TypeSystem @@ -196,6 +198,8 @@ namespace ICSharpCode.Decompiler.TypeSystem
new TopLevelTypeName("System.Runtime.InteropServices", "UnmanagedCallersOnlyAttribute"),
// C# 11 attributes:
new TopLevelTypeName("System.Runtime.CompilerServices", "RequiredMemberAttribute"),
new TopLevelTypeName("System.Diagnostics.CodeAnalysis", "UnscopedRefAttribute"),
new TopLevelTypeName("System.Runtime.CompilerServices", "RefSafetyRulesAttribute"),
// C# 12 attributes:
new TopLevelTypeName("System.Runtime.CompilerServices", "InlineArrayAttribute"),
// C# 14 attributes:

30
ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataParameter.cs

@ -125,17 +125,29 @@ namespace ICSharpCode.Decompiler.TypeSystem.Implementation @@ -125,17 +125,29 @@ namespace ICSharpCode.Decompiler.TypeSystem.Implementation
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
};
}
}

38
ICSharpCode.Decompiler/TypeSystem/MetadataModule.cs

@ -91,6 +91,7 @@ namespace ICSharpCode.Decompiler.TypeSystem @@ -91,6 +91,7 @@ namespace ICSharpCode.Decompiler.TypeSystem
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 @@ namespace ICSharpCode.Decompiler.TypeSystem @@ -112,6 +113,43 @@ namespace ICSharpCode.Decompiler.TypeSystem
public TypeSystemOptions TypeSystemOptions => options;
/// <summary>
/// Gets whether the module declares the C# 11 ref-safety rules.
/// </summary>
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<IType> 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; }

59
ICSharpCode.Decompiler/TypeSystem/TypeSystemExtensions.cs

@ -692,6 +692,65 @@ namespace ICSharpCode.Decompiler.TypeSystem @@ -692,6 +692,65 @@ namespace ICSharpCode.Decompiler.TypeSystem
}
#endregion
#region Lifetime annotations
internal static bool IsRefLikeOrAllowsRefLikeType(this IType type)
{
return type.IsByRefLike
|| type is ITypeParameter { AllowsRefLikeType: true };
}
/// <summary>
/// Gets the parameter's effective scope after applying implicit scope rules and
/// <c>UnscopedRefAttribute</c>.
/// </summary>
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;
}
/// <summary>
/// Gets the effective scope of the implicit <c>this</c> parameter.
/// </summary>
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
/// <summary>
/// Checks if the parameter is allowed to be assigned a default value.

Loading…
Cancel
Save