dart-lang / dart-lang/language

[Field promotion] Final expressions

Open
#3,327 6 comments 0 reactions 0 assignees View on GitHub
field-promotion
Dominant language
TeX
Stars
2.9k
Forks
239
Avg merge
2d 18h
Merged PRs (30d)
14

Description

Field reads cannot in general be promoted based on null checks or instance checks because stability of the result cannot be guaranteed. Older overview summary [here](https://github.com/dart-lang/language/blob/main/working/field-promotion/proposals-overview.md). Various proposals have been floated to resolve this based on either providing a way for APIs to opt into promotion (e.g. stable getters) or by providing better ways to bind a local variable (search for the label field-promotion for some discussion issues).

This issue sketches out an approach based on introducing an implicit hidden final variable to cache the value of a field read within the scope of a promotion. We allow a promotable expression (for the purposes of this discussion, we can consider the set of syntactic expressions which are currently candidates for private field promotion) to be prefixed by `final` to indicate that within the "scope" (loosely speaking) of the expression, subsequent references to the expression should re-use the value of the initial evaluation of the expression, rather than re-computing the value. This guarantees stability of the expression, making promotion sound. As with private field promotion, assignments to the root of the path would need to be accounted for appropriately. Examples:

```dart
class A {
int? x;
A(this.x);
void test(A other) {
if (final other.x != null) {
other.x.isEven; // Valid, `other.x` is promoted.
if (final x != null) {
print(x + other.x); // Valid, this.x and other.x are both promoted
}
}
if (x != null) {
x.isEven; // Valid implicit `this.x` is promoted
}
}
}

class Bad {
int _x = 0;
int get x => _x++;
void test(Bad other) {
other._x = 0;
int cache = other.x; // assigns 0 to cache
if (final other.x == 1) {
print(cache); // prints 0
print(other.x); // prints 1
other._x = 99; //
print(other.x) // prints 1
}
print(other.x); // We are out of the "scope" of the final expression prints 99
}
```

An advantage of this approach is that it applies to any field or getter, without any requirements for the API designed to make a choice. It does so without requiring the user to invent, bind, and use a new local variable.

A disadvantage of this approach is that the "scope" of the hidden variable may not be obvious. This doesn't matter for "well-behaved" getters, but as the `Bad` example shows, if a getter actually returns different values on different reads, then which invocations are cached becomes relevant, and is not clearly visible in the syntax.

One approach to mitigating this would be to only allow final expressions to be "bound" in a limited set of locations in which scope is clear (similar to the way we handle `if case` expressions now. So then we could say that `if (final e ) ` caches the value of `e` within ``, where `` is one of a limited set of binary operations (e.g. `!= null` and `is T` ). This makes it clear what the scope of the "finalization" is. Example:

```dart
void test(Bad bad) {
if (final bad.x != null) {
bad.x.isEven; // promotion is valid, value is the same as the last read
} else {
throw "Unreachable";
}
// bad.x.isEven; // Static error, promotion is disabled
print(bad.x); // New value is read, rather than re-using the value cached in the scope of the `if`.
}
```

There is still some possibility for confusion around assignments to the root of the path. For example, the following code might be confusing:
```dart
void test(Bad bad) {
if (final bad.x != null) {
print(bad.x);
bad = Bad();
print(bad.x);
}
}
```
It seems wrong to allow the second call to `bad.x` in the body of the `if` to use the cached value. We could say that this code is valid, but that the "finality" ends at the point of the assignment, but perhaps it is better to say that within the scope of a final expression, assignments to the root variable of the path are statically disallowed, and hence the code above becomes an error.

With the adjustments above, we end up with something that essentially behaves as if `if (final e != null) ` were shorthand for `if (e case final freshVar?) ` where `block2` is `block` with all uses of `e` replaced with `freshVar`.

cc @dart-lang/language-team

Contributor guide

Open the contributing guide

Research direction

Start with this issue and the linked working/field-promotion/proposals-overview.md overview, then read the field-promotion-labeled discussion issues mentioned in the description. No implementation files, tests, or entry points are named; the work is not done until the syntax, scope, assignment rules, and preferred proposal are settled.

Written by the indexing model from the issue text.

Assessment

Tech stack
dart
Domain
compilers
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
20/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.