Description
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 by 1
.
// lib1.dart
class A {
final int _privateField = 3;
void testThisCall() => print(_privateField);
}
class B {
int _backingStore = 0;
int get _privateField => _backingStore++;
}
import "lib1.dart";
class C extends A with B {
}
void main() {
new C()..testThisCall()..testThisCall();
}
A slightly more elaborate example runs with no errors in both the CFE and the analyzer, and again prints 0
, 1
:
// 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();
}
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.
// lib1.dart
class A {
final int _privateField = 3;
}
void testOtherCall(A a) => print(a._privateField);
// main.dart
import "lib1.dart";
class E implements A {
int _count = 0;
dynamic noSuchMethod(_) => _count++;
}
void main() {
var e = new E();
testOtherCall(e);
testOtherCall(e);
}
This code prints 0
, 1
. The implicit method forwarder in E
delegates calls to _privateField
to noSuchMethod
which can provide an implementation. To avoid this, I propose that forwarding stubs for private members from other libraries always throw, rather than delegating to noSuchMethod
. 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):
// lib1.dart
class A {
final int _privateField = 3;
}
class B {
int _backingStore = 0;
int get _privateField => _backingStore++;
}
void testOtherCall(A a) => print(a._privateField);
// main.dart
import "lib1.dart";
class D extends B implements A {}
void main() {
var d = new D();
testOtherCall(d);
testOtherCall(d);
}
This code prints 0
, 1
, since D
brings together two previously unrelated private members to provide an implementation of A
which uses the concrete private member from B
.
To prevent this, there are a few different options.
- We might choose to disallow a class to implement a private member from a different library with the same name from two different classes entirely.
- We might choose to disallow a class to implement a private member from a different library with the same name from two different non-subtype related classes.
- We might choose to say that any time a class implements a private member from a different library with the same name from two different classes (or two different non-subtype related classes) we generate a stub which throws.
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 on this
, 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
Metadata
Metadata
Assignees
Type
Projects
Status