Skip to content

Conversation

@leafpetersen
Copy link
Member

First cut at specifying constructors in static extensions directly. This handles all of the core semantics, but currently does not allow declaring constructors in an extension using a typedef name, and currently forbids using generative constructors declared in an extension as targets of a redirecting generative constructor or as a super constructor call in another constructor. We almost certainly should reconsider the former (either before or after landing this), and may wish to reconsider the latter.

in an extension, with the same syntax as in a class and in other type
introducing membered declarations.
It is a compile-time error to declare a constructor with no type
parameters in an extension whose on-type is not regular-bounded,
Copy link
Contributor

Choose a reason for hiding this comment

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

"Constructor with no type parameters" implies that constructors may have type parameters, which should not be possible according to this change. Please let me know if I'm misreading the sentence.

Copy link
Member

@eernstg eernstg left a comment

Choose a reason for hiding this comment

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

LGTM!

The most substantial comment is probably about constant constructors, they don't seem to be covered by the description of the dynamic semantics.

I have some comments about constructor names.

Finally, I added a bunch of change requests that are only concerned with formatting (some lines in source code blocks are too long, which is not good for readability). These are marked as "Format".

Comment on lines +29 to +30
// Static members declared in an extension can be accessed by prefixing
// with the extension name.
Copy link
Member

Choose a reason for hiding this comment

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

Several of these source code snippets have comments which are cut off when the document is printed on A4 paper. This does happen on screen, too, with a smallish window. So I'm suggesting changes to make those comments so much shorter that it doesn't happen (just in case I'm not the only person on this planet who could get the idea that it would be nice to read this on paper ;-).

Format:

Suggested change
// Static members declared in an extension can be accessed by prefixing
// with the extension name.
// Static members declared in an extension can be accessed by
// prefixing with the extension name.

Comment on lines +65 to +66
// Static members on an extension are available via the on-declaration
// name
Copy link
Member

Choose a reason for hiding this comment

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

Format:

Suggested change
// Static members on an extension are available via the on-declaration
// name
// Static members on an extension are available via the
// on-declaration name

Comment on lines +69 to +70
// Static members on an extension continue to be available via the
// extension name.
Copy link
Member

Choose a reason for hiding this comment

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

Format:

Suggested change
// Static members on an extension continue to be available via the
// extension name.
// Static members on an extension continue to be available via
// the extension name.

Comment on lines +108 to +109
// Constructors declared in extensions may be invoked via the on-declaration
// name.
Copy link
Member

Choose a reason for hiding this comment

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

Format:

Suggested change
// Constructors declared in extensions may be invoked via the on-declaration
// name.
// Constructors declared in extensions may be invoked via the
// on-declaration name.

Comment on lines +112 to +113
// Constructors declared in extensions may be torn-off via the on-declaration
// name.
Copy link
Member

Choose a reason for hiding this comment

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

Format:

Suggested change
// Constructors declared in extensions may be torn-off via the on-declaration
// name.
// Constructors declared in extensions may be torn-off via the
// on-declaration name.

compatible with the return type of the constructor being declared,
which is given by the on-type of the extension.*

#### On unnamed constructors.
Copy link
Member

Choose a reason for hiding this comment

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

I know this is a popular concept.

However, I'd certainly prefer the terminology which has always been used in the language specification, that is, class C { C(); } has a constructor whose name is C, and class C { C.name(); } has a constructor whose name is C.name.

I just don't think we can be consistent if we claim that anything has a name whose spelling is the empty string.

Also, the feature specification about constructor tear-offs has the following:

A constructor declaration with declared name C.new declares a constructor named C.

If C denotes a class, C.new denotes a constructor named C of that class.

Anyway, it should certainly be possible to avoid the duplication of rules caused by constructors whose name may have either of two different forms. Isn't it sufficient to say once and for all that a constructor name is derived from <typeIdentifier> ('.' <identifierOrNew>)? (or just refer to <constructorName>, which is defined in exactly that way), and then use a symbol (say, N) that isn't limited to only one of those two forms?

Similarly, a reference to a constructor (be it in an instance creation or a tear-off) can have two forms, C.new or C.name, and they refer to N iff N is C respectively C.name.

Alternatively, I think it would work to keep talking about C.name, and specify that C denotes a type introducing membered declaration (a possibly prefixed identifier), and name stands for an identifier or new, and every instance creation of the form C(arguments) or C<typeArguments>(arguments) is treated as C.new(arguments) respectively C<typeArguments>.new(arguments).

In that there might not be a need to have this subsection at all.

denotes an invocation of a constructor declared on `E`, rather than an
explicit resolution of the instance member invocation of `m`.

#### Fully resolved invocations and references
Copy link
Member

Choose a reason for hiding this comment

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

The 'fully resolved' entities determine which extension we'd search in order to find a declaration of a constructor with the specified name, but it doesn't necessarily include type parameters, and it also doesn't necessarily omit them.

I think that's rather confusing because 'fully resolved' seems to imply that type inference has been performed on the given expression.

It would be nice if we could find a phrase that clearly communicates "we know which declaration is the enclosing declaration for the denoted constructor" (or even "we know which extension declaration is the enclosing declaration ..." if that's the only case which is relevant here). It would then be a rather natural continuation that we can use this information during subsequent steps in the static analysis.

Perhaps 'declaration resolved' or 'extension resolved' would work?

Alternatively, 'extension resolution' on an instance creation or a constructor tear-off would yield that extension E, and the synthetic terms like E.name(arguments) wouldn't be needed.

original program.*

The static type of the constructor invocation in this case is
`C<TypeArguments>`.
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 one of those situations where this mechanism will introduce a potential for covariance which would not exist if the static type were M rather than C<TypeArguments>.

However, I gave up fighting against that. It works fine as just another argument why we really need statically checked variance in the language. ;-)

in exactly the same manner as an invocation of a static generic method
in context `K`, the type parameters of which are the type parameters of
the extension; the return type of which is `R`; and the parameter
types of which are the declared parameter types of the constructor.
Copy link
Member

Choose a reason for hiding this comment

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

This doesn't seem to cover the case where the constructor is constant and redirecting, in particular during constant expression evaluation.

var p2 = Pair.fromList([3, 4]);
// A constructor provided by an extension may be torn off using the
// on-declaration name with type arguments provided explicitly.
Copy link
Member

Choose a reason for hiding this comment

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

Format:

Suggested change
// on-declaration name with type arguments provided explicitly.
// A constructor provided by an extension may be torn off using
// the on-declaration name with type arguments provided
// explicitly.

}
```

The enhancements specified for extension declarations in 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 surprising to me. Aren't all extensions required to have an on-declaration?

// Constructors declared in extensions may be torn-off via the on-declaration
// name.
Distance Function(int) fromHalf = Distance.fromHalf;
walk(fromHalf);
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 you mean walk(fromHalf(10)); here.

// Constructors declared in extensions may be torn-off via the extension
// name.
Distance Function(int) fromHalf = E1.fromHalf;
walk(fromHalf);
Copy link
Member

Choose a reason for hiding this comment

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

Again, this should be walk(fromHalf(10));.

extension E6<Y> on Map<String, Y> {
factory Map.fromString(Y y) => {y.toString(): y};
}
Constructors declared in extensions may not be used as
Copy link
Member

Choose a reason for hiding this comment

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

If we're not going to allow a constructor declared in an extension to be used as a super-initializer, or as the target of a redirecting generative constructor, then what's the point of allowing an extension to declare a redirecting generative constructor at all? Is it just so that an extension can declare a const constructor?

If so, I'm not sure that carries its weight. I would be more inclined to prohibit an extension from declaring a redirecting generative constructor, and if at some future point we decide that we want extensions to be able to declare const constructors, we do so by a more general feature that allows any factory constructor with a potentially constant body to be declared const.

{...}` where `C` is an identifier (or an identifier with an import
prefix) that denotes a class, mixin, enum, or extension type
declaration, we say that the _on-declaration_ of the extension is `C`.
Here (and throughout) we includes the case that `j` is 0,
Copy link
Member

Choose a reason for hiding this comment

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

Nit: includes -> include

}
Constructors declared in extensions may not be used as
super-initializers, nor as targets of redirecting generative
constructors.
Copy link
Member

Choose a reason for hiding this comment

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

Can they be use as the redirection target of a redirecting factory and, if so, can they be referenced through the on-declaration:

class A {
  A.a();
  factory A.b() = E.d; // Through the extension name. 
  factory A.c() = A.d; // Through the on-declaration.
}
extension E on A {
  A.d() : this.a();
}

?


At first, we establish some sanity requirements for an extension declaration
by specifying several errors.
### Syntax
Copy link
Member

Choose a reason for hiding this comment

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

What is the syntax for the constructor name in the declaration? Consider

class C<T> {
  C._();
}
typedef F<T> = C<T>;
extension E1<T> on C<T> {
  E1() : this._();
  E1.new() : this._();
  E1.named() : this._();
  C() : this._();
  C.new() : this._();
  C.named() : this._();
}
extension E1<T> on F<T> {
  E1() : this._();
  E1.new() : this._();
  E1.named() : this._();
  C() : this._();
  C.new() : this._();
  C.named() : this._();
  F() : this._();
  F.new() : this._();
  F.named() : this._();
}

Should/can the declaration use the name of the extension (E1 and E2 in the example), the on-declaration (C in the example, and/or the syntactic name in the on clause (F` in the example) ?

Copy link
Member

@lrhn lrhn 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 the model works. I'm (as usual) not happy about specification by desugaring or explanations using "is treated like". (I know, I've done it too. That didn't go well either.)

Some of the terms confuse me, maybe because I'm not sure what category they belong to. (Is a "fully resolved reference" an expression or a semantic entity, and if the latter, shy does it have a syntax)?

I still think this can be implemented without too much confusion, by people who know what is intended, but I fear for the edge cases.

generic method whose return type is given by the `on` type of the
extension and whose generic parameters are the generic parameters of
the extension declaration. The `fromList` constructor defined above
is semantically equivalent to the following generic static method:
Copy link
Member

Choose a reason for hiding this comment

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

The word "semantically" is a little vague.
Maybe "extensionally"? (Behaves the same way.)

(Unless this is intended as a desugaring semantics, and then I'm, as usual, opposed to doing that way of specifying things.)

}
Constructors declared in extensions may not be used as
super-initializers, nor as targets of redirecting generative
constructors.
Copy link
Member

Choose a reason for hiding this comment

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

Could allow if on-declaration is not generic?

Maybe say why.

When you redirect to an extension generative constructor, it may be to a constructor in an extension which has a different instantiation of the on-declaration.

When invoking a generative constructor, a new object is created, to be initialized, and all participating constructors must agree on the type of that object, invariantly.

(That suggests that we could allow generative redirecting constructors to be used as generative if the extension directly forwards its type parameters to the on-declaration. That would likely be a common case, if you have

extension MoreMap<K, V> on Map<K, V> {   
  Map.combine(Iterable<Map<K, V>> maps)
     : Map.fromEntries(
        Iterable.concatentate(maps.map((m) => m.entries));
}

then another redirecting generative constructor could safely redirt to that.
And then the non-generic case is just a trivial proper forwarding of zero type parameters.)

{...}` where `F` is a type alias whose transitive alias expansion
denotes a class, mixin, enum, or extension type `C`, we say that the
_on-declaration_ of `E` is `C`, and the declaration is treated as if
`F<T1 .. Tk>` were replaced by its transitive alias expansion.
Copy link
Member

Choose a reason for hiding this comment

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

All this is defining the on-declaration syntactically, and doing type alias expansion to get to the final declaration. Which works, as long as we can resolve identifiers to declarations, and perform type argument substitution at the syntactic level.

Maybe we could instead say if the uninstantiated on type (the type denoted by the on type clause) is a type introduced by a class, mixin, enum or extension type declaration, then that declaration is the on declaration of the extension?
At least that bypasses the type aliases, and doesn't have to handle non-generic, raw and instantiated types separately.

It would mean that we can't know the on-declaration until we have ensured that declarations introduce types, and type expressions can be resolved to the type they denote, but I think we can wait for that.
(It's probably the step after being sure that all identifiers can be resolved to a declaration of the correct kind.)

neither are record types or function types.
For the purpose of identifying the on-declaration of a given
extension, the types `void`, `dynamic`, and `Never` are not considered
to be classes, and neither are record types or function types, with
Copy link
Member

Choose a reason for hiding this comment

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

... classes, mixins, enums or extension types,

For the purpose of identifying the on-declaration of a given
extension, the types `void`, `dynamic`, and `Never` are not considered
to be classes, and neither are record types or function types, with
the exception of the types `Record` and `Function`, which are
Copy link
Member

Choose a reason for hiding this comment

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

Not really an exception, because they are not record or function types.
Maybe:

Both `Record` and `Function` themselves are classes.

(They are not just considered to be classes, they are classes.)

Comment on lines +855 to +856
the un-instantiated function type of the constructor reference with
`TypeArguments` substituted throughout for `TypeParameters`.
Copy link
Member

Choose a reason for hiding this comment

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

So it's the instantiated function type, for the extension instantiated with the inferred type arguments?

generic function or constructor.*


## Dynamic Semantics
Copy link
Member

Choose a reason for hiding this comment

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

We probably need a section on tearing off extension constructors through an alias.

Most things are unchanged. If instantiated, it just works like tearing off from the instantiated type.

For normal constructors, a tear-off through an uninstantiated generic alias has the same type parameters as the type alias. The tear-off is identical to the tear-off directly on the class if the alias is properly forwarding type arguments. (Has the same type parameters in the same order, and forwards them directly to the class.)

For an extension constructor tear-off through an uninstantiated generic alias, the function would still have the same type arguments as the alias, but it should likely be identical to the direct tear-off from the extension if the alias has the same type parameters as the extension, and forwards them to the class the same way that the extension puts them into its on type.

I think the only change would be identity, basing it not agreeing with the target type, but with the declarer of the constructor.

### Dynamic Semantics of constructors defined in extensions

Declarations of constructors defined in extensions are semantically
treated as declarations of static methods with return type given by
Copy link
Member

Choose a reason for hiding this comment

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

"treated as" again 😉 .
Are they, or are they not static methods?

That is: Does a syntactic constructor declaration in an extension introduce/define a static method, or does it introduce an entity which is not a static method?

If they are not static methods, we don't need to say that they are treated as static methods. Just say how they work, in its entirety, without falling back on "like a static function", because that will very likely leave some details unspecified that are not actually exactly like a static function.

ordinary static member of the extension by treating it as if both the
type parameters and the on-type of the extension were copied down onto
the declaration of the constructor to serve as the type parameters and
return type of the static member*
Copy link
Member

Choose a reason for hiding this comment

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

Or say:

A constructor declaration of an extension introduces a static method with the following
signature:

  • The same type parameter list as the extension
  • The uninstantiated on type of the extension as return type.
  • The same parameter list as the constructor declaration.
  • A fresh name, which we associate with the constructor declaration.

When a constructor invocation denotes the extension constructor declaration
When invoked as a object creation expression
with TypeArguments as type arguments list as arguments as argument list,
then bind the type arguments and arguments to the formal type parameters
and formal parametrs of the constructor declaration. Then

  • if the constructor declaration is a redirecting generative constructor
    with a redirecting clause this(args) or this.id(args),
    let C be the type-parameter instantiated constructor denoted by this or this.id,
    evaluate args to an argument list,
    and invoke C with that argument list as an object creation expression.
    The extension constructor invocation evaluates to the result of that invocation.
  • If the constructor is a redirecting factory constructor with redirection = T; or =T.id
    where T is a type clause, let C be the type parameter instantiated constructor
    denoted by T or T.id after type inference, with the type parameters of the
    extension replaced by the corresponding TypeArguments.
    Then invoke C with arguments as argument list as an object creation expression.
    The extension constructor invocation evaluates to the result of that invocation.
  • If the constructor is a non-redirecting factory constructor, execute the body in the
    scopes resuliting from binding actuals to formals.
    If the function body completes by returning a value, then
    the extension constructor invocation evaluates to that value.
    Otherwise the function body compled by throwing an error,
    and the extension construtor invocation threw the same error.

That is:
Say which semantic entity exist (a static function), what its properties are (function signature and name), and how it behaves when run.


#### Dynamic Semantics of fully resolved constructors

Invocations of fully resolved constructors are treated as invocations
Copy link
Member

Choose a reason for hiding this comment

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

That does not describe what happens for object creation expression (anything with a new or const in front). Those are not invocations, or if they are, they are special ("invoked to initialize object o").

We can probably say that redirecting generative extension constructors are treated "more like" forwarding factory constructors, but since they can do things that a factory constructor cannot, that's not a full description.

We can maybe say that an object construction expression invoking an extension constructor is not actually an object construction expression, it's just an invocation of a static function, and the body of that static function will then invoke another constructor as an object creation expression. That is, "more like a factory."

But that doesn't explain const invocations.

class C {
   final num i;
   const C(this.i);
}
extension on C {
  const C.add(num i, num j) : this(i + j);
  const factory C.sum(int i, int j) = C.add;
}

Neither of these constructors can be explained by desugaring to a static function, because they do things that no static function can do (even if only "be evaluated as const").

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.

6 participants