dart-lang / dart-lang/language

Always coerce before LUB

Open
#3,363 0 comments 1 reaction 0 assignees View on GitHub
least-upper-bound
Dominant language
TeX
Stars
2.9k
Forks
239
Avg merge
2d 18h
Merged PRs (30d)
14

Description

Dart has the three implicit coercions: Implicit downcast from dynamic, implicit `call` method tear-off and implicit generic method instantiation.

The coercions are triggered by having an expression whose static type is not a subtype of its context type, but which is *coercible* to the context type.
However, we do not perform those coercions at the first possible chance. Sometimes we omit doing the coercion, and allow the value to flow out from a subexpression into becoming the value of a containing expression instead, knowing that the value will be coerced there instead (or even further out).

The reasoning is that we can do this if:
* The value of the subexpression becomes the value of the containing expression (the subexpression is in tail position).
* The context type of the subexpression is the same as the context type of the containing expression (which is mostly the case for subexpressions in tail position).
* The static type of the subexpression is also the static type of the containing expression.

If all those are true, then it "doesn't matter" whether we coerce now or later, the result will be the same.

The reason it's even visible, is cascades. `Foo f = (fooAndBar as dynamic)..barMethod();` works because the implicit downcast from `dynamic` to `Foo` happens *after* the `..barMethod()` is called, as a dynamic invocation.
The receiver expression of a cascade satisfies all three points above, and it also makes it detectable where coercion happens.

**The problem** is that we also delay coercion for expressions in tail position of conditional expressions and null-guards: `e ? tail : tail` and `e ?? tail`, and now `switch`-expression cases.

It is a problem because those expressions *do not satisfy the third point*, the static type of the expression is *not* necessarily the same as that of the tail expression, it's the LUB of that and something else, which means that the coercion which made os *accept* an expression, because its static type was coercible to the context type, might not happen.
It's not unsound, it's just that LUB gives us something guaranteed to be useless, instead of doing the coercion at the latest point where we knew it would definitely happen.

I've made some examples below, but the short point is that you should always do coercions before changing the static type, which means always do coercions before doing LUB.

---------------------------

Turns out CFE and analyzer disagrees on when to instantiate generic function values.
Both agree on generic tear-offs being instantiated immediately.
Both agree on `.call`-tear-off and downcast from `dynamic` happening late (too late when after LUB).
Analyzer instantiates generic function values as early as possible, CFE as late as possible.
(And when you instantiate a generic `call`-tear-off, it's after the `call`-tear-off, so late.)

Analyzer also seems to perform `.call` tear-off for switch cases, but not downcast from `dynamic`,
where CFE delays both.

```dart
// Function declaration.
T id(T value) => value;

// "Unpredictable" Getter
T Function(T) get vf => id;

// Callable class.
class IntId {
int call(int x) => x;
}

// Generic callable class.
class GenId {
T call(T x) => x;
}

// Just some value.
dynamic d = 42;

void main() {
test(true, id);
}

void test(bool b, T Function(T) fn) {
int Function(int) f;

f = id; // Valid.
f = id; // Valid, implicitly instantiated to be the same as Id.
f = (b ? id : id); // Accepted in both CFE and Analyzer.
f = switch (b) {true => id, false => id}; // Same

f = vf; // Valid.
f = vf; // Valid, implicitly instantiated to be the same as Id.
f = (b ? vf : vf); // LUB is `Function` in CFE, accepted in Analyzer!
f = switch (b) {true => vf, false => vf}; // Same

f = fn; // Valid.
f = fn; // Valid, implicitly instantiated to be the same as Id.
f = (b ? fn : fn); // LUB is `Function` in CFE, accepted in Analyzer!
f = switch (b) {true => fn, false => fn}; // Same

f = IntId().call; // Valid.
f = IntId(); // Valid, implicit `call` tear-off.
f = b ? IntId().call : IntId(); // Error. LUB is `Object`, both CFE and Analyzer.
f = switch (b) {true => IntId().call, false => IntId()}; // Error in CFE, accepted in Analyzer.

f = GenId().call; // Valid.
f = GenId().call; // Valid, implicit instantiated.
f = GenId(); // Valid, implicit `call` tear-off, then instantiated.
f = b ? GenId().call : GenId().call; // Accepted in both CFE and Analyzer
f = switch (b) { true => GenId().call, false => GenId().call}; // Accepted in both CFE and Analyzer
f = b ? GenId().call : GenId(); // Error. LUB is `Object`, both CFE and Analyzer.
f = switch (b) {true => GenId().call, false => GenId()}; // Error in CFE, accepted in Analyzer.

int i;

i = 1; // Valid.
i = d; // Valid, implicit downcast to dynamic.
i = b ? "Not an int" : d; // "Valid", LUB is `dynamic`, forgets the string.
i = switch (b) {true => "Not an int", false => d}; // Same.

int Function(int)? p;
// Using `if (b)` to avoid promotion afterwards
if (b) p = id; // Valid, implicit instantiation.
if (b) p = IntId(); // Valid, implicit `call` tear-off
if (b) p = p ?? id; // Valid, tear-off is instantiated eagerly.
if (b) p = p ?? vf; // LUB is `Function` in CFE, accepted in Analyzer!
if (b) p = p ?? fn; // LUB is `Function` in CFE, accepted in Analyzer!
if (b) p = p ?? IntId(); // Invalid, LUB is `Object`, both CFE and Analyzer
if (b) p = ("abc" as String?) ?? d; // "Valid", LUB is `dynamic`.

// Tear-off instantiation always happens eagerly.
f = id..isIntFunc;
f = GenId().call..isIntFunc;
// Function value instantiation is eager in analyzer, late in CFE.
f = fn..isIntFunc; // Error in CFE, not declared on `T Function(T)`
f = vf..isIntFunc; // Error in CFE, not declared on `T Function(T)`
}

extension on int Function(int) {
// The cascade knows!
void get isIntFunc {}
}
```

Contributor guide

Open the contributing guide

Research direction

Start by comparing the CFE and analyzer behavior described in the Dart examples, focusing on conditional expressions, null-guards, switch-expression cases, and cascades. Trace where coercions and LUB are applied; done means the language behavior is specified consistently so coercions occur before LUB.

Written by the indexing model from the issue text.

Assessment

Tech stack
dart
Domain
compilers
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.