dart-lang / language

Design of the Dart language
Other
2.65k stars 202 forks source link

Do we allow static or library members whose implementation is omitted? #4008

Open eernstg opened 1 month ago

eernstg commented 1 month ago

Thanks to @sgrekhov for bringing up this topic! For an instance member we're allowed to use the existing syntax of an abstract declaration to omit the implementation, which is then provided by an augmentation:

class A {
  void method(); // OK, implementation provided below.
  int i; // OK, "implementation" provided below.
}

augment class A {
  augment method() {}
  augment i = 1;
}

A similar approach could be used with static members, except that it is currently a syntax error:

class B {
  static void method(); // Syntax error!
  static final int i; // Syntax error!
}

augment class B {
  augment static void method() {}
  augment static final i = 1;
}

It seems reasonable to allow this. We do not have a terminology for this usage (the first declaration of B.method isn't abstract), but it is hardly appropriate to say that the first declaration of A.method is "abstract", either. So we'd need to talk about these declarations as unimplemented or something like that, and then we'll only be able to tell whether the effective declaration of a locally unimplemented member is actually abstract or not when we know more (it must be an instance member and it must be true that every augmentation of it is locally unimplemented).

Another case which is similar is final library variables and library functions:

final int i; // OK because of the augmentation.
augment final i = 1;

void foo(int i); // OK because of the augmentation.
augment foo(i) => print(i);

These are currently syntax errors, but they seem to be motivated by the same considerations as the previous cases.

@dart-lang/language-team, do you wish to modify the grammar to allow unimplemented static members?

Edit, Aug 6: Added some library member cases, adjusted the title.

jakemac53 commented 1 month ago

Yes, this came up recently in another issue as well. But we do generally want to allow top level and static methods with no body (as long as they are augmented with one).

lrhn commented 1 month ago

Yes we do.

In a post augmentation world, a function declaration never needs to have a body, since one can be applied by a later augmenting function declaration.

The rules today are based on the declarations uniquely defining the semantic entity, and since a static function cannot be without a body (possibly external), we didn't allow a declaration without a body. With augmentations, only the final, fully augmented, function definition needs to have a body, no individual declaration or augmenting declaration needs to contain everything that is required of the final result.

eernstg commented 1 month ago

I added some library member cases, they are also currently syntax errors, and the motivation for supporting them is similar to the case for static members with no implementation.

eernstg commented 1 month ago

Sounds like we could have support for these generalizations in the language team. We would then need to update the grammar in several locations.

lrhn commented 1 month ago

Are there any other places where we need to change something in the grammar, other than allowing ; instead of a body for any non-local declaration with a body? Including constructors. We may just need to put a ? on ​`external' for those, or change (`external' `static'?)? to ​`external'? `static'?.

(Do we have grammar that assumes that, fx, that a static final variable must have an initializer?)

eernstg commented 1 month ago

Do we have grammar that assumes that, fx, that a static final variable must have an initializer?

Yes, that's currently a property of the specified grammar. The implementations may well treat the situation differently.

lrhn commented 1 month ago

Could we restructure the grammar, like, completely? It's mildly annoying that the grammar for a function declaration doesn't have a name, it starts at the (<metadata> <memberDeclaration>)* of fx <classDeclaration>, and includes only two of the <memberDeclaration> options. I could be easier to talk about this if the grammar matched the concepts more directly. Consider:

<classDeclaration> ::= ... <memberDeclaration>* ...
<memberDeclaration> ::=
     <functionDeclaration> 
  | <getterDeclaration>
  | <setterDeclaration>
  | <variableDeclaration>
  | <constructorDeclaration>

<functionDeclaration> ::=
  <metadata> `external`? `static`? <functionSignature> <functionBodyOpt>

<functionBodyOpt> ::= <functionBody> | `;`

<functionSignature> ::= 
  <type>? <functionName> <formalParameterPart>

<functionName> ::= <identifier> | `operator` <operator>

and then we can use the same <functionDeclaration> everywhere, including top-level, just by saying that it's a compile-time error to use static if not occurring as a member, external together with a <functionBody> and an operator-name if not occurring as a member or together with static. (And now, not an immediate error if static and not having a body.)

One declaration that contains all of what a function declaration is, and much easier to talk about, rather than having six different parse traces through a number of declarations that each correspond to a function declaration.

Not intended to change what a valid program is, just restructuring the specification to align with the concepts we need to talk about. (I'll be happy to write it.)

Maybe it's not so bad if I actually have to treat all the members anyway, iterating through all the possible productions, it's talking about a "function declaration" by itself that requires you to extract a sub-grammar from the language grammar for what the (<metadata> <classMember>) actually matched when it matches a function declaration.

eernstg commented 1 month ago

I think we can focus on how to change the grammar and/or specification separately if and when we have a decision about the overall principle.

The overall principle here would be that the grammar supports unimplemented versions of all variable and function declarations, because the ones that can't be abstract can still be unimplemented and not an error now, due to augmentations.

If we do agree on this then I'm sure the generalization that uses the same <functionDeclaration> everywhere will be an attractive approach.

jakemac53 commented 1 month ago

The overall principle here would be that the grammar supports unimplemented versions of all variable and function declarations, because the ones that can't be abstract can still be unimplemented and not an error now, due to augmentations.

Yes, I agree.

eernstg commented 2 weeks ago

Note that there is a certain overlap with https://github.com/dart-lang/language/issues/4060, which is also about the ability to omit "implementation" parts of a declaration, and providing them in augmentations.