language icon indicating copy to clipboard operation
language copied to clipboard

Update feature-specification.md

Open lrhn opened this issue 2 years ago • 2 comments

Tweak a wording in Extension Type spec.

lrhn avatar Nov 16 '23 10:11 lrhn

I actually think we should define the meaning of the representation variable as a getter.

Something like:

An extension type declaration of the form extension type constopt Name <TypeArgs>opt .constructorNameopt(R id) { ... } introduces an implicit non-redirectinging generative constructor name Name/Name.constructorName with function signature Name Function(R). When invoked with one argument, the invocation valuates to that argument value. It also introduces an implicit extension type getter declaration with a member signature of R get id, which, when invoked as an extension instance member on a value v, returns the value v. For all purposes, including naming conflicts, those declarations count as if they were declared as members of the extension type.

Then we only have to define what those constructors and extension type getters do, which is something we have to do anyway for other constructors or getter declarations. So saying something like:

A non-redirecting generative constructor (of an extension type) is executed in a context with a temporary instance variable V of type R that it can, and must, initialize. An initializing formal or initializer-list assignment may initialize this variable, and no other variables are available to initializer (so the only valid initializing formal is this.id and the only initializer list assignment is to id/this.id). It's a compile-time error if the constructor would not initialize the V variable, or if it would initialize it more than once. After execution of the initializer list has completed, let v be the value that the V variable is initialized to. Then any constructor body is executed as an instance method body with this bound to the value v. Finally, the result of invoking the constructor is the value v.

That accounts for every non-redirecting generative constructor. If we defined the extension type header to introduce a non-redirecting generative constructor "equivalent to" something, it would cover that too, but as usual I prefer not to desugar.

Similarly, we need to say (and probably do) that execution of the body of an extension type method, what happens when invoking it, will happen in a scope where this is bound to the value of the receiver expression of the invocation (as normal for instance member invocations). Having done that, also specifying how you access the representation variable separately from that, is another discrepancy waiting to happen.

Take:

extension type N(num numValue) implements num {
  num get negativeNumValue => -value;
  int get intValue => this.toInt();
}
extension type I(int intValue) implements N, int {
 int get negativeIntValue => I(-intValue);
}
void main() {
  print(I(42).negativeNumValue); // Prints -42
  print(I(42).numValue); // Prints 42
}

If we don't just say that the N(num numValue) introduces an implicit extension type getter, effectively equivalent to num get numValue => this as num;, then we have to special-case every part of this to ensure that I(42).numValue works.

For example, currently:

We say that an extension type declaration DV has an extension type member named n in the case where DV declares a member named n, and in the case where DV has no such declaration, but DV has a direct extension type superinterface V that has an extension type member named n. In both cases, when this is unique, the extension type member declaration named n that DV has is said declaration.

By this description, N does not thave an extension type member named numValue since it doesn't (explicitly) declare a member named numValue. And therefore I doesn't have an extension type member named numValue.

But I should have one, which means at N should count as having an extension type member named numValue. But that requires a reference to a declaration. The path of least resisitance is to introduce that declaration (or rather, a reference to a member rather than a declaration, to include synthetic members that have no explicit declaration), and then we're just done! Everything else falls out of the normal rules for an extension type declaration with such an extension type member declaration, rules we already have.

As long as we make it perfectly clear that implicit and explicit declarations are otherwise equivalent in all following steps, which means they count towards naming conflicts in declarations, towards "has an extension type member", etc., then their actual behavior should be subsumed by the rules for such declarations that we already have. Making the primary constructor and "representation variable" into implicit declarations means we don't have to do anything again just for those. (And we won't forget a case.)

(And "because this would reasonably be specified to be a getter that returns id as Name<...>" is not how this works in Dart. For a number of reasons, not limited to this then being in the lexical scope for static members. The this is not a variable name, it's an operator which evaluates to an object set during the invocation of the member that refers to the this. Sure, like a parameter, but still not a parameter. It's not a local variable, it's not subject to promotion, which is a mistake, and it's the implicit target of unqualified invocations that are not in the lexical scope, but that's a specification artifact, they're called on the same receiver, which can also be accessed as this, we don't have to make them invoked on this itself, that was just the easy way to specify it, "as if prefixed by this.".)

Actually, more generally, we could say that a generative constructor is executed in an "initialization scope" containing a set of variables to assign a value to, some of which must be assigned a value by the constructor. For a class, the variables in that scope are the instance variables declared by the class, except those that are final and have an initializer expression, and the variable must be assigned a value if they are final or non-nullable, and not late. The constructor initializing those means that the instance variables are thus initialized when the constructor ends. For an extension type generative constructor's initialization scope is the single temporary variable named id with type R, which must be initialized. At the end of initializer-list execution, the constructor provides the object which is the initialized value created by the constructor. Then any constructor body is executed as an instance member body with this bound to the created value. That's would allow us to use the same specification of what a non-redirecting generative constructor does for both classes and extension types, without having to repeat everything for extension types as if they are a new thing, and probably forgetting something. (Repeating outselves is a good way to do so incorrectly, and have slight deviations in specified behavior between things that should have been the same, just because we wrote it twice instead of reusing. And since I also frown at specification by "works the same as a similar class would", the solution I'd prefer is abstracting over the differences and defining the shared requirements and behaviors once, then paramterizing only with the necessary differences.)

lrhn avatar Nov 17 '23 11:11 lrhn

Something like: ...

I think these are great comments, and we do need to tighten the language about the semantics of constructors!

I'm not convinced, though, that it is helpful to specify a representation getter that returns this as R, and deny the existence of the representation variable. But I do think that the representation variable should be mentioned explicitly in several locations in the feature specification where it currently isn't, because the mere reference to the representation name breaks when that identifier resolves to some other (shadowing) declaration.

eernstg avatar Nov 20 '23 11:11 eernstg