Skip to content

Propagate DynamicallyAccessedMemberTypes through Nullable<T> to T#2675

Merged
jtschuster merged 37 commits intodotnet:mainfrom
jtschuster:nullables
Mar 18, 2022
Merged

Propagate DynamicallyAccessedMemberTypes through Nullable<T> to T#2675
jtschuster merged 37 commits intodotnet:mainfrom
jtschuster:nullables

Conversation

@jtschuster
Copy link
Member

@jtschuster jtschuster commented Mar 4, 2022

This PR enables annotations placed on Nullable to propagate through to T in the linker. This does not work in the analyzer yet and will fail in CI until I update the analyzer to work with the changes.

This creates a new InstrinsicId value for Nullable.GetUnderlyingType it creates a new struct NullableSystemTypeValue that inherits from SystemTypeValue, and tracks the MultiValue of the wrapped type. It also creates NullableRuntimeTypeHandleValue which works similarly. In MethodBodyScanner, when previously a RuntimeTypeHandleValue was created in ScanLdtoken, it now will create a NullableRuntimeTypeHandleValue when the type of the handle is Nullable`1. Then when a typeof is called on that handle, it converts a NullableRuntimeTypeHandleValue to the NullableSystemTypeValue. Finally, if Nullable.GetUnderlyingType is called on that NullableSystemTypeValue, it propagates the annotations of each ValueWithDynamicallyAccessedMembers to the return value, or propagates the SystemTypeValue to the return value.

I think this covers all the areas where nullables need to be handled, but I might be missing something.

Fixes #2662

Copy link
Member

@vitek-karas vitek-karas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other than the comments (main worry there is handling of generic arguments), this is missing marking changes.

We need to add a test which validates that code like

[StructLayout(LayoutKind.Auto)]  // This is important since without this linker will not trim fields, but add a test for methods as well just in case
struct TestS
{
    [Kept]  // <--- this is the important bit. There's no reference to Field, it should be preserved because of DAM
    public int Field;
}

void Test()
{
    typeof(Nullable<StructS>).RequirePublicFields(); // Applies DAM PublicFields onto the type, this should propagate to the StructS.
}

Note that "marking" is not just a linker concept - in analyzer it means check all members of the "marked" type that they're not RUC (for example). So add tests which check for these warnings as well - those will be valid for both linker and analyzer.

But we do need the "Keep" tests for linker as well.

record RuntimeTypeHandleValue : SingleValue
sealed record RuntimeTypeHandleValue : SingleValue
{
public RuntimeTypeHandleValue (in TypeProxy representedType) => RepresentedType = representedType;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add an assert that the type is not Nullable<T>

Copy link
Member Author

@jtschuster jtschuster Mar 8, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I added an assert I ran into cases where System.Nullable`1 wouldn't have a type argument, so it wouldn't make a NullableValue and would instantiate a SystemTypeValue or RuntimeTypeHandleValue. To me a SystemTypeValue makes sense since there is nothing extra to track in those situations, but would it make more sense to have the UnderlyingType of NullableValue's be optional?

Generates NullableValue's in a few missing places, and tracks
NullValue.Instance's in TypeHandle intrinsics.
@jtschuster jtschuster marked this pull request as ready for review March 8, 2022 22:00
@jtschuster jtschuster requested a review from marek-safar as a code owner March 8, 2022 22:00
Copy link
Member

@vitek-karas vitek-karas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need to fix the MakeGenericType as it currently creates an analysis hole which could break apps.

jtschuster and others added 3 commits March 9, 2022 16:50
Also adds ArrayCreationOperation visitors to create ArrayValue's. Also
addresses some other PR comments.
/// </summary>
sealed record NullableRuntimeTypeWithDamHandleValue : SingleValue
{
public NullableRuntimeTypeWithDamHandleValue (in TypeProxy nullableType, in SingleValue underlyingTypeValue)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

`UnderlyingTypeValue needs to be SingleValue here because in MethodBodyScanner, a RuntimeTypeHandleForGenericParameterValue is passed as the underlying type.

namespace ILLink.Shared.DataFlow
{
// Adds ability to deep copy a value
public interface IDeepCopyValue<TSingleValue>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think ideally this would be required by every type used as a value in a lattice, but we could do that in a separate change.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah - I think it makes sense to require it everywhere - this change does require it on SingleValue. Other values - I would also prefer a different PR>

namespace ILLink.Shared.TrimAnalysis
{
/// <summary>
/// This represents a Nullable<T> where T is a known SystemTypeValue.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is true, should we enforce at the type system level that the underlying type is a SystemTypeValue?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I considered that, but Vitek had a reason to leave it as SingleValue. I can't remember exactly why, but having it different makes sure that we handle it separately from SystemTypeValue instead of accidentally matching NullableSystemTypeValue to the SystemTypeValue case.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's OK to change the underlying type to SystemTypeValue if it's always that.
The NullableSystemTypeValue must be separate from SystemTypeValue - it does act very similarly, but it adds the "this is Nullable flag" to the type.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now I remember we also wanted to keep SystemTypeValue sealed to make type checking faster. I think it might be best to leave it as inheriting from SingleValue.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry - what I meant was, can we make the underlyingType a SystemTypeValue (not that NullableSystemTypeValue should itself be a SystemTypeValue)? I think this is more consistent with how the underlying type of a NullableValueWithDynamicallyAccessedMembers is also a dataflow value, rather than a TypeProxy.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question for RuntimeTypeHandleForNullableSystemTypeValue.

Adds tests to validate dataflow in multidimensional arrays, adds a
temporary nullable MethodReturnValue variable to differentiate
intentional Top values and unassigned return values. Moves the static
MultiValueLattice field to the shared ArrayValue definition.
Adds tests to validate dataflow in multidimensional arrays, adds a
temporary nullable MethodReturnValue variable to differentiate
intentional Top values and unassigned return values. Moves the static
MultiValueLattice field to the shared ArrayValue definition.
Copy link
Member

@vitek-karas vitek-karas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good. Let's wait on Sven for another opinion.

Copy link
Member

@sbomer sbomer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few more comments, overall LGTM. Thanks a lot!

AddReturnValue (new NullableValueWithDynamicallyAccessedMembers (typeValue.RepresentedType, damValue));
break;
// Don't warn on Nullable<T>, it will throw instead
case NullableSystemTypeValue:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: maybe put this and NullableValueWithDynamicallyAccessedMembers next to each other and have one fall through - since they aren't tracked for the same reason.

namespace ILLink.Shared.TrimAnalysis
{
/// <summary>
/// This represents a Nullable<T> where T is a known SystemTypeValue.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry - what I meant was, can we make the underlyingType a SystemTypeValue (not that NullableSystemTypeValue should itself be a SystemTypeValue)? I think this is more consistent with how the underlying type of a NullableValueWithDynamicallyAccessedMembers is also a dataflow value, rather than a TypeProxy.

namespace ILLink.Shared.TrimAnalysis
{
/// <summary>
/// This represents a Nullable<T> where T is a known SystemTypeValue.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question for RuntimeTypeHandleForNullableSystemTypeValue.

new GenericParameterValue (gp, _context.Annotations.FlowAnnotations.GetGenericParameterAnnotation (gp)));

case TypeReference underlyingType:
return new NullableSystemTypeValue (genericArgumentType, ResolveToTypeDefinition (underlyingType)!);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably uncommon, but I think it's possible for ResolveToTypeDefinition to fail - maybe this should return an UnknownValue if it resolves to null?

case ValueWithDynamicallyAccessedMembers damValue:
AddReturnValue (new NullableValueWithDynamicallyAccessedMembers (typeValue.RepresentedType, damValue));
break;
// Don't warn on Nullable<T>, it will throw instead
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same nit here, I would combine these two cases.


return true;

void AddReturnValue (MultiValue value)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for making this more consistent with the shared code!

@jtschuster
Copy link
Member Author

@sbomer
Would you mind opening an issue to track making the analyzer consistent with the linker on this?

Unless I'm misunderstanding what you're talking about, it looks like #2680 tracks the overall discrepancy between the analyzer and linker for arrays. I'll make a note about multidimensional arrays though.

@jtschuster jtschuster merged commit ed8b22a into dotnet:main Mar 18, 2022
agocke pushed a commit to dotnet/runtime that referenced this pull request Nov 16, 2022
…tnet/linker#2675)

Add intrinsic support for Nullable.GetUnderlyingType and support for MakeGenericType with Nullables

Adds ArrayCreationOperation visitors to create ArrayValue's in the analyzer, and adds start of dataflow analysis for array values.

Adds tests to validate dataflow in Arrays.

Co-authored-by: vitek-karas <[email protected]>

Commit migrated from dotnet/linker@ed8b22a
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Apply annotations through Nullable<T> transparently

3 participants