Skip to content

Consider pushing implicit conversions down #2129

Open
@stereotype441

Description

@stereotype441

The Dart language currently supports the following kinds of implicit conversions:

  • Conversion from an integer literal to an equivalent double literal
  • Conversion from dynamic to some other type via an implicit cast
  • Conversion from an interface type containing a .call method to a function type, by implicitly tearing off .call

The first conversion only happens on integer literals, and is based on the context type. The other two conversions are currently only performed at sites where the spec demands assignability, and is based on the type being assigned to. The language team is considering changing the behavior so that all implicit conversions are based on context type, and happen at the innermost expression where they're applicable, regardless of assignability constraints. (We would retain the restriction that int-to-double conversion only happens to literals).

Here are some examples to illustrate the user-observable consequences of this change (changes are in bold). (Assume F is a function type, f is an expression of type F , fq is an expression of type F?, C is a callable class with a foo method of type void Function() and a call method of type F, c is an expression of type C, cq is an expression of type C?, and d is an expression of type dynamic. Also assume there's a declaration of Object o = f; in scope, with declared type Object and promoted type F):

  • (1) F x = b ? c : f; will be interpreted as F x = b ? c.call : f;, which will work. Today it's a compile time error because b ? c : f has static type Object, which is not assignable to F.
  • (2) F x = b ? c : d; will be interpreted as F x = b ? c.call : (d as F), which will work, unless b is false and d's runtime type is not F. Today this code is accepted, because b ? c : d has static type dynamic, but it fails at runtime if b is true, because there is no implicit tearoff of call.
  • (3) F x = b ? "x" : d; won't change. Today it's interpreted as F x = (b ? "x" : d) as F;. This will change to F x = b ? ("x" as F) : (d as F);, which is indistinguishable.
  • (4) o = c; will be interpreted as o = c.call;, retaining the promotion on o. Today this code is accepted, but there is no implicit tearoff of call, so it demotes o to Object, which is probably not what the user wants.
  • (5) o = d; will be interpreted as o = (d as F);, which retains the promotion on o, but fails at runtime if d's runtime type is not F. Today this code is interpreted as o = (d as Object);, which demotes o.
  • (6) F x = cq ?? f; won't change. This code is rejected because we don't imply implicit .call tearoffs to nullable expressions, so cq ?? f has type Object, which is not assignable to F.
  • (7) var x = cq ?? f; wont' change. This code is accepted and interpreted as Object x = cq ?? f; (no downcasts or implicit tearoffs).
  • (8) var x = cq ?? d; will be interpreted as C? x = cq ?? (d as C?), which has a more specific type, but will fail at runtime if cq is null and d's runtime type is not C?. Today this code is interpreted as dynamic x = cq ?? d;, which never fails but has a less specific type.
  • (9) var x = fq ?? d; will be interpreted as F? x = fq ?? (d as F?), which has a more specific type, but will fail at runtime if fq is null and d's runtime type is not F?. Today this code is interpreted as dynamic x = fq ?? d;, which never fails but has a less specific type.
  • (10) F x = fq ?? c; will be interpreted as F x = fq ?? c.call;, which will work. Today it's a compile time error because fq ?? c has static type Object, which is not assignable to F.
  • (11) var x = fq ?? c will be interpreted as F x = fq ?? c.call;, which is more useful. Today it's accepted and interpreted as Object x = fq ?? c, which doesn't fail, but also leaves x in a state where it's hard for the user to do anything useful with it, because at runtime it might either be an instance of C or a function.
  • (12) F x = c..foo(); will be a compile-time error because it's interpreted as F x = c.call..foo();, and c.call has type F, which has no method foo. Today it's interpreted as F x = (c..foo()).call;, which works.
  • (13) F x = (c); won't change. Today it's interpreted as F x = (c).call;. This will change to F x = (c.call);, which is indistinguishable.

(Thanks @lrhn for working through most of these examples).

A rough summary of this might be:

  • For the conditional operator (cases (1) through (3)) it's a clear win: code that leads to compile-time and runtime errors today will "just work".
  • For assignments to local variables (cases (4) and (5)) it's probably a win; I suspect that retaining the promotion is more likely to be what the user wants in most circumstances. But it's not as clear cut, since the downside is a tighter downcast for (5), which might lead to a runtime failure if it's not what the user intended.
  • For the null-aware operator (cases (6) through (11)) it's also not 100% clear whether it's an improvement. Cases (10) and (11) seem like clear wins, but cases (8) and (9) could lead to runtime failures. Personally I think it's still a worthwhile tradeoff, because even though (8) and (9) could lead to runtime failures, they also lead to the variable x having a more specific type, which should help reduce dynamism (and catch errors at compile time) in the code that follows.
  • For cascades (case (12)) it's a clear loss of functionality, although IMHO it's probably an acceptable loss, since I think doing this kind of thing is rare, and there's an easy workaround (the user can just explicitly write F x = (c..foo()).call;).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions