Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ECMA spec addendum dealing with static interface methods #49558

Merged
merged 16 commits into from
May 14, 2021
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
390 changes: 390 additions & 0 deletions docs/design/specs/Ecma-335-Augments.md
Original file line number Diff line number Diff line change
Expand Up @@ -420,3 +420,393 @@ The algorithm is amended as follows:
**Section** "III.4.2 callvirt" is extended to allow throwing `AmbiguousImplementationException` if the implementation of the interface method resolves at runtime to more than one default interface method. It's also extended to specify throwing `EntryPointNotFoundException` if the default interface implementation is abstract.

**Section** "III.4.18 ldvirtftn" is extended to allow throwing `AmbiguousImplementationException` if the implementation of the interface method resolves at runtime to more than one default interface method. It's also extended to specify throwing `EntryPointNotFoundException` if the default interface implementation is abstract.

## Static Interface Methods

Follow proposed changes to the ECMA standard pertaining to static interface methods. The quotations and page numbers refer to
version 6 from June 2012 available at:

https://www.ecma-international.org/publications-and-standards/standards/ecma-335/

### I.8.4.4, Virtual Methods

(Add second paragraph)

Static interface methods may be marked as virtual. Valid object types implementing such interfaces shall provide implementations
AlekseyTs marked this conversation as resolved.
Show resolved Hide resolved
for these methods by means of Method Implementations (II.15.1.4). Polymorphic behavior of calls to these methods is facilitated
Copy link
Contributor

Choose a reason for hiding this comment

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

I think virtual methods don't have to be implemented. They can be, but don't have to be.

by the constrained. call IL instruction where the constrained. prefix specifies the type to use for lookup of the static interface
method.

### II.9.7, Validity of member signatures

(Edit bulleted list under **Generic type definition** at the top of page 134)

* Every instance method and virtual method declaration is valid with respect to S
* Every inherited interface declaration is valid with respect to S
* There are no restrictions on *non-virtual* static members, instance constructors or on the type's
own generic parameter constraints.

### II.9.9, Inheritance and Overriding

(Edit first paragraph by adding the word *virtual* to the parenthesized formulation *for virtual **instance** methods*)

Member inheritance is defined in Partition I, in “Member Inheritance”. (Overriding and hiding are also
defined in that partition, in “Hiding, overriding, and layout”.) This definition is extended, in an obvious
manner, in the presence of generics. Specifically, in order to determine whether a member hides (for
static or instance members) or overrides (for virtual instance methods) a member from a base class or interface,
simply substitute each generic parameter with its generic argument, and compare the resulting member
signatures. [*Example*: The following illustrates this point:

### II.9.11, Constrains on Generic Parameters
trylek marked this conversation as resolved.
Show resolved Hide resolved

(Change first paragraph)

A generic parameter declared on a generic class or generic method can be *constrained* by one or more
types (for encoding, see *GenericParamConstraint* table in paragraph II.22.21) and by one or more special
constraints (paragraph II.10.1.7). Generic parameters can be instantiated only with generic arguments that are
*assignable-to* (paragraph I.8.7.3) (when boxed) and *implements-all-static-interface-methods-of* (**paragraph
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we want to use wording used in proposed language specification at https://github.com/dotnet/csharplang/blob/main/proposals/statics-in-interfaces.md#interface-constraints-with-static-abstract-members? Specifically: "When I has static abstract members this needs to be further restricted so that T cannot itself be an interface."

Copy link
Member

Choose a reason for hiding this comment

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

That would be an additional restriction on top of this one. This is designed to cover the case where abstract classes can be used as generic parameters.

Copy link
Contributor

Choose a reason for hiding this comment

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

That would be an additional restriction on top of this one.

Could you elaborate please?

This is designed to cover the case where abstract classes can be used as generic parameters.

Abstract classes are not interfaces. Right?

Copy link
Member

Choose a reason for hiding this comment

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

So, we need to restrict the generic to fully implement the constraint. Normally, this is done by requiring that the type fully implement the interface through one of two mechanisms.

  1. The type must be a reference type, in which case any actual object instance must be a non-abstract type and therefore have a full implementation of the interfaces it and its base types claim to implement.
  2. The type must be a valuetype which must by definition cannot be abstract type, and thus satisfy the rule above.

With the addition of the static interfaces feature, instantiations over abstract classes bring along the possibility that the type may not fully implement the interface. Thus we need to protect against that.

Now, as you note, the C# language proposal suggests that we simply prevent any interface from being used as a generic for a static interface. I would prefer to word this restriction in a different way. My preference would be to requite that the type which is used as the generic parameter to provide a full set of implementations, and to then restrict the ability to use a MethodImpl on an interface type to work with these static methods.

reference needed**) each of the declared constraints and that satisfy all specified special constraints.

(Change the last paragraph on page 137)

[*Note*: Constraints on a generic parameter only restrict the types that the generic parameter may
be instantiated with. Verification (see Partition III) requires that a field, property or method that a
generic parameter is known to provide through meeting a constraint, cannot be directly
accessed/called via the generic parameter unless it is first boxed (see Partition III) or the **callvirt**,
**call** or **ldftn** instruction is prefixed with the **constrained.** prefix instruction (see Partition III). *end note*]

### II.10.3 Introducing and overriding virtual methods

(Change first paragraph)

A virtual method of a base type is overridden by providing a direct implementation of the method
(using a method definition, see paragraph II.15.4) and not specifying it to be newslot (paragraph II.15.4.2.3). An existing
method body can also be used to implement a given instance or static virtual declaration using the .override directive
(paragraph II.10.3.2).

### II.10.3.2 The .override directive

(Change first paragraph)

The .override directive specifies that a virtual method shall be implemented (overridden), in this type,
by a virtual instance method with a different name or a non-virtual static method, but with the same signature.
This directive can be used to provide an implementation for a virtual method inherited from a base class, or
a virtual method specified in an interface implemented by this type. The .override directive specifies a Method
Implementation (MethodImpl) in the metadata (§II.15.1.4).

(Change the third and fourth paragraph on page 148, the second and third one below the table)

The first *TypeSpec::MethodName* pair specifies the virtual method that is being overridden, and shall
be either an inherited virtual method or a virtual method on an interface that the current type
implements. The remaining information specifies the virtual instance or non-virtual static method that
AlekseyTs marked this conversation as resolved.
Show resolved Hide resolved
provides the implementation.

While the syntax specified here (as well as the actual metadata format (paragraph II.22.27) allows any virtual
method to be used to provide an implementation, a conforming program shall provide a virtual instance
or static method actually implemented directly on the type.
trylek marked this conversation as resolved.
Show resolved Hide resolved

### II.12 Semantics of Interfaces

(Add to the end of the 1st paragraph)

Interfaces may define static virtual methods that get resolved at runtime based on actual types involved.
trylek marked this conversation as resolved.
Show resolved Hide resolved
These static virtual methods must be marked as abstract in the defining interfaces.

### II.12.2 Implementing virtual methods on interfaces

(Edit 8th paragraph at page 158, the first unindented one below the bullet list, by
basically clarifying that "public virtual methods" only refer to "public virtual instance methods"):

The VES shall use the following algorithm to determine the appropriate implementation of an
interface's virtual abstract methods on the open form of the class:

* Create an interface table that has an empty list for each virtual method defined by
the interface.

* If the interface is an explicit interface of this class:

* If the class defines any public virtual instance methods whose name and signature
match a virtual method on the interface, then add these to the list for that
method, in type declaration order (see above). [*Note*: For an example where
the order is relevant, see Case 6 in paragraph 12.2.1. *end Note*]

* If there are any public virtual instance methods available on this class (directly or inherited)
having the same name and signature as the interface method, and whose generic type
parameters do not exactly match any methods in the existing list for that interface
method for this class or any class in its inheritance chain, then add them (in **type
declaration order**) to the list for the corresponding methods on the interface.

* If there are multiple methods with the same name, signature and generic type
parameters, only the last such method in **method declaration order** is added to the
list. [Note: For an example of duplicate methods, see Case 4 in paragraph 12.2.1. *end Note*]

* Apply all MethodImpls that are specified for this class, placing explicitly specified
virtual methods into the interface list for this method, in place of those inherited or
chosen by name matching that have identical generic type parameters. If there are
multiple methods for the same interface method (i.e. with different generic type
parameters), place them in the list in **type declaration order** of the associated
interfaces.

* If the current class is not abstract and there are any interface methods that still have
empty slots (i.e. slots with empty lists) for this class and all classes in its inheritance
chain, then the program is invalid.

### II.12.2.1, Interface implementation examples (page 159)

For now I'm inclined to add a completely separate section describing static interface
methods to this paragraph. The existing example is already quite sophisticated and
I think that expanding it even further with static interface methods would make it really
confusing.

(Add at the end of the section before the closing title "End informative text" at the bottom of page 161)

**Static interface method examples**

We use the following interfaces to demonstrate static interface method resolution:

```
interface IFancyTypeName
{
static string GetFancyTypeName();
}

interface IAddition<T>
{
static T Zero(); // Neutral element
static T Add(T a, T b);
}

interface IMultiplicationBy<T, TMultiplier>
{
static T One(); // Neutral element
static T Multiply(T a, TMultiplier b);
}

interface IMultiplication<T> : IMultiplicationBy<T, T>
{
}

interface IArithmetic<T> : IAddition<T>, IMultiplication<T>
{
}
```

We demonstrate the basic rules of static interface method resolution on several simple classes
implementing these interfaces:

```
class FancyClass : IFancyTypeName
{
public static string IFancyTypeName.GetFancyTypeName() { return "I am the fancy class"; }
}

class DerivedFancyClass : FancyClass
{
}

class GenericPair<T> : IArithmetic<GenericPair<T>>, IMultiplicationBy<GenericPair<T>, T>
{
public T Component1;
public T Component2;

public GenericPair(T component1, T component2)
{
Component1 = component1;
Component2 = component2;
}

static GenericPair<T> IAddition<GenericPair<T>>.Zero()
{
return new GenericPair<T>(0, 0);
}

static GenericPair<T> IAddition<GenericPair<T>>.Add(GenericPair<T> a, GenericPair<T> b)
{
return new GenericPair<T>(a.Component1 + b.Component1, a.Component2 + b.Component2);
}

static GenericPair<T> IMultiplicationBy<GenericPair<T>, GenericPair<T>>.One()
{
return new GenericPair<T>(1, 1);
}

static GenericPair<T> IMultiplicationBy<GenericPair<T>, GenericPair<T>>.Multiply(GenericPair<T> a, GenericPair<T> b)
{
return new GenericPair<T>(a.Component1 * b.Component1, a.Component2 * b.Component2);
}

static GenericPair<T> IMultiplicationBy<GenericPair<T>, T>.Multiply(GenericPair<T> a, T b)
{
return new GenericPair<T>(a.Component1 * b, a.Component2 * b);
}
}

class FancyFloatPair : GenericPair<float>, IFancyTypeName
{
public static string IFancyTypeName.GetFancyTypeName() { return "I am the fancy float pair"; }
}
```

Given these types and their content we can now demonstrate the resolution and behavior of
static interface methods on several simple algorithms:

```
void PrintFancyTypeName<T>()
where T : IFancyTypeName
{
Console.WriteLine("My fancy name is: {0}", T.GetFancyTypeName());
}
```

Calling `PrintFancyTypeName<DerivedFancyClass>()` should then output `I am the fancy class`
to the console. Likewise, `PrintFancyTypeName<FancyFloatPair>()` should output `I am the fancy
float pair`. In both cases the actual type parameter of the `PrintFancyTypeName` generic method
implements the `IFancyTypeName` interface and its virtual static method `GetFancyTypeName`.

**Note**: Please note that `DerivedFancyClass` implements the `IFancyTypeName.GetFancyTypeName`
method via its base class `FancyClass`. While implementing the static interface method in a
base class is fine, this design proposal doesn't address implementing static interface methods
in the interfaces themselves or in derived interfaces akin to default interface support.

```
T Power<T>(T t, uint power)
where T : IMultiplication<T>
{
T result = T.One();
T powerOfT = t;

while (power != 0)
{
if ((power & 1) != 0)
{
result = T.Multiply(result, powerOfT);
}
powerOfT = T.Multiply(powerOfT, powerOfT);
power >>= 1;
}

return result;
}
```

This is an example of polymorphic math where the underlying operators can take arbitrary
form based on the types involved - you can calculate an integral power of a byte or an int,
of a float, a double, a complex number, a quaternion or a matrix without much distinction
with regard to the underlying type, you just need to be able to carry out basic arithmetic
operations.

```
T Exponential<T, TFloat>(T exponent) where
T : IArithmetic<T>,
T : IMultiplicationBy<T, TFloat>

{
T result = T.One();
T powerOfValue = exponent;
TFloat inverseFactorial = (TFloat)1.0;
const int NumberOfTermsInMacLaurinSeries = 6;
for (int term = 1; term <= NumberOfTermsInMacLaurinSeries; term++)
{
result = T.Add(result, (IMultiplicationBy<T, TFloat>)T.Multiply(T.powerOfValue, inverseFactorial));
inverseFactorial /= term;
powerOfValue = T.Multiply(powerOfValue, exponent);
}

return result;
}
```

Another example of polymorphic maths calculating the exponential using the Taylor series,
usable for calculating the exponential of a matrix.

### II.15.2 Static, Instance and Virtual Methods (page 177)

(Clarify first paragraph)

Static methods are methods that are associated with a type, not with its instances. For
static virtual methods, the particular method to call is determined via a type lookup
based on the `constrained.` IL instruction prefix or on generic type constraints but
the call itself doesn't involve any instance or `this` pointer.

### II.22.26, MethodDef: 0x06

(Edit bulleted section "This contains informative text only" starting at the bottom of page
233):

Edit section *7.b*: Static | Virtual | !Abstract

(Add new section 41 after the last section 40:)

* 41. If the owner of this method is not an interface, then if Flags.Static is 1 then Flags.Virtual must be 0.

### II.22.27, MethodImpl: 0x19

(Edit bulleted section "This contains informative text only" at the top of the page 237)

Edit section 7: The method indexed by *MethodBody* shall be non-virtual if the method indexed
by MethodDeclaration is static. Otherwise it shall be virtual.

davidwrighton marked this conversation as resolved.
Show resolved Hide resolved
(Add new section 14 after section 13:)

* 14. If the method indexed by *MethodBody* has the static flag set, the method indexed by *MethodBody* must be indexed via a MethodDef and not a MemberRef. [ERROR]

### III.2.1, constrained. - (prefix) invoke a member on a value of a variable type (page 316)

(Change the section title to:)

III.2.1, constrained. - (prefix) invoke an instance or static method or load method pointer to a variable type

(Change the "Stack transition" section below the initial assembly format table to a table as follows:)

| Prefix and instruction pair | Stack Transition
|:----------------------------------------------|:----------------
| constrained. *thisType* callvirt *method* | ..., ptr, arg1, ... argN -> ..., ptr, arg1, ... argN
| constrained. *implementorType* call *method* | ..., arg1, ... argN -> ..., arg1, ... argN
| constrained. *implementorType* ldftn *method* | ..., ftn -> ..., ftn

(Replace the first "Description" paragraph below the "Stack Transition" section as follows:)

The `constrained.` prefix is permitted only on a `callvirt`, `call`
or `ldftn` instruction. When followed by the `callvirt` instruction,
the type of *ptr* must be a managed pointer (&) to *thisType*. The constrained prefix is designed
to allow `callvirt` instructions to be made in a uniform way independent of whether
*thisType* is a value type or a reference type.

When followed by the `call` instruction or the `ldftn` instruction,
the method must refer to a virtual static method defined on an interface. The behavior of the
`constrained.` prefix is to change the method that the `call` or `ldftn`
instruction refers to to be the method on `implementorType` which implements the
virtual static method (paragraph *II.10.3*).

(Edit the paragraph "Correctness:" second from the bottom of page 316:)

The `constrained.` prefix will be immediately followed by a `ldftn`, `call` or `callvirt`
instruction. *thisType* shall be a valid `typedef`, `typeref`, or `typespec` metadata token.

(Edit the paragraph "Verifiability" at the bottom of page 316:)

For the `callvirt` instruction, the `ptr` argument will be a managed pointer (`&`) to `thisType`.
In addition all the normal verification rules of the `callvirt` instruction apply after the `ptr`
transformation as described above. This is equivalent to requiring that a boxed `thisType` must be
a subclass of the class `method` belongs to.

The `implementorType` must be constrained to implement the interface that is the owning type of
the method. If the `constrained.` prefix is applied to a `call` or `ldftn` instruction,
`method` must be a virtual static method.

### III.3.19, call - call a method (page 342)

(Edit 2nd Description paragraph:)

The metadata token carries sufficient information to determine whether the call is to a static
(non-virtual or virtual) method, an instance method, a virtual instance method, or a global function. In all of
these cases the destination address is determined entirely from the metadata token. (Contrast this with the
`callvirt` instruction for calling virtual instance methods, where the destination address also depends upon
the exact type of the instance reference pushed before the `callvirt`; see below.)

(Edit numbered list in the middle of the page 342):

Bullet 2: It is valid to call a virtual method using `call` (rather than `callvirt`); this indicates that
the method is to be resolved using the class specified by method rather than as
specified dynamically from the object being invoked. This is used, for example, to
compile calls to “methods on `base`” (i.e., the statically known parent class) or to virtual static methods.