Skip to content

Always coerce before LUB #3363

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

Open
lrhn opened this issue Sep 20, 2023 · 0 comments
Open

Always coerce before LUB #3363

lrhn opened this issue Sep 20, 2023 · 0 comments
Labels
least-upper-bound Proposals about improvements of the LUB algorithm

Comments

@lrhn
Copy link
Member

lrhn commented Sep 20, 2023

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.

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

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

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

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

// Just some value.
dynamic d = 42;

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

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

  f = vf<int>; // Valid.
  f = vf; // Valid, implicitly instantiated to be the same as Id<int>.
  f = (b ? vf<int> : vf); // LUB is `Function` in CFE, accepted in Analyzer!
  f = switch (b) {true => vf<int>, false => vf}; // Same
  
  f = fn<int>; // Valid.
  f = fn; // Valid, implicitly instantiated to be the same as Id<int>.
  f = (b ? fn<int> : fn); // LUB is `Function` in CFE, accepted in Analyzer!
  f = switch (b) {true => fn<int>, 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<int>; // Valid.
  f = GenId().call; // Valid, implicit instantiated.
  f = GenId(); // Valid, implicit `call` tear-off, then instantiated.
  f = b ? GenId().call<int> : GenId().call; // Accepted in both CFE and Analyzer
  f = switch (b) { true => GenId().call<int>, 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>(T)`
  f = vf..isIntFunc; // Error in CFE, not declared on `T Function<T>(T)`
}

extension on int Function(int) {
  // The cascade knows!
  void get isIntFunc {}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
least-upper-bound Proposals about improvements of the LUB algorithm
Projects
None yet
Development

No branches or pull requests

1 participant