-
Notifications
You must be signed in to change notification settings - Fork 205
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Enable promotion for private final fields and fields on non-escaping private classes #2020
Comments
Level 0 is probably sound. (I say probably because I can't find any reason it shouldn't be, but it's always possible to miss something. This is like security — you only need one flaw in one place, no matter how obscure, to be unsound.) Because we only promote instance fields on We currently disallow mixins from mixing private fields onto private fields, but not shadowing private fields in general. The CFE apparently fails to implement that restriction. We could extend that to any override of a field, or of any member. That might be breaking, if someone deliberately has a mixin that is intended to override private members. It's probably rare, but not impossible, and I can't see any workaround if we blanket-disallow overriding privates in mixin applications. The getter of a final field is currently the only kind of instance getter where we can predict future behavior from current behavior, so if we want to promote any current kind of getter, that's really the only thing we have to work with. (Otherwise we'd need to introduce "stable getters"). This proposal has the advantage that the promotion only works inside the same library, so you can't break anything for anyone other than yourself. That limits the potential issues. We'd probably want to add a warning if a mixin declares a getter with the same name as an instance variable of any class in the same library. Maybe only if the mixin's Level 1 is probably sound too. If the class has no non-private subclasses (declared or type-aliased), then all subclasses must exist in the same library, and we can see whether the final field is ever overridden. If not, it's safe to promote accesses. Level 2 sounds reasonable too. I'm more worried about making it a compile-time error when private members of another library clash. This sound more like a step towards declaration based virtual members rather than name based ones. That is, the And, as you say, we want to recognize "stable" expressions, because we can only allow promotion of a final field on an object reference if that object reference definitely doesn't change between check and use. All in all, this gives promotion only of certain private fields. That's probably enough for many use-cases. We can also trivially promote private static final variables the same way. A private static final variable is definitely not overridden. It breaks getter/field symmetry to only promote fields, not getters, but it only does so locally in the same library. If you want to change the field to a getter, you are also in position to change all the promotion sites. If you add something which breaks promotion, you are already editing the library, and will see it immediately. It only works for some private final fields, depending on the pattern of declarations around it, and if the user can't figure out the border between promotable and non-promotable, then it's going to be a frustrating feature. The rule that people will follow is probably going to be that there can't be any other member declaration with the same name in the same library. That's simple and it works. Maybe we could just do the simple version: A private final instance variable is locally unique if there is no other instance variable, getter or setter with the same basename declared in the same library.
|
@leafpetersen: // mixin0.dart
import "mixin1.dart";
class A {
final int _privateField = 3;
void testThisCall() => print(_privateField);
}
class B {
int _backingStore = 0;
int get _privateField => _backingStore++;
}
class C extends D with B {} // mixin1.dart
import "mixin0.dart";
class D extends A {}
void main() {
new C()..testThisCall()..testThisCall();
} And then said:
I don't think this rule removes the loophole, because in the example, the override of a private field is happening in the declaration But I think that's ok, because there's not actually a loophole in this example. In your definition of level 0, you say:
And therefore, since So I think all we have to do is port the analyzer's PRIVATE_COLLISION_IN_MIXIN_APPLICATION error over from the analyzer to the CFE; I don't think it needs to be extended in any way. |
For level 0, I think we need to add an exception for extension declarations: they can't safely promote private instance fields on We could support promotion of private instance fields on |
I like all three levels you have here and I think the minor language changes required to enable them are worth doing. At some point, I think we should consider level 4: Allow promotion on access to a public field from another library iff:
1 and 2 are the same restrictions as the other levels. 3 requires something like packaged libraries to be able to annotate classes as sealed and non-interface. 4 requires some new annotation or modifier we'd have to define and specify. I think it's important to require this opt-in because otherwise it means changing a field to a getter is a breaking API change. I think "promotability" should be considered part of a class's public API and something the API designer can explicitly control. If we opt all possible final fields in to it by default, I worry authors would get in the habit of "opting out" by preemptively wrapping every field in a getter like they do in Java. |
Like stable getters (#1518) ? |
Yes, like that. I don't know if I am sold on committing to stable getters—to being able to define non-fields that are promotable—but at least being able to control which fields are seems worth doing to me. |
All final fields are stable, yes. The problem is that an API maintainer may not want to commit to that property being a final field henceforth and forever. If we implicitly allow any public final field to promote, then it means changing that field to a getter is a breaking API change. If we don't have some sort of annotation to control whether a public final field allows promotion or not, then API authors have no ability to control that aspect of their public API. |
Think about it going in the other direction: I'm an API designer. I'm creating a class that exposes some property. Right now, I can implement it correctly just using a final field. But I don't know if that will always be the case. I want to give my future self the freedom to change that field to a non-stable getter without having it be a breaking change. That means that today I want this final field to not allow promotion. If we allow public final fields to promote and don't give class authors any way to opt out, they will just pre-emptively wrap every field in a getter (just like you see in Java) in order to not paint their API into a corner. |
@tatumizer this discussion is getting very far afield from the issue topic, can you please move discussion of stable getters to the stable getter issue? Thanks! |
I took a look at the different levels in the initial comment, and I think there's a need to make a couple of small adjustments, or at least clarifications. tl;dr
About level 0: Allow property accesses to final, private fields on
|
A note on stability: this implies that changing a private getter or field in a way that makes it non-stable can break unrelated downstream code. This isn't a deep change - it's already the case that adding a private getter or field, or changing the type of one can break unrelated downstream code, but worth noting. If we made private members unreachable via dynamic dispatch, we could potentially make privacy class (hierarchy?) specific and avoid all of these issues, I think. I wonder how breaking that would be? |
Changes to private members should only affect the library itself, so it's not breaking unrelated code (considering the entire library as related to itself). If we introduce the rules that a mixin application cannot override an inaccessible member, then adding a private member with the same name as another member in the same library (unless both override the same super-interface members), where at least one is in a mixin-able declaration, is potentially breaking unrelated code. We should probably have a warning for that. |
No, sorry, I really mean unrelated code, in the same way that is true currently. Right now, in existing Dart, adding a private member to a class is a breaking change, that can break arbitrary downstream code. The (well, a) reason is that some other code in some other library may be using the class as a mixin on another class from the original library with a member with the same private name, and if those members have conflicting types, the mixin application will suddenly cause a type error. Making stability part of the API in the same way that types are part of the API means that the same breakage can now happen even if the types are compatible. Again, not a sea change, just another instance of the same problem. And yes, forbidding mixins from inducing overrides makes all such examples errors. |
It would be really useful to estimate the % of error cases each level proposed here can solve. Maybe we can do some corpus analysis using internal Google code? If, say, 80% of error cases can be covered, it's fine to leave the rest 20% to additional tooling support such as dart-lang/sdk#47588. |
@munificent didn't you do that analysis? |
That sounded familiar, but it took me a while to dig it up. Yes, I did a very rudimentary bit of digging. Here's the old email: I went through all of the nullable fields in dart_style. Obviously, this is just one codebase with its own possibly idiosyncratic style, but I figured some data is better than none. There are 33 nullable fields spread across 17 classes. I believe none of them are intended to be overridden or implemented.
Based on this small amount of anecdata:
The dart_style codebase is kind of interesting because it does a lot of lazy initialization. Much of it is sort of a dynamic programming style. I don't know how well the numbers here would correlate to other codebases. We could automatically gather some data like this by looking at the nullability of assignments to nullable fields, but that kind of analysis is a little beyond my expertise. Might be worth talking to @pq and @brianwilkerson if we think it would be useful data to have. It is fairly easy to scrape a corpus to see how many nullable-annotated fields are final/non-final. I went ahead and did that on 2,000 Pub packages:
|
This is intended to be an umbrella for several features we plan to implement during Q2: - Improved type inference for methods like `Iterable.fold` (dart-lang/language#731). - Promotion of final fields (dart-lang/language#2020). - Other type inference and type promotion issues as time allows. Change-Id: I31db3c15d3f6a2654650f056866c61f3d1023600 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/237924 Reviewed-by: Konstantin Shcheglov <[email protected]> Reviewed-by: Johnni Winther <[email protected]> Reviewed-by: William Hesse <[email protected]> Commit-Queue: Paul Berry <[email protected]>
This is initially intended to support type promotion of fields (dart-lang/language#2020). However, if time allows, we may roll other type inference improvements into it. Change-Id: Ie4548ceafe671a9a328a11ad950a4e70f4d3ca41 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/244766 Reviewed-by: Devon Carew <[email protected]> Reviewed-by: Konstantin Shcheglov <[email protected]> Commit-Queue: Paul Berry <[email protected]>
New feature in upcoming Dart 3.2. See dart-lang/language#2020. Feature is enabled by bumping the min SDK version to 3.2. Part of #134476.
New feature in upcoming Dart 3.2. See dart-lang/language#2020. Feature is enabled by bumping the min SDK version to 3.2. In these packages, no private fields were found to be promotable. Part of #134476.
New feature in upcoming Dart 3.2. See dart-lang/language#2020. Feature is enabled by bumping the min SDK version to 3.2. Part of #134476.
New feature in upcoming Dart 3.2. See dart-lang/language#2020. Feature is enabled by bumping the min SDK version to 3.2. Part of #134476.
New feature in upcoming Dart 3.2. See dart-lang/language#2020. Feature is enabled by bumping the min SDK version to 3.2. Part of flutter/flutter#134476.
New feature in upcoming Dart 3.2. See dart-lang/language#2020. Feature is enabled by bumping the min SDK version to 3.2. Part of #134476.
New feature in upcoming Dart 3.2. See dart-lang/language#2020. Feature is enabled by bumping the min SDK version to 3.2. Part of flutter#134476.
New feature in upcoming Dart 3.2. See dart-lang/language#2020. Feature is enabled by bumping the min SDK version to 3.2. Part of flutter/flutter#134476.
This is primarily here to support private field promotion: dart-lang/language#2020 Also discussion at dart-lang/language#2275 The broad stroke is that users may need to start declaring little mixins next to their base classes with implementations for this or that private API which is (intentionally or not) accessed against a mock instance during a test. Fixes dart-lang/mockito#342 PiperOrigin-RevId: 461933542
Field accesses in general are not subject to promotion even for final fields, since it is in general not possible to know that the field is not overridden with a getter without seeing the whole program. In a limited set of cases, with a small set of changes to the language, library level analysis could show that certain field accesses are safe to promote.
This was discussed previously here and here.
Level 0: Allow property accesses to final, private fields on
this
to promote.We could allow a property access on
this
to a final private field which is not overridden in its declaration library to promote if we made the following change to the language.Disallow mixins from inducing concrete overrides of private members from other libraries.
In current Dart (as of 2.16) a mixin can be used to induce an override of a private member from another library. The following code has an error in the analyzer, but not the CFE, and prints
0
followed by1
.A slightly more elaborate example runs with no errors in both the CFE and the analyzer, and again prints
0
,1
:To enable Level 0, we must therefore remove this loophole. I believe it is sufficient to enforce that it is uniformly an error to cause an override of a private field (concrete or abstract) in any library outside of the library in which the private field is declared. This is a breaking change, but likely to be non-breaking in practice.
Level 1: Allow property accesses to final fields (private or not) on private non-escaping classes to promote.
A class is non-escaping if it is private, and it is never implemented, mixed in, or extended by a public class either directly or transitively, nor given a public name via a typedef.
All overrides of the members of a non-escaping class can be observed locally, and an access to a non-overridden field could be allowed to be promoted, whether on
this
or on a different instance, and whether the field name is private or public.Level 2: Allow property accesses to final, private fields on instances other than
this
to promote.For Level 0, it is sufficient to ensure that all potential overrides are visible. Since promotion is restricted to accesses on
this
, it is not necessary to ensure that there are no other implementations of the field. For Level 2 promotion, we must also ensure that all non-throwing implementations of a private field are visible. This entails the following steps (at the least, are they sufficient?).Don't delegate private names to
noSuchMethod
.In current Dart (as of 2.16),
noSuchMethod
can be used to provide a concrete implementation of a private name from a different library.This code prints
0
,1
. The implicit method forwarder inE
delegates calls to_privateField
tonoSuchMethod
which can provide an implementation. To avoid this, I propose that forwarding stubs for private members from other libraries always throw, rather than delegating tonoSuchMethod
. This is a breaking change, probably unlikely to be significant, but it is possible that there may be uses of this misfeature (e.g. mockito?).Forbid private members from being implemented by unrelated private members outside of their defining library.
For accesses on instances other than
this
, it is not sufficient to prevent unexpected overrides of private members - we must also prevent unexpected implementations of private members. For example, the following is valid in current Dart (2.16):This code prints
0
,1
, sinceD
brings together two previously unrelated private members to provide an implementation ofA
which uses the concrete private member fromB
.To prevent this, there are a few different options.
All of these are technically breaking. I suspect they are unlikely to be very breaking in practice, but this is something we would want to validate with corpus analysis.
Discussion
Level 0 and Level 1 seem like clear wins to me, with fairly minimal cost. I don't see much down side to pursuing them, even if they don't fully solve the issue. Level 1 requires generalizing promotion to paths (e.g. allowing
a.b
to be promoted), and we would need to define exactly what paths we promote and under what circumstances it's valid to do so, but I'm not too concerned about this (a simple story might be that a promotable path is either a promotable property access onthis
, a promotable local variable, or a promotable property access on a promotable path).Level 2 potentially has more cost, and I'm less confident that I've captured all of the corner cases. Corpus analysis and careful thought would be required. However, it does feel unsatisfying to not support promotion of private fields on non-this instances, so I'm tempted to pursue this further.
cc @mit-mit @lrhn @eernstg @munificent @jakemac53 @natebosch
The text was updated successfully, but these errors were encountered: