Skip to content

Enable a default implementation of an interface method to be provided as part of its declaration.#17927

Merged
AlekseyTs merged 4 commits intodotnet:features/DefaultInterfaceImplementationfrom
AlekseyTs:DII_methods
Mar 20, 2017
Merged

Enable a default implementation of an interface method to be provided as part of its declaration.#17927
AlekseyTs merged 4 commits intodotnet:features/DefaultInterfaceImplementationfrom
AlekseyTs:DII_methods

Conversation

@AlekseyTs
Copy link
Contributor

@dotnet/roslyn-compiler Please review.

@AlekseyTs
Copy link
Contributor Author

@dotnet/roslyn-compiler Please review.

}
}

/// <summary>
Copy link
Member

Choose a reason for hiding this comment

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

runtime [](start = 26, length = 7)

Please clarify "target runtime"

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Please clarify "target runtime"

Will do.

implicitImpl = interfaceMember;
}
}

Copy link
Member

Choose a reason for hiding this comment

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

This is correct only for the simplest scenario. This doesn't take into account interface member overrides appearing in interfaces. Please add a PROTOTYPE comment.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Overrides aren't supported at all, there is no need to add prototype comment for such cases.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There is absolutely no risk to forget adjusting this place once the support is added, unless there would be no testing, which won't happen.

@gafter
Copy link
Member

gafter commented Mar 17, 2017

            if (!isInterface)

Per spec, private and static are now permitted on an interface method. Please add a .work. file, or a PROTOTYPE comment here.


Refers to: src/Compilers/CSharp/Portable/Symbols/Source/SourceMemberMethodSymbol.cs:777 in 38176cb. [](commit_id = 38176cb, deletion_comment = False)

if (containingTypeIsInterface)
{
mods = (mods & ~DeclarationModifiers.AccessibilityMask) | DeclarationModifiers.Public | DeclarationModifiers.Abstract;
mods = (mods & ~DeclarationModifiers.AccessibilityMask) | DeclarationModifiers.Public |
Copy link
Member

Choose a reason for hiding this comment

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

mods [](start = 16, length = 4)

This is no longer correct (a private interface method is not public or virtual, for example, and a static method is not virtual).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is not a goal of this PR to enable private or static members in interfaces. No one is making a claim that the entire spec is implemented. Please focus on the subject of this PR.

Copy link
Member

Choose a reason for hiding this comment

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

I don't see either a PROTOTYPE or a .work. tracking this issue. Given that you are updating AddImpliedModifiers, you should probably do so in a way that complies with the feature spec. Or track what isn't done.


In reply to: 106702810 [](ancestors = 106702810)

else
{
diagnostics.Add(ErrorCode.ERR_InterfaceMemberHasBody, location, this);
}
Copy link
Member

Choose a reason for hiding this comment

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

This is not correct; an accessor is permitted to have a body.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As PR title clearly states, only methods are supported, other kinds of members are NYI. No one is making a claim that the entire spec is implemented. Please focus on the subject of this PR.

Copy link
Member

Choose a reason for hiding this comment

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

It might be helpful to add a quick note of what is supported and what is NYI. A small example might be enough. I was also under impression that the change enables more than literally stated in the subject.

Copy link
Member

Choose a reason for hiding this comment

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

The comment applies to the PR, not to a particular piece of code.

Copy link
Contributor Author

@AlekseyTs AlekseyTs Mar 17, 2017

Choose a reason for hiding this comment

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

The comment applies to the PR, not to a particular piece of code.

I will make the current level of support clear in the feature markdown file.

// (13,23): error CS0737: 'Derived' does not implement interface member 'I1.M2()'. 'Derived.M2()' cannot implement an interface member because it is not public.
// class Derived : Base, I1
Diagnostic(ErrorCode.ERR_CloseUnimplementedInterfaceMemberNotPublic, "I1").WithArguments("Derived", "I1.M2()", "Derived.M2()").WithLocation(13, 23),
// (13,23): error CS0737: 'Derived' does not implement interface member 'I1.M1()'. 'Base.M1()' cannot implement an interface member because it is not public.
Copy link
Member

Choose a reason for hiding this comment

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

This diagnostic does not appear to be correct. Derived does implement I1.M2() by inheriting the default implementation.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This behavior is in agreement with the decision made at the LDM. I specifically presented this scenario and the decision was that for now we continue reporting an error in this situation.

Diagnostic(ErrorCode.ERR_CloseUnimplementedInterfaceMemberNotPublic, "I1").WithArguments("Derived", "I1.M2()", "Derived.M2()").WithLocation(13, 23),
// (13,23): error CS0737: 'Derived' does not implement interface member 'I1.M1()'. 'Base.M1()' cannot implement an interface member because it is not public.
// class Derived : Base, I1
Diagnostic(ErrorCode.ERR_CloseUnimplementedInterfaceMemberNotPublic, "I1").WithArguments("Derived", "I1.M1()", "Base.M1()").WithLocation(13, 23)
Copy link
Member

Choose a reason for hiding this comment

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

Diagnostic [](start = 16, length = 10)

Same here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This behavior is in agreement with the decision made at the LDM. I specifically presented this scenario and the decision was that for now we continue reporting an error in this situation.

// (7,15): error CS0736: 'Test1' does not implement interface member 'I1.M1()'. 'Test1.M1()' cannot implement an interface member because it is static.
// class Test1 : I1
Diagnostic(ErrorCode.ERR_CloseUnimplementedInterfaceMemberStatic, "I1").WithArguments("Test1", "I1.M1()", "Test1.M1()").WithLocation(7, 15)
);
Copy link
Member

Choose a reason for hiding this comment

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

The diagnostic is misleading at best, and possibly incorrect. The second sentence is true, but the first is not. It isn't clear that an error is required under these circumstances (though a warning might be helpful).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Same here.

compilation1.VerifyDiagnostics(
// (8,15): error CS8152: 'Test1' does not implement interface member 'I1.M2()'. 'Test1.M2()' cannot implement 'I1.M2()' because it does not return by value
// class Test1 : I1
Diagnostic(ErrorCode.ERR_CloseUnimplementedInterfaceMemberWrongRefReturn, "I1").WithArguments("Test1", "I1.M2()", "Test1.M2()", "value").WithLocation(8, 15),
Copy link
Member

Choose a reason for hiding this comment

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

Diagnostic [](start = 16, length = 10)

While I agree that there is an error here, the first sentence is misleading and should not be reported.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This behavior is in agreement with the decision made at the LDM. I specifically presented this scenario and the decision was that for now we continue reporting an error in this situation.

Copy link
Member

Choose a reason for hiding this comment

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

@AlekseyTs You do not appear to have read my comment. I agree that it is an error. What I said is that the specific diagnostic being reported is very misleading.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What I said is that the specific diagnostic being reported is very misleading.

To me, it doesn't look misleading or more misleading than in scenarios, in which it was reported before. I will capture your concern in the feature file.

Copy link
Member

@gafter gafter Mar 17, 2017

Choose a reason for hiding this comment

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

This scenario should probably not be an error, by analogy with the following, which is not an error:

interface IA
{
    int M();
}
class Base: IA
{
    int IA.M() => 1;
}
class Derived: Base, IA
{
    static int x;
    public ref int M() => ref x;
}

Copy link
Member

Choose a reason for hiding this comment

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

(I edited the above response)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In this case the base class explicitly states that it implements the interface, so it doesn't look similar to me. In any case, I've made a note to go over these scenarios at LDM again.

Copy link
Member

Choose a reason for hiding this comment

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

A method that is an explicit interface implementation "implements the interface" with respect to that method.

Diagnostic(ErrorCode.ERR_CloseUnimplementedInterfaceMemberWrongRefReturn, "I1").WithArguments("Test1", "I1.M2()", "Test1.M2()", "value").WithLocation(8, 15),
// (8,15): error CS0738: 'Test1' does not implement interface member 'I1.M1()'. 'Test1.M1()' cannot implement 'I1.M1()' because it does not have the matching return type of 'void'.
// class Test1 : I1
Diagnostic(ErrorCode.ERR_CloseUnimplementedInterfaceMemberWrongReturnType, "I1").WithArguments("Test1", "I1.M1()", "Test1.M1()", "void").WithLocation(8, 15)
Copy link
Member

Choose a reason for hiding this comment

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

ERR_CloseUnimplementedInterfaceMemberWrongReturnType [](start = 37, length = 52)

Ditto.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This behavior is in agreement with the decision made at the LDM. I specifically presented this scenario and the decision was that for now we continue reporting an error in this situation.

Copy link
Member

Choose a reason for hiding this comment

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

@AlekseyTs You do not appear to have read my comment. I agree that it is an error. What I said is that the specific diagnostic being reported is very misleading.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What I said is that the specific diagnostic being reported is very misleading.

Doesn't look like this to me, but I will capture your concern in the feature file.

Copy link
Member

Choose a reason for hiding this comment

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

This should probably not be an error, by analogy with the following, which is accepted today:

interface IA
{
    void M();
}
class Base: IA
{
    void IA.M() { }
}
class Derived: Base, IA
{
    public int M() => 1;
}

@AlekseyTs
Copy link
Contributor Author

Per spec, private and static are now permitted on an interface method. Please add a .work. file, or a PROTOTYPE comment here.

No one is making a claim that the entire spec is implemented. Please focus on the subject of this PR. Private or static members are not supported in interfaces at all. There is no need to sprinkle code with prototype comments about that fact.

{
// PROTOTYPE(DefaultInterfaceImplementation): Implement real detection if the feature is supported by the runtime
// For now we assume that it is supported by default
_lazyRuntimeSupportsDefaultInterfaceImplementation = ThreeState.True;
Copy link
Member

Choose a reason for hiding this comment

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

Just to confirm, we don't need Interlocked.CompareExchange for integer types?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That is correct.

Copy link
Member

@agocke agocke Mar 18, 2017

Choose a reason for hiding this comment

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

Don't we want this write to be immediately visible to other threads?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Don't we want this write to be immediately visible to other threads?

In case of a very small chance of that not happening, the behavior is still going to be correct. This is a common pattern in both compilers around structures that are guaranteed to be written atomically.

Copy link
Member

@VSadov VSadov Mar 19, 2017

Choose a reason for hiding this comment

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

"Immediately visible" is an inconvenient term. Memory writes are immediately visible, but that is not enough in many cases because of read/write ordering.

The important part here is consensus - all readers read the same value. We have that in this pattern.

Yes, per our memory model, write/read of an int is atomic. And since we deal with a single value assigned the same value, the ordering is irrelevant.

ERR_InvalidPreprocessingSymbol = 8301,
ERR_FeatureNotAvailableInVersion7_1 = 8302,

ERR_RuntimeDoesNotSupportDefaultInterfaceImplementation = 8401,
Copy link
Member

Choose a reason for hiding this comment

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

You may want to pick a different range (8500 probably) to avoid conflict with Omar/Vlad's branch on readonly refs.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You may want to pick a different range (8500 probably) to avoid conflict with Omar/Vlad's branch on readonly refs.

Thanks, will do.

Copy link
Member

Choose a reason for hiding this comment

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

Thanks. We have learned already that reserving a noncontiguous block of error ids while developing is good. We can "compress" the error id's later when merging back to master.

compilation1.VerifyDiagnostics(
// (4,17): error CS0106: The modifier 'static' is not valid for this item
// static void M1()
Diagnostic(ErrorCode.ERR_BadMemberFlag, "M1").WithArguments("static").WithLocation(4, 17),
Copy link
Member

Choose a reason for hiding this comment

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

Diagnostic [](start = 16, length = 10)

This is not correct per the feature spec.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No one is making a claim that the entire spec is implemented. Please focus on the subject of this PR. Private or static members are not supported in interfaces at all. The behavior is correct given the current state of the product.

public async Task TestDoNotReorderComImportMembers()
public async Task TestDoNotReorderComImportMembers_01()
{
await TestInRegularAndScriptAsync(
Copy link
Member

Choose a reason for hiding this comment

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

await [](start = 12, length = 5)

Can you please explain the relationship of this test to the rest of the PR?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Can you please explain the relationship of this test to the rest of the PR?

There used to be one test for implementing an interface, I cloned it to cover scenarios when there is and there isn't default implementation for interface methods.

{
interfaceLocation = implementingType.Locations[0];
}
Location interfaceLocation = GetInterfaceLocation(interfaceMember, implementingType);
Copy link
Member

Choose a reason for hiding this comment

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

The code here previously handled null implementingType. But now it asserts that is it non-null.
I looked at the one caller for the ReportImplicitImplementationMismatchDiagnostics, but didn't see how it guarantees non-null implementingType here.

Copy link
Contributor Author

@AlekseyTs AlekseyTs Mar 17, 2017

Choose a reason for hiding this comment

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

The code here previously handled null implementingType.

You mean it did it by dereferencing it (look what the else block was doing)?

But now it asserts that is it non-null.
I looked at the one caller for the ReportImplicitImplementationMismatchDiagnostics, but didn't see how it guarantees non-null implementingType here.

The lookup is always performed in some type, that is the implementingType.

Copy link
Member

Choose a reason for hiding this comment

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

Ah. Makes sense.
Consider duplicating or moving the assert up into ReportImplicitImplementationMismatchDiagnostics or even its caller?

{
Debug.Assert((object)implementingType != null);
var @interface = interfaceMember.ContainingType;
SourceMemberContainerTypeSymbol snt = implementingType as SourceMemberContainerTypeSymbol;
Copy link
Member

@jcouv jcouv Mar 17, 2017

Choose a reason for hiding this comment

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

Nit: What's the n in this acronym? #Closed

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nit: What's the n in this acronym?

This was an extract method refactoring. I didn't change the line, or the names.

/// If it is not, a diagnostic is added with the current type.
/// </summary>
protected void CheckModifiersForBody(Location location, DiagnosticBag diagnostics)
protected void CheckModifiersForBody(SyntaxNode syntax, Location location, DiagnosticBag diagnostics)
Copy link
Member

Choose a reason for hiding this comment

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

syntax [](start = 56, length = 6)

Nit: could you leave a comment or maybe rename the parameters to clarify what is syntax pointing out versus location?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nit: could you leave a comment or maybe rename the parameters to clarify what is syntax pointing out versus location?

Will rename.

@jaredpar
Copy link
Member

No one is making a claim that the entire spec is implemented. Please focus on the subject of this PR. Private or static members are not supported in interfaces at all.

I agree we need to keep our focus on the intent of the PR.

At the same time though feature branches should always be in a known state. That way absent minded managers can quickly check where a given feature stands. Once this PR is merged it's going to be hard to tell what the current state of the feature is by looking at the branch.

I think we should either add comments for the unimplemented state or clearly document the present state of the implementation in the feature markdown file / feature issue. Either is fine by me.

compilation1.VerifyDiagnostics();
Assert.True(m1.IsMetadataVirtual());

CompileAndVerify(compilation1, verify:false,
Copy link
Member

@jcouv jcouv Mar 17, 2017

Choose a reason for hiding this comment

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

It looks like PEVerify fails, so it is skipped for now. Should we file a follow-up issue? #Closed

Copy link
Contributor Author

@AlekseyTs AlekseyTs Mar 17, 2017

Choose a reason for hiding this comment

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

It looks like PEVerify fails, so it is skipped for now. Should we file a follow-up issue?

Regular runtime doesn't support the feature. Once we have runtime we can test against, new tests will have to be written. I will make it clear in the feature markdown file. #Resolved

@AlekseyTs
Copy link
Contributor Author

I think we should either add comments for the unimplemented state or clearly document the present state of the implementation in the feature markdown file / feature issue. Either is fine by me.

I will make it clear in the feature markdown file.

if (implicitImpl.ContainingType.IsInterface && implementingType.ContainingModule != implicitImpl.ContainingModule)
{
// PROTOTYPE(DefaultInterfaceImplementation): Should we check language version as well?
// Usually, it is done based on specific syntax that targets a new feature, but in this 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 we use language version in binding, in rare cases.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think we use language version in binding, in rare cases.

That is true, but it still in response to specific syntax that wouldn't be valid for prior version of the language. In this case, there is no special syntax used.

Copy link
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

switch (feature)
{
// C# 7.1 features.
case MessageID.IDS_DefaultInterfaceImplementation:
Copy link
Member

Choose a reason for hiding this comment

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

This is not correct; this feature is targeting 8.0 or later per LDM. Probably a PROTOTYPE comment is good enough.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Will make a note in the feature file.

@AlekseyTs
Copy link
Contributor Author

@dotnet/roslyn-compiler I believe all feedback provided so far has been addressed. Please take a look.

@gafter
Copy link
Member

gafter commented Mar 17, 2017

I asked @MadsTorgersen to check on what his LDM notes and memory say about this situation:

interface IA
{
    void M() {}
}
class Derived: IA
{
    private void M() { }
}

Mads said that he does not recall it being discussed, and his LDM notes do not show it being discussed. However, he believes it should behave the same way that this behaves:

interface IA
{
    void M();
}
class Base: IA
{
    void IA.M() { }
}
class Derived: Base, IA
{
    private void M() { }
}

I agree with that: in either case Derived has a most specific implementation of IA.M, and so IA is fully implemented by Derived. The method Derived.M is simply not a candidate to implement IA.M and is unrelated to it.

This treatment is also desirable to minimize the break that could be introduced by adding a new default method to an existing interface.

If there is any controversy regarding this treatment we could bring it to the LDM for an explicit decision, but I don't see any need for that at this time.

Copy link
Member

@gafter gafter left a comment

Choose a reason for hiding this comment

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

Tests should be modified to pass if each interface method has a most specific implementation in each non-abstract type. This is true even if the type declares a method that isn't a candidate because it is static, private, has the wrong return type, ref return mismatch, etc.

@AlekseyTs
Copy link
Contributor Author

The method Derived.M is simply not a candidate to implement IA.M and is unrelated to it.

This doesn't agree with my recollection of the LDM events. However, since I don't have a strong opinion about this behavior, I am going to adjust implementation to follow this logic for now and will bring the scenarios to LDM to get an official confirmation.

@AlekseyTs
Copy link
Contributor Author

@gafter I believe all your feedback has been addressed. Please take a look.

Copy link
Member

@agocke agocke left a comment

Choose a reason for hiding this comment

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

LGTM although I'd like us to record our memory consistency decisions in a more concrete manner

Copy link
Member

@VSadov VSadov left a comment

Choose a reason for hiding this comment

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

LGTM

@AlekseyTs AlekseyTs merged commit 50210ff into dotnet:features/DefaultInterfaceImplementation Mar 20, 2017
Copy link
Member

@gafter gafter left a comment

Choose a reason for hiding this comment

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

LGTM

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.

7 participants