Skip to content
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

Binding type cast and type check. #1191

Open
lrhn opened this issue Aug 31, 2020 · 4 comments
Open

Binding type cast and type check. #1191

lrhn opened this issue Aug 31, 2020 · 4 comments
Labels
feature Proposed language feature that solves one or more problems field-promotion Issues related to addressing the lack of field promotion nnbd NNBD related issues

Comments

@lrhn
Copy link
Member

lrhn commented Aug 31, 2020

To fill out the option space on promoting non-local variables, let's have a design sketch for a binding type cast and type check.


Dart has type check e is int/e is! int and type cast e as int which both check the type of e against int. The check evaluates to a bool, and if e is a local variable x, it can promote the type of x along the true branch for is, and along the false branch for is!. The cast throws if the corresponding is check would fail, and can promote a local variable's type on the non-throwing branch.

Promotion only works for local variables, and only if the type being checked is a subtype of the current type of the variable. To check and promote something which is not a local variable, or which has a type which is not a supertype of the type being checked, you need to first create a new local variable and bind it to the value. That requires a statement, so it can't be done at the place where the expression would otherwise occur.

This feature will allow is and as to introduce a new variable, and it allows assignments to existing variables to be promotable.

Promotable Assignment

Currently the only expressions which causes is/as to promote are identifiers denoting a local variable.

We will extend that to also apply to expressions of the form x = e where x is a local variable, and expressions of the form (e) where e allows promotion. This allows you to assign-and-promote in a single expression:

class Foo {
  void fooMethod(Foo other) { ... }
}

Object? z;
 ... (z = e) is Foo ? z.fooMethod(z) : ...   // assign e to z, then promote z.
 ... ((z = e) as Foo).fooMethod(z) ...       // assign e to z, then promote z

Then you won't have to move the assignment out of a larger expression and potentially change the evaluation order.

This is not a very high impact feature, but it lays the ground for the even more usable feature below.

Binding Cast and Check

You can now bind and promote in-place, using (x = e) is T. The next step is to allow declaring x in the check/cast.

We use the syntax e is T id and e as T id.

The type check works just like (id = e) is T where id was a pre-existing variable declared in the same block, which was promotable to Type, but which could not be used at its unpromoted type. The scope of the variable is exactly the scope where it would be promoted to a type different from what it was declared as. It is like it's a void variable promoted to T, it can only be used where it's not demoted back to void. The e is! T id will bind id along the false branch, just as x is! T will promote x along the false branch.

The type cast also works as if it was (id = e) as T for a pre-declared variable which is unusable at its unpromoted type.

In either case, the scope of the variable is the entire containing statement block (just as any other local variable declaration), and you are not allowed to use it before its declaring cast/check is evaluated. More precisely, you are only allowed to use the variable in code dominated by the declaration, which is exactly the same code where a promotion would apply.

Generalized Binding Syntax

The T id binding looks very much like a variable declaration, and we'll allow a few more syntactic constructors which look like variable declarations in the same place:

  • e as var x — Pure binding, no type check or promotion.
  • e is final int x — Final binding promotion. Some people prefer final.
  • e as final x; — Pure final binding.

More precisely, what comes after is , is! or as is a <checkType> defined as:

<checkType> ::=
   (`var` | `final` <type>?) <identifier> 
 | <type> <identifier>?
 ;

This mimics the format of parameters or variable declarations, except that single identifier denotes a type, not a name. You need to add var in front to have a single variable.

An expression of the form e as var x or e as final x or e as SomeType x is promotable, just as the assignment to an existing variable would be, so if ((e as var x) is int) will promote x to int on the true branch.

Stronger Binding Cast

The as check usually requires parentheses, (e1.foo.bar.baz as Baz b).qux(b);

This reads badly, you have to look back a long way to see where the parentheses begins.

We introduce a stronger binding as cast:

selector ::= ... 
  | `as`  `(` <checkType `)`

The parentheses ensure that the end of the type is unambiguous.

This allows you to write e1.foo.bar.baz as(Baz b).qux(b).

Possible Alternative Syntaxes

The example above doesn't read as well as one could hope.
The usual way to scan for the end of a selector sequence is to find the first space, skipping over parenthesized things because they are probably arguments.
Here the space occurs before the as, not inside parentheses.

We can perhaps move the start parentheses before the as instead:

selector ::=
  | `(` `as` <checkType> `)`

yielding e1.foo.bar.baz(as Baz b).qux(b).

This allows the parentheses to fit snugly against the previous selector, which makes the expression easier to scan, but also makes it look, possibly too much, like a method call.

We could also use < and > instead of parentheses, yielding one of:

  • e1.foo.bar.baz as <Baz b>.qux(b), or
  • e1.foo.bar.baz<as Baz b>.qux(b)

This looks less like a method invocation, but more like type arguments. However, the <Baz b> is untraditional in Dart for declaring a binding construct, compared to for example catch (e), for (var i;;) and Function(int i). Angled brackets is a place to put a type argument or type parameter, not a non-type-variable binding.

Discussion

The promotable assignment is not really necessary, but it's consistent and makes it easier to explain what the binding check/cast does.

The scope of the variable is the entire block. This is consistent with how Dart currently treat local variables, but that can also sometimes be annoying. If we end up changing the scope of local variables to only be after their declaration, we would need special handling of expression-introduced variables. All other variable introductions happen at the statement level, and we don't consider reachability as an issue there. If a use of a variable is syntactically later than the declaration, and inside the same block, then the use is either dominated by the declaration, or it is unreachable, and we don't have to care. It's easy because all control flow constructs introduce a new syntactic scope. A conditional expression, or a short-circuit operator, does not.

Far-out extra feature: Generic Type Check

Example:

if (e is<T> List<T> list) ...

This could capture the run-time type argument of the type of e at List, and expose both T and list, where list has type List<T>.
Not proposing to do that right now, but it means we should probably reserve the e is<T> syntax.

@lrhn lrhn added the feature Proposed language feature that solves one or more problems label Aug 31, 2020
@munificent
Copy link
Member

Somewhat related (but you got to this first): #1201

@eernstg
Copy link
Member

eernstg commented Sep 9, 2020

I made an attempt to combine two nice properties: The idea from #1201 'if-variables' that we may introduce a variable with an existing name without repeating that name, and the idea from this proposal that the introduction of a new name should be usable in a general expression. The resulting proposal is #1210 'binding expressions'.

@leafpetersen
Copy link
Member

@lrhn I would like to explore what some actual code looks like under these various proposals. Here's an example piece of code that I migrated, which I think suffers from the lack of field promotion. This is from the first patchset from this CL - I subsequently refactored it to use local variables, the result of which can be seen in the final patchset of that CL. This was a fairly irritating refactor, and I don't particularly like the result. Would you mind taking a crack at showing how this code would look under your proposal?

// The type of `current` is `Node`, and the type of the `.left`  and `.right` fields is `Node?`.
while (true) {
      comp = _compare(current.key, key);
      if (comp > 0) {
        if (current.left == null) break;
        comp = _compare(current.left!.key, key);
        if (comp > 0) {
          // Rotate right.
          Node tmp = current.left!;
          current.left = tmp.right;
          tmp.right = current;
          current = tmp;
          if (current.left == null) break;
        }
        // Link right.
        right.left = current;
        right = current;
        current = current.left!;
      } else if (comp < 0) {
        if (current.right == null) break;
        comp = _compare(current.right!.key, key);
        if (comp < 0) {
          // Rotate left.
          Node tmp = current.right!;
          current.right = tmp.left;
          tmp.left = current;
          current = tmp;
          if (current.right == null) break;
        }
        // Link left.
        left.right = current;
        left = current;
        current = current.right!;
      } else {
        break;
      }
    }

@lrhn
Copy link
Member Author

lrhn commented Sep 15, 2020

I'd rewrite that code as:

    // The type of `current` is `Node`, and the type of the `.left`  and `.right` fields is `Node?`.
    while (true) {
      comp = _compare(current.key, key);
      if (comp > 0) {
        if (current.left is! Node currentLeft) break;
        comp = _compare(currentLeft.key, key);
        if (comp > 0) {
          // Rotate right.
          current.left = currentLeft.right;
          currentLeft.right = current;
          current = currentLeft;
          if (currentLeft.left is! Node leftLeft) break;
          currentLeft = leftLeft;
        }
        // Link right.
        right.left = current;
        right = current;
        current = currentLeft;
      } else if (comp < 0) {
        if (current.right is! Node currentRight) break;
        comp = _compare(currentRight.key, key);
        if (comp < 0) {
          // Rotate left.
          current.right = currentRight.left;
          currentRight.left = current;
          current = currentRight;
          if (currentRight.right is! Node rightRight) break;
          currentRight = rightRight;
        }
        // Link left.
        left.right = current;
        left = current;
        current = currentRight;
      } else {
        break;
      }
    }

If writing it from scratch, I'd probably go with more positive tests and else branches for the breaks.

(Also, consider making break an expression like throw:

  var currentLeft = current.left ?? break;

The "problem" with ?? operators is that you need a value as alternative, so you need an if if you don't have one and just want to promote or branch.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature Proposed language feature that solves one or more problems field-promotion Issues related to addressing the lack of field promotion nnbd NNBD related issues
Projects
None yet
Development

No branches or pull requests

4 participants