Should "augment" be optional?
The augmentations proposal currently says that the introductory declaration can't be marked augment and all augmenting declarations must be.
It's unambiguous that an augmenting declaration is augmenting: it has the same name as a previous declaration in the same augmentation context. That means we could make the keyword optional or eliminate it entirely. Should we?
Personally, I do like having it be explicit. Say you were to write:
class SomeVeryLongName { ... }
// Much later...
class SomeVeryLongName { ... }
You meant the latter to be an unrelated class but accidentally used the same name. If we allow implicit augmentation, instead of a simple name collision error, you'll get a cascade of errors for every part of the class that doesn't overlap properly. If, for example, they both happen to define methods with the same name but different signatures, you'll get errors in those methods. Perhaps worse, if they don't overlap, you'll just silently create one class that is the Frankenstein amalgamation of both.
I don't think augment is much ceremony and avoids those pitfalls. But I could be persuaded otherwise.
My reason for wanting to make augment optional is that requiring it in every declaration except the first introduces an entirely syntactic dependency on ordering.
If I have two, or more, parts of a class, and every member can have at most one implementation, then order of the parts only matter if I add mixins, and then only if those mixins are order dependent. Most of the time, order doesn't matter.
So most of the time, I can freely move parts around, and even reorder part directives ... except if I swap the first part with another, then I have to add an augment to one part and remove it from another.
The parts are symmetric, but we force you to move around a special (lack of a) marker when you move the first declaration.
I want a way to avoid that problem, for the same reasons that we allow trailing commas (to avoid a lack of a special marker on only the last element of a list).
We can either allow augment on the first declaration too, or allow not having it on other decorations, or both.
I suggest "both".
Make augment optional, and allow the first declaration too have it too.
The only way to get an error is to have augment on a single declaration, with no other declaration of the same name in the same augmentation scope. (Don't say augment if you don't actually augment. You probably typo'ed a name!)
And someone will then definitely ask for a lint to have augment on all the declarations, if there is more than one.
(Like annotate_overrides this would be annotate_augments, even if it's optional.)
Then there will likely be two schools, two styles: "Never use augment" and "Always use augment". I don't expect to see "use augment except on the first declaration" as a style anyone would actually use.
That's another hint to me: If we make augment completely free. You can use it or not use it freely, then I predict only those two dominant styles.
If we enforce one style, and it's different from both of those, I don't think we are giving people what they actually want.
We could also consider using part instead of augment. When you can't stack implementations, it feels more like separate symmetrical part declarations, than it does a layering of augmentations.
This is a really compelling argument. You're right that it introduces a pointless asymmetry. With augmentations, the introductory declaration is just as incomplete as the augmenting ones.
Then there will likely be two schools, two styles: "Never use
augment" and "Always useaugment". I don't expect to see "useaugmentexcept on the first declaration" as a style anyone would actually use.
An argument for the second style is that it means when looking at the introductory declaration, you know there may be other fishy stuff going that isn't obvious from that one declaration.
In this case, I think we should probably use partial. It will be more familiar to users coming from C#, and it's a generally more common word (about 10x more common according to Google Ngram viewer).
Then there will likely be two schools, two styles: "Never use augment" and "Always use augment". I don't expect to see "use augment except on the first declaration" as a style anyone would actually use.
Then the keyword is incorrectly named IMO.
If all declarations are named with 'augment', then that's more of a part class than augment class
augment means "update an existing declaration". But part means "this declaration is defined in multiple pieces"
I think it's also important to keep in mind code-generation.
If it's stylistic to write either of:
@annotation
augment class A{}
// OR
@annotation
class A{}
But all declarations must match this style, then the stylistic choice leaks inside generated code.
Given:
@annotation
augment class A{}
Generated code has to generate:
augment class A {...}
But for
@annotation
class A{}
Generated code has to generate:
class A {...}
That sounds like an unnecessary complication.
Also, if the keyword is optional: Do we need it at all? Similarly to @override, can't augment be an annotation?
@augment
class A{}
I wouldn't require generated code to generate a specific style, the two styles are just styles, not requirements. I just don't expect anyone to hand-craft code that uses augment on one declaration and not on another. Generated code can do whatever it likes, and I'd prefer if style checkers don't complain about generated code.
(Not that it's particularly hard for a code generator to check what the existing declaration does, and do the same thing.)
The parts are symmetric, but we force you to move around a special (lack of a) marker when you move the first declaration.
Thinking about this more, I'm not sure I still agree with it.
When I'm writing code, defining a new thing is a deliberate action I might take. Modifying or an extending an existing thing is also an action I might take. At that point in time, I definitely know which action I'm taking.
If a user writes class Foo { ... it's hard for me to imagine them thinking "I don't really know or care if there's a Foo in this library already. If there isn't, here's a new one. If there is, I guess it's fine to add this stuff to it."
Given that, it seems reasonable to me for there to be distinct syntaxes for those operations. It would be weird if there was no way to say, "I want a new class and tell me if it collides with another one."
I'm OK with the current proposal.
It has the nice benefit that it minimizes the ceremony for declarations that are modified by code generation. You just write class Foo { ... . Of course, it has the drawback that you can't tell from the introductory declaration that it's incomplete. In practice, though, I suspect that most of them will, as Remi notes, have some metadata annotation on them that sends a signal to the reader that other stuff is happening.
I can imagine having two different class declarations that both add members to the same class, without needing one of them to be before the other. They really are symmetric.
I can see the problem with declaring two classes with the same name accidentally.
So, suggestion: Make the keyword be part (it's shorter and symmetrical) and require it on all declarations that have more than one syntactic declaration.
Then you can move around parts without needing to add or remove a keyword.
That runs into the use-case of a user written class work generated augments. There you need to know how the code generation works, and you have to write part. So that sucks.
Second suggestion: Still use part, and allow at most one of a group of coinciding declarations to be without the keyword, but it doesn't have to be the first one.
Then you can write part in front of all your partial declarations if you prefer that, you can omit it if you write only one declaration, allowing code generation to implement that signature using a part declaration or a mixin, and you can omit it from the first declaration of that's how you think of it. but you can also move around parts without having to move the non-part around.
If you give it weird to have the non-marked declaration not be first, you can just write part on front of that too, removing the issue in the future.
(In sure there'll be lots for particular styles. We don't have to decide what the best style is up front.)
We discussed this at the language meeting today. There are basically two orthogonal questions:
- Which declarations can/must/can't have a modifier on them?
- What modifier do we use (
augment,part,partial, etc.)
We're focused on the first question and I'm fairly confident that the answer we pick for that will help us pick a keyword. So in the examples here, I'll use the placeholder keyword We discussed five options:
-
Omitted on first declaration, mandatory on others:
class C {} keyword class C { void foo(); } keyword class C { keyword void foo(); }This is the current proposal. It is zero ceremony for the introductory declaration. That does mean that if you look at an introductory declaration, you can't immediately tell that there may be other augmentations affecting it.
-
No modifiers at all on any declaration:
class C {} class C { void foo(); } class C { void foo(); }This is as low-ceremony you could possibly get. But it has the flaw that if you accidentally have two unrelated declarations with the same name, it will attempt to merge them. Any errors you get from that will likely be incomprehensible.
We agreed to not do this approach.
-
Optional modifier on any declaration:
class C {} keyword class C { void foo(); } class C { keyword void foo(); }You can put the modifier on the first declaration, any augmenting ones, all, some, or none. Users can have as little or as much ceremony as they want. The flaw here is that if you don't see a modifier on a declaration, that doesn't tell you anything. It doesn't reliably mean that the declaration complete. Conversely, if you do see the modifier, that doesn't tell you anything either. It could still be the only declaration with that name.
We agreed to not do this approach.
-
Mandatory modifier on all declarations:
keyword class C {} keyword class C { keyword void foo(); } keyword class C { keyword void foo(); }This is C#'s approach with partial classes. It is explicit and clear. If you see a declaration with no modifier, you know it's a complete declaration. If you see a declaration with a modifier, you know there must be others.
However, it places some mandatory ceremony on the introductory declaration. For the use case of code generators where there will likely already be some kind of metadata annotation on the introductory declaration to trigger the code generator, an extra keyword there potentially seems redundant.
This approach may also help us resolve another open issue: should there be a distinction between abstract (for overriding) and incomplete (for augmenting) member declarations (#4363)? Currently the answer is "no", and we overload a method declaration with a
;to mean either abstract or incomplete. A mandatory keyword for incomplete declarations would make that unambiguous.On the other hand, that ambiguity could be useful. If the introductory declaration has a method with no body, a code generator could either fill in that method by adding an augmenting declaration or by adding a
withclause to apply a mixin that implements that method. It's really an implementation detail of the code generator as to how that method ends up with a body. Making the introductory declaration syntax explicitly distinguish abstract and incomplete takes that freedom away from code generators. -
Mandatory on all but one of the declarations:
keyword class C {} class C { keyword void foo(); } keyword class C { void foo(); }Sort of a combination of some of the previous options. You can apply the keyword to all declarations (like option 4), or you can omit it on the first one (like option 1), or you can omit it on exactly one of the augmentations.
This gives you the freedom to treat all of declarations symmetrically (like option 4) or to make the introductory declaration special with minimum ceremony (like option 1), or make... one of the other ones low ceremony. I'm not sure exactly what the motivation for that third bit is. @lrhn would be able explain better.
It also has many of the downsides of the various approaches. The presence or absence of a keyword doesn't provide an unambiguous signal to the reader. Users now have to decide what style they prefer for which declaration to omit the keyword on, if any.
We haven't reached a conclusion yet. The options still on the table are the current approach, the C# approach, or the last hybrid option.
We'll keep discussing it.
When looking at this, I'm mainly worried about having to use the keyword on class members.
I don't care about keyword class A{}. But keyword void foo() inside classes feels odd.
IMO 1. is the ideal in terms of usability.
The 'it's unclear that the class is augmented' is IMO a non-issue when we consider:
- There's hardly any generator out there that doesn't require placing an annotation on the class to augment.
So we actually have:
The annotation is already a context clue that the class will be augmented.@annotation class Example {} - IDEs likely would generate a codelense such as "go to augmentation". We had one of those with the macro experiment. So that too is another context clue.
In 4., does that mean that all user-defined methods would have to specify keyword too? If so, that's too much for me.
I personally want to augment user methods in some of my code-generators:
@riverpod
class Example {
@mutation
int doSomething() => 0;
}
....
augment class Example {
int doSomething() {
try {
final value = augmented();
_report(value);
return value;
} catch {
_reportError(value);
rethrow;
}
}
}
It would be a real pain if users had to add part on all methods too.
... Although from the previous discussions, the augmented() inside functions may also be going away ... So I'm not sure how I'd implement this using augmentations without augmented() :shrug:
The reason for allowing up-to-one declaration without a keyword, but not requiring it to be the first, is to allow for the code generator use-case without a keyword (so one declaration without keyword), still protect against accidentally making two declarations the same (so at most one unmarked declaration) and not introducing any ordering requirement.
The language cannot distinguish that scenario from handwritten code with multiple declarations, it only sees the code after code generation has completed.
Member declaration order doesn't matter.
If you swap the order of two part files, perhaps because you renamed a file and is sorting part directives, it feels weird that you now need to move a keyword from one file to another just to satisfy an ordering relation that has no effect anyway.
Also, it allows a code generator to decide where to add its extra code. It can add a part directive with an augmenting part class, without being limited to placing it after all existing declarations.
I did a writeup of how that could be specified, with minimal requirements on, well, anything: No ordering requirements, modifiers must be repeated, most other details can be written only once, in any declaration, but must agree if repeated. (I think I'll adapt that to something that is less permissive for member declarations, fx requiring types on every declaration, because you probably won't have more than two declarations anyway - one abstract and one implementation - and the abstract one will be there to show the types, so it really just means that the implementation must repeat that type, which is good for readability, and not that cumbersome.)
I now think that requiring the unmarked declaration to be the "first" declaration (in the generalized source ordering) is acceptable (or in above-ordering, see next comment). Code generators will likely be OK with adding their augmentations after the existing declarations, they're automated tools so they'll adapt to any requirement we give them. The fix for user-written declarations, if they get out of order, is to put the keyword on all the declarations. Like adding the trailing comma to all lines, when you've done that once, the ordering problem goes away in the future.
If we have lints that can distinguish generated code from handwritten code, I'll expect a lint that requires keywords on all of multiple handwritten declarations, and that maybe requires omitting the keyword if there is only one non-generated declaration. (I expect that to be the style I'd personally use for "properly written code", and small hacky scripts don't get linted anyway.)
Based on talking to @leafpetersen, one reason it can feel arbitrary that the first declaration can omit the keyword, is that being "first" in the generalized source ordering can itself feel arbitrary. We have a defined ordering between sibling parts, but it is an accident of source ordering.
What if being first meant that the declaration must be above all other declarations? (Using the partial above/below ordering of the enhanced parts specification, rather than the complete before/after relation, where a declaration is above later declarations on the same file, and above all declarations in parts of that file, but it's not above or below declarations in sibling files.)
So define: a declaration is introducing if it is above all other declarations for the same entity. Only an introducing declaration can omit the keyword.
There can be at most one introducing declaration for a name, and if there is only one declaration for a name, it's trivially introducing. If you have declarations only in sibling parts, neither is introducing, and both/all must have the keyword.
Then the introducing declaration will not change by reordering part files, or by sorting members in a file (if the sorting is stable).
I still wouldn't require the introductory declaration to be complete (in either sense), but we could say that you cannot omit types or type parameters from an un-keyworded declaration. You can omit implementation, and you can add class-like declaration headers later (they're like the "implementation of a class" details).
The issue with this is that code generators must then put their code below the introducing declaration, which may require multiple files for a single library, up to one per leaf part file. It can't just put it all in one final part of the library file (which would be after all existing declarations of that library, but only below the declarations of the library file itself).
I guess a sufficiently smart code generation framework can handle the details, so the code generator logic can just say which declaration it wants to augment - if augmenting an initializing declaration, the augmentation can be put in a new final part file of the initializing declaration's file. (It could try to find the final-most transitive part file below that file, to combine declarations for every initializing declaration on that path in one file, but that risks running into a configurable part directive, and then it'll have to stop there anyway. And that's assuming that code generation creates new files - it could also write directly into the original file (suitably delimited by some kind of "GENERATED BY" comments, so it can be updated later).
What if being first meant that the declaration must be above all other declarations?
Ooh, I like this.
So define: a declaration is introducing if it is above all other declarations for the same entity. Only an introducing declaration can omit the keyword.
There can be at most one introducing declaration for a name, and if there is only one declaration for a name, it's trivially introducing. If you have declarations only in sibling parts, neither is introducing, and both/all must have the keyword.
Then the introducing declaration will not change by reordering part files, or by sorting members in a file (if the sorting is stable).
Oh, interesting. I was assuming that you always needed to have an introducing/introductory declaration. But what you're proposing here is that there are a few valid structures:
Trivial, solitary declaration.
class Foo {}
Main unmarked declaration above augmentations
// main.dart
part 'a.dart';
part 'b.dart';
class Foo {} // Or could write `augment` here if wanted.
// a.dart
augment class Foo {}
// b.dart
augment class Foo {}
C#-style symmetric all marked partial declarations
// main.dart
part 'a.dart';
part 'b.dart';
part 'c.dart';
// a.dart
augment class Foo {}
// b.dart
augment class Foo {}
// c.dart
augment class Foo {}
I'm not deeply opposed to this, but I do wonder if it's more generality than we need. Aside from you personally, @lrhn, do we know if there are a significant number of users who care about organizing code using the third style here?
Personally, I could live without that. But then I would be happy to keep the current proposal around this. :)
Would it not be sufficient to say that if you want to write in that C# style... just add an empty introducing declaration in the parent file?
The issue with this is that code generators must then put their code below the introducing declaration, which may require multiple files for a single library, up to one per leaf part file. It can't just put it all in one final part of the library file (which would be after all existing declarations of that library, but only below the declarations of the library file itself).
Oh, that is an issue. It would suck if moving a piece of hand-authored code from the main library to a hand-authored part broke a code generator.
A few thoughts:
-
A coworker brought up use case I hadn't considered. They have a code generator produces a new declaration. Sometimes a user wants to tweak the generated code. The obvious way to do that is writing an augmentation on the generated code.
This use case is the reverse of what we normally consider. Here, the introductory declaration is generated, and the hand-authored part is the augmentation. Does this use case affect the keyword design?
Potentially, yes. If the introductory declaration must be marked with a modifier before it can be augmented (C# style), then the code generator needs know to generate an introductory declaration with that modifier. If we make it an error for an introductory declaration to be marked and not have any sort of augmentation, then the code generator would need to know exactly which generated classes are augmented and which are not, because it has to put the keyword only on the ones the user wants to augment.
-
@rrousselGit says "When looking at this, I'm mainly worried about having to use the keyword on class members. I don't care about
keyword class A {}. Butkeyword void foo()inside classes feels odd.". This is a good point. So far, I've assumed the policy we come up with is symmetric across all kinds of augmentable declarations: classes, top-level functions, class members, etc. It may be that a policy that feels fine for classes is too onerous for members or vice-versa. I'm loath to make this corner of the design more complex, but it's worth thinking about. -
The current design eliminates
augmentedwhich makes augmenting functions and class members much less useful. You can fill in a body, but can't wrap a body. You can't really have multiple layers of augmentation on the same member.I worry that I cut too deep when I simplified the proposal and removed some of the most useful functionality. If we ever add this capability back, would that change how we think about these keywords?
Affordances
To evaluate the design, maybe it helps to think about how each choice affects the UX:
-
If an introductory declaration must have a keyword before it can be augmented, then:
-
(A) The absence of the keyword tells the reader that they are reading a complete declaration.
-
(B) Before a declaration can be augmented, the author (human or code generator) must put the keyword on it. There's no way to augment a declaration without having some ability to edit it to allow that augmentation.
-
(C) You can't accidentally create an augmentation when writing two declarations with the same name. The error reporting experience for an unintended name collision is easier to understand.
-
-
If an introductory declaration must not have a keyword if it is not augmented, then:
-
(D) The presence of the keyword tells the reader that there is something somewhere augmenting it.
-
(E) The introductory declaration fully controls whether it is augmented. The author of the introductory declaration can't unconditionally put the modifier on there and leave it up to the code generator or other part files to decide to augment or not.
-
-
If an augmenting declaration must have a keyword, then:
- (C) You can't accidentally create an augmentation when writing two declarations with the same name. The error reporting experience for an unintended name collision is easier to understand.
Are there other ways these keywords affect the language UX that I'm not thinking of?
Evaluation
Here's my take:
I think (B) and (E) are real problems. The main goal of augmentations is to able to break a declaration across multiple files that can be edited entirely independently (either by humans or code generators). That independence is hampered if the introductory declaration has to explicitly control whether or not it's augmented. It can't delegate that choice to another part file.
I think (C) is valuable. Copying and pasting declarations to make a new declaration is pretty common. If you do that and forget to rename the second one, silently merging them into one declaration will likely lead to very confusing errors.
(A) and (D) are nice but if something has to give, I'll sacrifice them. When you read a declaration, there's already a lot of implicit stuff going on. You may not realize what members a class inherits. You may not realize that a method's return type or parameter types are inferred.
If that evaluation is right, then it suggests:
-
The introductory declaration does not need to be marked. That way a later part file can choose to augment it or not. This avoids (B) and (E).
-
The augmentations must be marked. That way you can't implicitly inadvertently merge declarations. This gives (C).
Then we sacrifice (A) and (D) to get those.
Can the introductory declaration be marked? If we allow the keyword but don't require it, then users can choose whether to do the current proposal or C# style. I don't think we should support this. I think the introductory declaration should always be unmarked. My reasoning is:
-
One less thing for users to worry/argue about.
-
It makes it clear that there is one canonical introductory declaration.
-
It avoids (C) again. If you mark the introductory declaration, then copy/paste it to make a new unrelated introductory declaration but forget to rename it, this would then merge them together. If we say that the introductory declaration can't be marked, then it avoids this (admittedly minor) pitfall.
In short, I still think the current proposal is the best. We seemed fairly happy with this design back when macros were on the table and I don't see how removing macros from the picture has really changed things.
How does that sound?
A coworker brought up use case I hadn't considered. They have a code generator produces a new declaration. Sometimes a user wants to tweak the generated code. The obvious way to do that is writing an augmentation on the generated code.
IMO that is a bad way to design the API of a code-generator. Code-generators can rely on different mechanisms for customisation: Overrides, parameters on annotations, ...
My hot-take is that augmentations should be a feature that is only used by code-generator authors. A good goal IMO would be that the average Dart dev has no clue this feature exists, because they shouldn't need this. So I personally find the idea that augmentations are both for code-generators and for the sake of organisation dubious.
I wouldn't use augments to split a class apart. I'd use mixins or other OOP patterns.
So honestly I'd rather make the "reverse" scenario straight up illegal :)
I prefer to require at most one declaration to be unmarked, but the unmarked one doesn't have to be the first declaration.
If there is only one implementing declaration, then order doesn't matter (for the majority of declarations). There is no need for even the concept of an introducing declaration.
There are just multiple declarations, with at most one being implementing, and at most one being unmarked.
That allows the use cases of a human author writing a signature, and having a code-generated implementation.
It also allows an unmarked code-generated feature, and a user written augmentation (but not both implementing with this proposal).
If there are two parties involved, at least one of them must know that they're augmenting. That avoids accidental augmenting. It doesn't matter which party that is.
A coworker brought up use case I hadn't considered. They have a code generator produces a new declaration. Sometimes a user wants to tweak the generated code. The obvious way to do that is writing an augmentation on the generated code.
IMO that is a bad way to design the API of a code-generator. Code-generators can rely on different mechanisms for customisation: Overrides, parameters on annotations, ...
Agreed.
A human augmenting generated code is not a great mechanism because it's hard to make it a clear/stable API.
I would definitely steer clear of asking users to write augmentations to my generator output.
I'm not super certain what an "above relationship" mentioned by @lrhn earlier is. So my comment may be a dup, but personally I'd require augmentations to be inside a part of that points to the declaration.
TL;DR this would be legal:
part 'a.g.dart';
class A{}
...
part of 'a.dart';
augment class A{}
But this wouldn't be legal:
part 'a.g.dart';
augment class A{}
...
part of 'a.dart';
class A{}
And a siblings couldn't augment each-other's code.
How would we deal with code-generators that output code which needs another code-generator to run?
The idea would be to rely on a tree-like part structure. We could have:
part 'a.freezed.dart'
@freezed
class Person {}
...
part of 'a.freezed.dart';
part 'a.freezed.g.dart';
@JsonSerializable()
class _PersonImpl implements Person {}
augment class Person {...}
...
part of 'a.freezed.dart';
augment class _PersonImpl {...}
This should help clearly spotting what is a declaration and what isn't.
For instance, we could still require augment to be applied when augmenting methods. And if two separate parts try to declare the same method, we could resolve this:
// a.dart
class A {}
// a.g.dart
augment class A {
void confict() {}
}
// a.freezed.dart
augment class A {
void conflict() {}
}
Here, both parts are declaring the same method. This clashes, so either one of the declared methods needs to be renamed, or one of them should become an augment void conflict() {} (and then said part needs to become a "child" of the declaring generated part)
This is most likely what @lrhn was saying, but in case it wasn't and this proposal is separate, enjoy! Otherwise ignore me :P
My hot-take is that augmentations should be a feature that is only used by code-generator authors.
I don't think we do a good job with language design when we presume a feature should be used only narrowly and doesn't work well if users decide to use it more than we expected. As much as possible, I think it's better to design features that still work well even when used in unanticipated ways.
A good goal IMO would be that the average Dart dev has no clue this feature exists, because they shouldn't need this.
The primary reason this feature was initially conceived was so that when a macro generates code, there was a resulting file on disk that users could navigate to when debugging or doing go-to-definition. If we didn't expect users to ever see this code, we wouldn't need the feature at all.
If they are going to see it, we should make the feature as comprehensible as possible.
If there is only one implementing declaration, then order doesn't matter (for the majority of declarations). There is no need for even the concept of an introducing declaration.
The language doesn't need it, but I still think it's a good idea for usability. It's a statement of intent. If you have two classes with the same name, it's not clear if the user intended one to augment the other or it's an accidental name collision. Because of that, the language can't give you helpful error messages based on how it interprets that collision.
If there are two parties involved, at least one of them must know that they're augmenting. That avoids accidental augmenting. It doesn't matter which party that is.
Does this flexibility buy the user anything meaningful, or is it just an arbitrary choice they have to make? Why not just say the first declaration is the unmarked one?
I'm not super certain what an "above relationship" mentioned by @lrhn earlier is.
The proposal used to define an "above" relation on declarations. Basically, a declaration A is above another B if:
-
They are in the same file and A is textually before B.
-
They are in different files and B is in a part file included (directly or indirectly) by the file where A appears.
So basically "above" the file in the part tree.
The idea would be to rely on a tree-like part structure.
We discussed this a while back (it's possibly in the issue tracker somewhere). The problem with requiring an augmentation to always be below the thing it augments is this:
Let's say a user has some code like:
// lib.dart
part 'lib.g.dart';
class A {}
class B {}
// lib.g.dart
// Generated:
part of 'lib.dart';
augment class A { ... }
augment class B { ... }
They've got a hand-authored main library, and a code generator that's generating a part file for it that augments some stuff in it. Later, they decide that their library is getting too big, so they create a hand-authored part file and move B into it:
// lib.dart
part 'b.dart';
part 'lib.g.dart';
class A {}
// b.dart
class B {}
// lib.g.dart
// Generated:
part of 'lib.dart';
augment class A { ... }
augment class B { ... }
If we require augmentations to always be below what they augment, this no longer works. lib.g.dart is trying to augment a class in a sibling part file.
If we have this restriction, then code generators and hand-authored part files won't play nice together. The code generator would have to generate a part file for each hand-authored one, and the user would have to add a part directive to each of their part files. The current proposal avoids all that.
A good goal IMO would be that the average Dart dev has no clue this feature exists, because they shouldn't need this.
The primary reason this feature was initially conceived was so that when a macro generates code, there was a resulting file on disk that users could navigate to when debugging or doing go-to-definition. If we didn't expect users to ever see this code, we wouldn't need the feature at all.
I hope I'm not coming off as rude, but if that's the goal of augments, then I think the feature needs to be reevaluated.
-
From my experience, this goal is the exact opposite of what people are asking. I've had discussions all over the world about codegen, and people repreatedly asked for a mean to never have to step into the
.g.dart. As a maintainer of numerous code-generators, this is probably the number 1 complaint people have with my code-generators. They don't want to step into generated code, but the nature of code-generation makes them step into it even when they don't want to. -
Folks who want to read generated code can inspect parts without augmentations (clicking on
part, using "find all references", using "go to implementation", using "go to definition" on generated symbols, etc). A codelense to do the jump is low value. -
Even if we do consider this goal to be valuable, augmentations will fail at this goal in many ways. It is very common that code-generators will not use augmentations for their generated code, and instead generate a new symbol. Augmentations would fail at redirecting people to the new symbol.
-
We do not need to solve this at the language level. Generated code could be annotated by
@GeneratedFrom(symbol), and this would add a codelense onsymbolwhich redirects to the associated generated code. -
I don't think people are aware that this is the goal. I and many others have seen augments not as a "tool to step into the generated code" but as "a mean to remove
_$Fooand solve some limitations around parts (such as lack of imports)"
And to be clear: I'm not saying that people shouldn't be able to read the generated code. They should be able to. But it's a "once in a moonlight" kind of thing.
If there's a real navigation issue, it's not in this direction. The most common navigation issue is the other way around. I've raised many issues about it, and folks expressed similar concerns many time too. TL;DR, given:
part 'a.g.dart';
@annotated
class A {}
part of 'a.dart';
final a = A();
And:
void main() {
print(a);
}
Then:
- from
print(a), people should be able to jump toAin a single click, skippinga.g.dartentirely - from
print(a), it should be possible to renameA - using "find all references" on either
Aorashould list the references of both
Basically, treat A and a as the same symbol.
On top of which:
- Generators need a mean to display custom diagnostics in user code
- It should be possible to move generated code to a different, hidden folder (be it
lib/.generatedor another) - Within reasons, syntax workaround that code-gen relies on to work should be removed.
This includes
_$Foo, but it also includes MobX's:
which is necessary to haveclass Example = _Example with _$Example; class _Example { @observable int value = 0; @action void method() {} }_$Exampleoverride_Example.value/_Example.methodand encapsulate them to do logic before/after getter/setter and method invocations.
Out of all of these, augments and reworked parts only solve the _$Foo.
And even class Example = _Example with _$Example; from Mobx isn't even going away since, as per the latest changes, augmented() isn't a thing anymore (but hopefully will do a comeback!).
Enhanced parts are cool, but they are independent from augmentations.
Re: reading the generated code; I agree it's rare to want to read it.
It's still important, and so augmentations are important, because:
- the tools want to read it: it's incredibly hard to have IDEs, build systems, etc, behave sensibly if some of your source is not on disk
- you want a way to talk about it and point to it--filenames--to talk about whether it's up to date, when it gets generated, etc
- when something goes wrong you occasionally want to read it--much like debugging, it's a very narrow use case that's just about important enough to support
- Generators need a mean to display custom diagnostics in user code
- It should be possible to move generated code to a different, hidden folder (be it lib/.generated or another)
Definitely.
If we have this restriction, then code generators and hand-authored part files won't play nice together. The code generator would have to generate a part file for each hand-authored one, and the user would have to add a part directive to each of their part files. The current proposal avoids all that.
FWIW I think there is a good chance this turns out to be the right thing to do anyway. I suspect we want one part per generated output, not merging of outputs in any way (because that requires merging namespaces / adding prefixes), and a part statement exactly where each part is used is nicely clear. A better place to put generated output, as suggested by Remi quoted above, will then be highly desirable https://github.com/dart-lang/build/issues/4085
It's important to be able to read generated code.
But I don't believe it is important to have a one-click jump from class A to its generated mixin _$A. For starter, "go to implementation" already jumps from A to _$A generally, so that's already mostly solved.
And to be clear: I'm not saying that people shouldn't be able to read the generated code. They should be able to. But it's a "once in a moonlight" kind of thing.
This is all I'm saying. I agree that users should mostly not need to step into the generated code and that the code that it's generated from is usually what they want.
But without a feature like augmentations, in the initial design for macros, it was not even possible to navigate to what the macro produced, and that leads to all sorts of really nasty problems. Like, for example, a macro generates a call to a function. That function throws an exception. What do you show in the stack trace if the macro generated code isn't actual, you know, code?
Augmentations exist to address that. They ensure that all Dart code that can be executed, even if generated, is still real code materialized on disk with a specified textual format that users can read.
In addition, we have some use cases around hand-authored augmentations as a way to break up large classes into smaller pieces. I'm not sure how compelling those use cases are when you can usually use mixins for that, but it might be helpful for things like a giant collection of static constants or something.
For these use cases, the augmentation shouldn't be thoughtful as "backstage" code that isn't user visible. It's as visible as the main declaration.
OK, I think it's time to fish or cut bait.
I re-read all the comments. If I extrapolate optimistically from what folks are saying, I believe there is a design that we might agree to. Here's a pitch with some rationale:
What gets annotated
An introductory declaration of a class, enum, function, method, whatever, is not annotated with any kind of special keyword. The absence of a keyword states an intention to create a new declaration. It is also the canonical location that code navigation should take users to for that named declaration.
Every other declaration that augments an existing declaration is always marked.
Why
I think the keyword rule should be the same across all kinds of declarations. If we do something like C#'s approach where the introductory declaration is marked too, then it means the hand-written code would have to mark not just the class but every augmented member in it too. I think that's too painful. Here's an example:
@builtValue
class Sandwich {
static Serializer<Sandwich> get serializer;
String get bread;
String get filling;
String get condiment;
bool get isToasted;
}
With the "introductory is unmarked augmentations are marked" approach, that's exactly what the hand-authored code looks like. If we do a C#-like approach, you'd have to write:
@builtValue
partial class Sandwich {
partial static Serializer<Sandwich> get serializer;
partial String get bread;
partial String get filling;
partial String get condiment;
partial bool get isToasted;
}
The goal of code generators is to eliminate boilerplate, and that would not do that.
We could have different rules for classes and members. We could say that once a class is marked partial, then any member in it can be augmented or not without needing a modifier. But in addition to the asymmetry and complexity, I think that's a bad design.
There is a real concern that code generated members might unintentionally collide with hand-authored ones that they don't intend to augment. When that happens, I want that to be an error and I want it be a clear error. I don't want users getting some confusing error about "method can't have two bodies" or something weird if the parameter signatures happen to not match.
So I think it's good if members have to be explicitly marked augment in the augmentation, but bad if the introductory ones have to be marked. And I think that rule works well for both top-level declarations and inside types.
What keyword
The keyword on augmenting declarations is augment.
Why
I don't love augment but it's short, unused, and no one seems to have actively complained about it. As noted earlier, partial and part don't make sense. The introductory declaration isn't marked and it's just as "partial" or a "part of" the declaration as the rest of them. "Augment" sends the right signal that there is an existing thing (which may or may not be perfectly complete on its own) and you're modifying it.
I think augment works well too because the word implies addition and not just modification. That aligns with the general principle that augmentations add capabilities but don't remove things the declaration can already do.
I considered expands but it's too confusingly close to extends and extension (which are honestly confusingly close to each other already). elaborates is abstruse (as is "abstruse"). add to is kind of cute but the last thing I want is to try to make add a contextual keyword.
What can get augmented
An augmenting declaration can only augment something above it, not before it (using those ordering relations as defined in the spec). In short, you can't augment a sibling in the part tree.
Why
Not being able to augment a declaration that's (only) in a sibling can make some code reorganization more annoying. It means that moving some code into a part file may also require you to move code that augments it into a part file as well. Earlier, I brought up the example where you start with:
// lib.dart
part 'lib.g.dart';
class A {}
class B {}
// lib.g.dart
// Generated:
part of 'lib.dart';
augment class A { ... }
augment class B { ... }
And want to move B into a separate b.dart part file. In that case, you would have to do:
part 'b.dart';
part 'lib.g.dart';
class A {}
// b.dart
part 'b.g.dart';
class B {}
// lib.g.dart
// Generated:
part of 'lib.dart';
augment class A { ... }
// b.g.dart
// Generated:
part of 'b.dart';
augment class B { ... }
Note how now there's a new b.g.dart file too.
I think that's OK. In practice, given how inherited imports work with part files work, I suspect that most code generators will move to a model where they generate one file per part file, not one part file per library. In that model, this is what the code generator would want to do anyway.
This also means that you can't have a family of sibling part files that all work in concert to summon a single unified definition out of the aether. You can't do:
// lib.dart:
part 'a.dart';
part 'b.dart';
part 'c.dart';
// a.dart:
part of 'lib.dart';
augment class Foo {
// stuff...
}
// b.dart:
part of 'lib.dart';
augment class Foo {
// stuff...
}
// c.dart:
part of 'lib.dart';
augment class Foo {
// stuff...
}
The fix is easy. Just add a placeholder introductory declaration at the top:
// lib.dart:
part 'a.dart';
part 'b.dart';
part 'c.dart';
class Foo {} // <-- Add this.
Now the part files are all augmenting that one. And now there is a single logical place for code navigation to take users to when they wonder what Foo is.
In return for some minor annoyances around reorganizing stuff in part files, I think we get a simpler to reason about feature. It feels very strange to me to be able to augment a declaration when the file containing the augmentation doesn't have any direct chain of part of pointers leading to what it augments.
(Note that while the restriction here is that an augmentation must be augmenting a declaration above it, there is still a linearization of augmentations across siblings which is potentially visible. I think that's OK.)
Ordering is a feature
The two general approaches we've talked about are C#-like or not.
In C#'s approach, the set of partial declarations really is a set. There is no order between them at all, and no overlap. You are just unioning disjoint stuff together. That's consistent with C#'s approach to libraries/projects where a csproj is just an unordered bag of files all dumping code into the same namespace.
There's nothing wrong with that approach for C#. But it's not how part files work in Dart. Because of import inheritance, they really are a tree with some ordering between parent and child parts.
And with augmentations, there is an ordering too. It's only user-visible for mixins and enum values, but I do think it's fairly likely we'll want something like augmented() in the future. I consider this ordering to be a feature. When a user chooses an explicit order for augmentations, we have the ability to hang useful user-visible behavior off that choice.
I think that's consistent with how mixins are ordered in Dart (unlike traits in some other languages) and we let users do something with that by having super calls.
Summary
In short, I think we stick with the current proposal except that we add a restriction that an augmentation can only augment a declaration above it and not just before it.
How does that sound?
Is it possible to hide or remove imports? One of the features I liked about macros was removing imports and uri_does_not_exist errors.
I suspect that most code generators will move to a model where they generate one file per part file, not one part file per library. In that model, this is what the code generator would want to do anyway.
I'm struggling to see how this can be made to work.
The way build_runner builders usually compose today is
- a builder accepts an incomplete library as input
- it outputs one file, a part file or a library, that contributes declarations to the library and/or makes more of it resolvable
- this is handed to the next builder as input; repeat
a builder can output a declaration and some annotation to trigger than a builder that comes after it provides the implementation for that declaration. (Using the hacks we have today --> some convention for the symbol that will be generated).
If augmentations have to be placed below declarations then instead of each step increasing the file count by one, each step doubles the file count in the worst case. For bazel we have to be able to determine the output file set before looking at source code, so we are forced to always hit the worst case.
Taking a step back, I think we should build as concrete as possible a story for codegen here: what will builders have to do, what will build_runner do, what will users see, what happens with bazel. I'm happy to give input from the build_runner point of view, obviously :) ... should we maybe chat face to face first to figure out how much overlap we have and how much needs to be covered?
Thanks :)
An introductory declaration ...
Having a designated "primary" declaration that you can use for "go to source" is probably reasonable. Allowing you to write the base declaration without a keyword is definitely good for usability with code generation.
I wouldn't make it required, because I could see myself using the feature for, effectively, "partial classes", and then I might want to mark every declaration to signal to a reader that this declaration is not the entire thing.
I think we have a good feature here because it can actually cover two use-cases:
- Separation of generated code from user-written code.
- Separation of different concerns in the same user-written code. (Aka. partial classes.)
I think we should embrace that, and not design the feature only for one of them. And definitely not choose the names based on what code generators will write, since they don't care what they have to write.
Maybe there is a distinction between the two features, in the generated code is more likely to add a second declaration with the same name, where partial classes are more likely to just have multiple classes with distinct declarations.
I'm not sure that should affect naming.
The keyword on augmenting declarations is
augment.
Strong disagree.
It's a new word. It doesn't really mean that much to anyone who hasn't been working on the feature for years. It doesn't match the partial-classes use. (And again, for the code-generation use, no user has to write the keyword.)
Using "augment" implies an order, where the partial classes use-case isn't really ordered.
When there can be at most one concrete declaration, almost no declaration is actually ordered, it's only mixins and enum elements. The rest do not require any ordering.
no one seems to have actively complained about it
Well, if I haven't been vocal enough before, let me go on record as complaining now.
The current feature, which does not allow multiple implementation declarations, is closer to a "define/declare" separation for functions and closer to a "partial class" behavior for class-like declarations. It's not really augmenting any more. It's filling in the blanks.
I could see using different keywords for the two cases - top-level types or functions - but neither would be best described by augment. We've lost the "stacking" that made that word fit.
I (still) prefer part to suggest that class-like declarations are equal and parallel, which they actually are.
For functions, the only reason to have more than one marked declaration is if they are doing different things, like one adding implementation and another adding metadata/documentation. They are piecing the complete declaration together from different parallel parts.
The word part is also closer to the C# partial, and is already a built-in identifier. (And is shorter than "partial".)
An augmenting declaration can only augment something above it, not before it (
What does that mean, precisely? (What does "something" mean?)
Is this only for members or also for classes?
Can I augment a declaration whose introductory declaration is above, or only one where all earlier declarations are above?
The latter, for classes, is far too strict. Having an introductory class declaration in a part, and then adding two separate members to it in different sibling parts, each with their own related imports, should be possible. Having to put one below the other adds unnecessary imports to the latter.
Others have pointed out that even requiring just the introductory declaration to be above, could be hard on code generators, which may need to add code below every part of a library. It's easy to add code after everything, that's just adding a final part in the library file.
I suspect that most code generators will move to a model where they generate one file per part file,
That is possible. It's also possible that they just import everything they need with fresh prefixes, instead of relying on the library's existing imports. If the same class declaration occurs in two parts, say each introducing a new instance variable, the code generator may need to generate code that refer to both. It can't rely on one set of imports for that anyway.
Requiring the introductory declaration to be above all other also means that moving some of the declarations of a file into a new first part file, might break the code. You'd have to move any other parts of the same declaration as well. Moving declarations into a new first part should otherwise always be a no-op, and a valid refactoring.
If we do require some order, the strongest requirement that I think is viable and palatable, would be that all declarations must be after the introductory declaration. (But I'd rather not have any requirement at all, and let users choose their style. There is no technical reason to force a specific ordering.)
Ordering is not a feature when it has no benefit. It's only a feature in the sense that it's not a bug. 😉
I don't think the (IMO hypothetical) benefit of allowing you to more easily be sure that you have found all other contributing declarations, without needing to check all files, is worth the extra complexity of defining and enforcing a fixed ordering. (I don't thing there would be many files in the common case)
I could see IDEs having small arrows to go to the prev/next declartion of the same member, either inline or if you hover. That's much better than having to look for other declarations manually, even in a restricted set of files.
There's nothing wrong with that approach for C#. But it's not how part files work in Dart. Because of import inheritance, they really are a tree with some ordering between parent and child parts.
I think that's mixing different orders. The ability of part-trees to have separate imports allows you to separate your implementation code by area of responsibility, while still adding everything declared into the top-level scope. Each scope is flat, every file contributes equally to the top-level scope or any nested class scope. The differences in imports represents separate concerns, not an ordering of the actual declarations.
The only reason we need the before/after ordering is to order mixin applications and enum values. If not for those, it wouldn't be needed.
For those, you better know what you are doing anyway. (And 99% of the time, you still don't rely on the order - mixins added in different declarations are so because they are unrelated, and enum values can be used without caring about their index or order.)
If augmentations have to be placed below declarations then instead of each step increasing the file count by one, each step doubles the file count in the worst case. For bazel we have to be able to determine the output file set before looking at source code, so we are forced to always hit the worst case.
Oof, good point. OK, so maybe restricting to above doesn't work.
I wouldn't make it required, because I could see myself using the feature for, effectively, "partial classes", and then I might want to mark every declaration to signal to a reader that this declaration is not the entire thing.
I really don't like the idea of an optional keyword because then it only sends half a signal. It means something when present, but its absence tells you nothing.
I think we have a good feature here because it can actually cover two use-cases:
- Separation of generated code from user-written code.
- Separation of different concerns in the same user-written code. (Aka. partial classes.)
I think we should embrace that, and not design the feature only for one of them.
We can support the second use case just fine even if the rule is that the introductory declaration isn't marked. Yes, you have to care about the order. But it's one keyword. Just leave it off the first one and move on with your life. Or if you really want all of the parts to be symmetric, make a one-line introductory declaration at the top.
The word
partis also closer to the C#partial, and is already a built-in identifier. (And is shorter than "partial".)
I think it's important that we don't require a modifier on the introductory declaration. So in the typical code generator use case, I expect the code to look like:
// Hand-authored:
class Foo { ... }
// Code-generated:
keyword class Foo { ... }
If we use part for that keyword:
// Hand-authored:
class Foo { ... }
// Code-generated:
part class Foo { ... }
Then it reads exactly backwards from how part works for files. For files, part appears in the file that is applying the part file. It's part of that appears on the subordinate file. "Part" means "apply that thing to me". But here it means "apply me to that thing".
We could do:
// Hand-authored:
class Foo { ... }
// Code-generated:
part of class Foo { ... }
It reads nicely and matches how part files work. I don't know if it gets weird on non-class declarations, though:
class Foo {
part of static int get getter { ... }
}
That's a lot of keywords, most of which are contextual. (We'd probably be in a better place if the language had used partof instead of part of.)
I really don't like the idea of an optional keyword because then it only sends half a signal. It means something when present, but its absence tells you nothing.
I may have been too vague here. When I said "I wouldn't make it required", the "it" referred to omitting the keyword.
I'd allow you to omit the keyword on at most one declaration, rather than require you to omit it on one declaration.
We already have one declartion without a keyword whose absence tells you nothing. I just want the option of telling something for that declaration too. (So I agree, that's exatly why I want this!)
And this is probably mostly for class-like declarations, where each declaration fills members into the same scope and they are essentially parallel. I don't think I'd need it much for member declarations. Using this feature as "partial classes", I'll have multiple class declarations as a way to structure and separate members, but rarely duplicate members.
(...can work...) even if the rule is that the introductory declaration isn't marked.
And even if it is. I see no benefit from disallowing you from marking it.
Or if you really want all of the parts to be symmetric, make a one-line introductory declaration at the top.
That's exactly the kind of workaround that smells like an incomplete design. There is a use-case, but it's not directly supported. In this case, it's not because it's not possible, but because we've gone out of our way to disallow it.
We could also just allow you to mark every declaration.
Everything can still work. If a tool needs to point to one instance, it can choose the first. Or the first one with documentation. Or for memers, the one with implementation. Heurisitcs are great! (Yes, I still need to care about order if it chooses the first, then I have to put my documentation there.)
@lrhn, @davidmorgan, and I met to discuss this more. We've reached agreement that:
The introductory declaration is never marked and the augmenting declarations are
This works well for the common use case of a code generated augmentation of a hand-authored declaration:
// Hand-authored:
@someCoolCodeGenerator
class Foo {
void bar();
}
// Generated augmentation:
augment class Foo {
augment void bar() {
print('generated body');
}
}
It means that the hand-authored code is maximally terse. At the same time, it ensures that two declarations that happen to have the same name but aren't intended to form a single declaration is reported as a clear collision error.
For the use case of splitting a large hand-authored declaration into multiple equally independent pieces (like partial in C#), it works a little less well. You either have to remove the keyword from whichever declaration happens to come first in the part file tree. Or, more likely, you make one tiny placeholder introductory declaration that all of the other pieces then augment:
// lib.dart:
part 'io_stuff.dart';
part 'other_stuff.dart';
// Placeholder introductory declaration:
class BigClass {}
// io_stuff.dart:
part of 'lib.dart';
import 'dart:io';
augment class BigClass {
BigClass.fromFile(String path) { ... }
void saveToDisk(String path) { ... }
}
// other_stuff.dart:
part of 'lib.dart';
augment class BigClass {
void doOtherThing() { ... }
}
There is a slight tax to write this placeholder declaration. But in return for that, you get a canonical place for code navigation and the doc comment. That seems like an acceptable trade-off, especially given that this use case is likely significantly rarer than the code-gen one.
The introductory declaration must be before all augmentations but not necessarily above
It's pretty confusing if an augmentation appears textually after the thing it augments:
augment class Foo { // Wait, what even is Foo?
...
}
// Much later...
class Foo { // Oh, it's this.
...
}
Requiring the introductory declaration to be before all augmentations (the proposal defines what "before" means) avoids that.
Likewise, it's pretty confusing if an augmentation in one file applies to a part file that it makes no mention of:
// lib.dart:
part 'a.dart';
part 'b.dart';
// a.dart:
part of 'lib.dart';
class Foo {}
// b.dart:
part of 'lib.dart';
augment class Foo {} // Wait, where did Foo come from?
Just using "before" doesn't help here because class Foo {} in "a.dart" is before augment class Foo {} in "b.dart". (The before relation is basically an in-order traversal of the part directive tree.)
We do need a full ordering of augmentations because application order is visible in a couple of corner cases, so "before" ordering is essential. I proposed that for the introductory declaration, it must be not just before but above all augmentations. That means basically from any augmentation, you should be able to get to the introductory declaration by following the part of chain.
@davidmorgan pointed out that that has potentially very bad interactions with code generators. It could mean that a code generator has to output not just a single part for each library, but for each part file in the library. And if you chain multiple generators, you can end up needing to double the number of generated part files for each level of generators.
Given that, we've decided to not go with the stricter ordering rule for the introductory declaration. The introductory declaration must be before all augmentations, but not necessarily above.
This means you can write a confusing library where a declaration is created in one part file and augmented in a sibling. But you can also choose to not do that.
The syntax to mark an augmenting declaration is augment
We discussed a number of other possible choices of punctuation or keywords, but nothing seemed to be better than augment.
-
Using
partdoesn't work well because elsewhere in the language, that's used where the part file is being referenced from not the part file itself. If the introductory declaration had to mention the augmentation (sort of likefriendin C++), then it could make sense to useparton that directive. But that's not how they work. -
Using
part ofwould be more consistent with how that syntax works for files. But it's uncomfortably long before a function declaration and... kind of weird. -
augmentswould be consistent withextendsbut reads weird without a subject preceding it. And it's longer.
As far as we can tell, augment is the least bad option.
Summary
After all this discussion, I think the result is pretty much what the proposal already states. :) But at least we are more confident that we've thought through the ramifications of those design choices. I'll leave this issue open for now to remind me to double-check that the proposal aligns with this and maybe add some of this rationale to the proposal.
Thanks Bob.
Edit: I think a lot of what is below is wrong, will add another post trying again :)
As promised I considered what the "declarations must be before" restriction means for codegen, and I think it's broadly fine. I think if we want to do anything complex like allowing generators to request implementation from any other generator, then it probably leads to asking them to split declaration output from augmentation output, and I think that makes enough sense for other reasons that it's not an undue burden.
But, one awkward corner did occur to me, and it's this: declarations that come from "implements" clauses or similar. Maybe you already thought of this, maybe it's new, anyway I'll share my line of thought :)
In what follows I am a bit treating "introductory declaration" and "abstract method" as the same thing, possibly they are not 100% the same, but also possibly because of the "bodyless method is ambiguous" they are sufficiently the same thing? :)
So the simplest question is, where does the "before" rule consider the declaration to be when the declaration comes from "implements":
class A implements B, C {}
class B {
void b();
void d();
}
augment class A {
augment void b() {} // allowed?
augment void c() {} // allowed?
augment void d() {} // allowed?
}
class C {
void c();
void d();
}
And then the reason I thought about this re: codegen, is that there are multiple ways for an augmentation to contribute declarations, which means it's not so clear-cut for codegen infra to try to separate declarations and augmentations.
I remember correctly, it's allowed to augment a class to add to the implements clause? So:
class A {}
class B {
void b();
}
augment class A {
augment void b() {} // allowed?
augment void b2() {} // allowed?
}
augment class A implements B {
}
augment class B {
void b2();
}
And then we maybe get to a weird place: if adding a declaration by creating a new class then adding to the implements clause gets around the "before" restraint, then it's strictly more powerful than adding declarations using augment and maybe generators just always do that.
class A {}
// generated
augment class A implements _ADecls {}
class _ADecls {
void a();
}
Or equivalently with mixins if adding declarations+implementation.
I don't like that idea because it feels like adding new types, even if they can be private, invokes much more complex machinery than just adding members directly with augment. Private types are still going to be discoverable by the person working on the code and add noise to their IDE experience. So it feels like we should encourage people to prefer not introducing a type.
I guess the summary of my line of thought is: does the fact that members can be aggregated from superclasses, maybe in combination with the fact that superclasses can be added by augmentations, maybe in combination with abstract methods and incomplete methods being indistinguishable, break the nice properties of the "before" requirement for members?
Related, it's already the case that a member can be declared in two superclasses and there is no definitive declaration :) I checked what VSCode does for "go to superclass/member" in such a case, the answer seems to be that it goes to the declaration in the type that's mentioned first in the implements clause.
If any of this is already not a problem because I misunderstood something, great :)
Okay, thinking about this a bit more :) I think I was misled by the fact that I'm used to thinking of codegen as subclassing.
I think in most cases the answer to my questions is "implements doesn't work like that, you don't get partial declarations that way". And you don't have to augment to provide a body for something declared abstract in a superclass, actually that would not be allowed.
So my first code block should have been:
class A implements B, C {}
class B {
void b();
void d();
}
augment class A {
void b() {}
void c() {}
void d() {}
}
class C {
void c();
void d();
}
then the second
class A {}
class B {
void b();
}
augment class A {
void b() {}
void b2() {}
}
augment class A implements B {
}
augment class B {
void b2();
}
so I think maybe there aren't any awkward cases after all. That would be good :)