Skip to content

Synthesize delegate types for lambdas with parameter types or return type that are not valid type arguments#57295

Merged
cston merged 19 commits intodotnet:mainfrom
cston:55217
Jan 10, 2022
Merged

Synthesize delegate types for lambdas with parameter types or return type that are not valid type arguments#57295
cston merged 19 commits intodotnet:mainfrom
cston:55217

Conversation

@cston
Copy link
Copy Markdown
Contributor

@cston cston commented Oct 21, 2021

Synthesize delegate types for lambda expressions and method groups with parameter types or return type that are not valid type arguments.

For delegates where all parameter types and return type are valid as type arguments, the synthesized delegate type is a generic type where the types are the type arguments That allows a synthesized delegate type to be shared across all delegates with the same number of parameters and same ref kinds.

For instance the delegate type for (ref int p) => p is emitted as <>F{00000001}<int, int> with the generic type internal delegate TResult <>F{00000001}<T1, TResult>(ref T1).

For delegates where one or more of the parameter types or return type are not valid as type arguments (this PR), the synthesized delegate type is non-generic with fixed parameter and return types. The synthesized delegate type is still shared across all delegates with the exact signature however.

For instance the delegate type for (int* p) => *p is emitted as internal delegate int <>f__AnonymousDelegate0(int*).

The PR includes changes to edit-and-continue where the new delegate types are matched across edits by comparing delegate type names and signatures, and where a signature change, which should be rare, is considered a rude edit.

Fixes #55217.

@ghost ghost added the Area-Compilers label Oct 21, 2021
@cston cston force-pushed the 55217 branch 2 times, most recently from f2e1ae5 to b28620d Compare November 2, 2021 15:14
@cston cston force-pushed the 55217 branch 2 times, most recently from 18b3e97 to d141ba4 Compare December 17, 2021 22:57
@cston cston marked this pull request as ready for review December 18, 2021 00:14
@cston cston requested a review from a team as a code owner December 18, 2021 00:14
@cston cston requested review from davidwengier and tmat December 18, 2021 00:14
@cston
Copy link
Copy Markdown
Contributor Author

cston commented Dec 27, 2021

@dotnet/roslyn-compiler, please review, thanks.

@tmat
Copy link
Copy Markdown
Member

tmat commented Dec 27, 2021

Why not use hash of the signature display string instead of an index in the synthesized name?


In reply to: 1001717281

@cston
Copy link
Copy Markdown
Contributor Author

cston commented Dec 28, 2021

Why not use hash of the signature display string instead of an index in the synthesized name?

Using an index (based on source order) matches the naming for anonymous types, and it seemed straightforward and sufficient to cover the scenarios.

@tmat
Copy link
Copy Markdown
Member

tmat commented Dec 28, 2021

Yeah, but it does not work with EnC.


In reply to: 1001893183

return (NamedTypeSymbol?)value.Delegate?.GetInternalSymbol();
if (delegateTemplate.HasFixedTypes)
{
TryFindAnonymousDelegateWithFixedType(delegateTemplate, out var value);
Copy link
Copy Markdown
Contributor

@AlekseyTs AlekseyTs Dec 31, 2021

Choose a reason for hiding this comment

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

TryFindAnonymousDelegateWithFixedType

Was the logic backwards before? It looks like the condition didn't change its meaning, but the branches got flipped. #Closed

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The branches were flipped because HasFixedTypes == !IsParameterizedDelegateType.

@jcouv jcouv self-assigned this Jan 4, 2022
Copy link
Copy Markdown
Contributor

@AlekseyTs AlekseyTs left a comment

Choose a reason for hiding this comment

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

LGTM (commit 16)

template.Construct(typeParameters);
}

static bool allValidTypeArguments(AnonymousTypeDescriptor typeDescr)
Copy link
Copy Markdown
Member

@jcouv jcouv Jan 6, 2022

Choose a reason for hiding this comment

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

The name of the local function doesn't seem to match its purpose #Closed

{
typeArgumentsBuilder.Add(fields[i].TypeWithAnnotations);
var xOwner = x.ContainingSymbol!;
var yOwner = y.ContainingSymbol!;
Copy link
Copy Markdown
Member

@jcouv jcouv Jan 6, 2022

Choose a reason for hiding this comment

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

var yOwner = y.ContainingSymbol!;


Can remove the nullable suppressions? #Closed

TypeSymbol? voidReturnTypeOpt,
int parameterCount,
RefKindVector refKinds)
: base(manager, Location.None) // Location is not needed since NameAndIndex is set explicitly below.
Copy link
Copy Markdown
Member

@jcouv jcouv Jan 6, 2022

Choose a reason for hiding this comment

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

Would it make sense to split the base constructor (one that takes a NameAndIndex and one that takes a Location)? #Closed

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It's not ideal that the two constructors here have different ways to set NameAndIndex, but it might be better to leave that difference here rather than change the base type.

RefKindVector refKinds)
: base(manager, Location.None) // Location is not needed since NameAndIndex is set explicitly below.
{
Debug.Assert(refKinds.IsNull || parameterCount == refKinds.Capacity - (voidReturnTypeOpt is { } ? 0 : 1));
Copy link
Copy Markdown
Member

@jcouv jcouv Jan 6, 2022

Choose a reason for hiding this comment

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

voidReturnTypeOpt is { }

nit: consider extracting the various null checks on voidReturnTypeOpt in this method to a local (returnsVoid or some such). #Closed

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'll leave as is because this constructor was essentially just moved from SynthesizedDelegateSymbol.

Debug.Assert((ordinal == 0) == (initialBaseline == null));
Debug.Assert((ordinal == 0) == (anonymousTypeMap == null));
Debug.Assert((ordinal == 0) == (synthesizedDelegates == null));
Debug.Assert((ordinal == 0) == (anonymousDelegates == null));
Copy link
Copy Markdown
Member

@davidwengier davidwengier Jan 6, 2022

Choose a reason for hiding this comment

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

Nit: Should there be a corresponding assert here for anonymousDelegatesWithFixedTypes? #Resolved

changedTypes: changedTypes.ToImmutableAndFree());
}

private static bool ContainsAnonymousDelegates(
Copy link
Copy Markdown
Member

@davidwengier davidwengier Jan 6, 2022

Choose a reason for hiding this comment

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

Nit: Naming seems off to me. This checks for changes to delegates, not just whether they exist, right? Or does the current name mean "Contains all existing anonymous delegates"? #Resolved

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The method checks the new collection contains all existing anonymous delegates. Renamed to ContainsPreviousAnonymousDelegates to clarify.

}
}

internal AnonymousDelegateTemplateSymbol(AnonymousTypeManager manager, AnonymousTypeDescriptor typeDescr, ImmutableArray<TypeParameterSymbol> typeParametersToSubstitute)
Copy link
Copy Markdown
Member

@jcouv jcouv Jan 6, 2022

Choose a reason for hiding this comment

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

nit: xml doc on these constructors would be useful. This constructor is for non-generic case, and above is generic case #Closed

@".class public A`1<T>
{
.method public hidebysig specialname rtspecialname instance void .ctor() cil managed { ret }
.method public static void F1(int32* modopt(class A`1<!T>) i) { ret }
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Consider expanding this test to observe the modopt in the synthesized delegate type

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The modopt is used to test that the resulting delegate is generic only (the type parameter is referenced in the modopt only), but the resulting delegate does not include a modopt.

internal override bool HasPossibleWellKnownCloneMethod() => false;
}

internal sealed class SynthesizedDelegateConstructor : SynthesizedInstanceConstructor
Copy link
Copy Markdown
Member

@jcouv jcouv Jan 6, 2022

Choose a reason for hiding this comment

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

Given that SynthesizedDelegateConstructor is the only type left in this file, it'd be good to rename the file #Closed

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'm reluctant to rename the file with changes in case that makes it difficult to track the history.

Assert.Equal("<>9__0_2", field3.Name);

var matcher = new CSharpSymbolMatcher(null, synthesizedDelegates0, compilation1.SourceAssembly, emitContext, peAssemblySymbol0);
var matcher = new CSharpSymbolMatcher(null, synthesizedDelegates0, null, compilation1.SourceAssembly, emitContext, peAssemblySymbol0);
Copy link
Copy Markdown
Member

@jcouv jcouv Jan 6, 2022

Choose a reason for hiding this comment

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

Feels like we should add some SymbolMatcher tests for delegates with fixed types #Closed

if (ReferenceEquals(template.Manager, this) && !template.HasFixedTypes)
{
builder.Add(template.Delegate);
builder.Add(template);
Copy link
Copy Markdown
Member

@jcouv jcouv Jan 6, 2022

Choose a reason for hiding this comment

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

nit: Consider adding an assertion that template.SmallestLocation is Location.None (which explains why we use the SynthesizedDelegateSymbolComparer comparer below) #Closed

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

There's actually no requirement that SmallestLocation is Location.None. But we are able to compare by MetadataName because the name is based only on the signature of the delegate type, not it's source order.

}

internal IReadOnlyDictionary<CodeAnalysis.Emit.SynthesizedDelegateKey, CodeAnalysis.Emit.SynthesizedDelegateValue> GetSynthesizedDelegates()
internal IReadOnlyDictionary<CodeAnalysis.Emit.SynthesizedDelegateKey, CodeAnalysis.Emit.SynthesizedDelegateValue> GetAnonymousDelegates()
Copy link
Copy Markdown
Member

@jcouv jcouv Jan 6, 2022

Choose a reason for hiding this comment

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

nit: consider renaming GetAnonymousDelegatesWithoutFixedTypes (and propagate up into PEDeltaAssemblyBuilder and EmitBaseline) #Closed

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'd considered that but slightly preferred having a simpler name for the more common delegate types.

Copy link
Copy Markdown
Member

@jcouv jcouv left a comment

Choose a reason for hiding this comment

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

Done with review pass (iteration 16)

@cston cston changed the base branch from main to release/dev17.1 January 6, 2022 06:28
Copy link
Copy Markdown
Member

@jcouv jcouv left a comment

Choose a reason for hiding this comment

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

LGTM Thanks (iteration 19)

@cston cston changed the base branch from release/dev17.1 to main January 7, 2022 23:34
@cston cston merged commit 09af9e4 into dotnet:main Jan 10, 2022
@ghost ghost added this to the Next milestone Jan 10, 2022
@cston cston deleted the 55217 branch January 10, 2022 18:42
@RikkiGibson RikkiGibson modified the milestones: Next, 17.2.P1 Feb 4, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Delegate types are only synthesized for lambdas with parameter types and return type that are valid type arguments

6 participants