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

Should await bind more tightly than postfix !? #319

Closed
Tracked by #110
leafpetersen opened this issue Apr 17, 2019 · 5 comments
Closed
Tracked by #110

Should await bind more tightly than postfix !? #319

leafpetersen opened this issue Apr 17, 2019 · 5 comments
Labels
nnbd NNBD related issues

Comments

@leafpetersen
Copy link
Member

If we simply make ! a postfix operator using the existing grammar, I believe we get the behavior that await e! is parsed as await (e!). Since await is accepting of null, it seems like it would be more useful to make it bind more tightly than !, and hence treat await e! as (await e)!. I'm not sure whether this matches the users intuition or not though. Would it be surprising to the user if await x++ were parsed as await (x++) but await x! were parsed as (await x)!?

Thoughts?

Issue raised by proposed tests here.

cc @lrhn @munificent @eernstg @stereotype441 @danrubel @bwilkerson @scheglov

@danrubel
Copy link

If we make this change, then does that mean that

var x = await foo!.bar();

would be parsed as

var x = (await foo)!.bar();

?
If yes, then yes I think this change will confuse users

@stereotype441
Copy link
Member

I keep coming up with reasons to prefer that we make ! a postfix operator using the existing grammar (and therefore await e! should parse as await (e!)). Here's what I've been thinking:

  • In my mind, there's a sort of kinship between ++, --, member access, method call, and indexing, in that all of them kind of look like suffixes that attach to the right hand side of an expression, and they all have maximum precedence. ! feels like it belongs in the same category, especially given how often we expect users to use it in conjunction with the other suffixes. I guess this argument is similar to @danrubel's.
  • On general principle, it makes sense for ! to bind extremely tightly, because there's practically nothing useful one can do with a nullable value; any time an expression contains both ! and some other operator, the user probably wants the ! to take effect first.
  • For that matter, I'm not even convinced that in the specific case of await there's any reason to prefer (await e)! over await (e!). Both parses have legitimate use cases.
  • I'm already in the habit of having to use parentheses around await e when I want to do operations on the result of the await. So having to explicitly parenthesize to get (await e)! is unsurprising. But I think I'm going to quickly get in the habit of assuming that I never need parentheses around e!, so having to explicitly parenthesize to get await (e!) would be a surprise.
  • If we lower the precedence of !, what do we lower it to? Do we make it bind less tightly than unary prefix operations? Because that seems bad--it would make -x! parse as (-x)!; the current parse of -(x!) is much better.

@leafpetersen
Copy link
Member Author

I think the two parsing concerns can be resolved;

For var x = await foo!.bar(); you'd need to split out e! from the unary expressions, something like:

nullAssertionExpression
    :  unaryExpression negationOperator
;

unaryExpression
    : nullAssertionExpression
    | awaitBodyExpression
;

awaitBodyExpression
    :    prefixOperator unaryExpression
    |    awaitExpression
    |    postfixExpression
    |    (minusOperator | tildeOperator) SUPER
    |    incrementOperator assignableExpression
    ;

awaitExpression
    :    AWAIT awaitBodyExpression
    ;

This doesn't resolve the -x! problem, but I think (but could be wrong) that you can do the same thing here: split unaryExpression again, so that applying ! directly to a prefix operator application isn't a valid parse.

To the rest of the concerns:

  • In my mind, there's a sort of kinship between ++, --, member access,

Fair point.

  • On general principle, it makes sense for ! to bind extremely tightly, because there's practically nothing useful one can do with a nullable value;

I think this is generally true, but not for await.

foo(FutureOr<int?> x) async {
   int y = await x!
}

is valid code if you parse as (await x)! but rejected if you parse as await (x!).

In general, await is already null aware, so projecting out of nullability world doesn't do much for you.

  • For that matter, I'm not even convinced that in the specific case of await there's any reason to prefer (await e)! over await (e!). Both parses have legitimate use cases.

I guess the example you have in mind is something like this?

  foo(Future<int?>? x) {
    int? y = await (x!); // Throw if the future is null, but not if the contents are null
  }

Fair enough.

  • I'm already in the habit of having to use parentheses around await e when I want to do operations on the result of the await.

Good point.

@munificent
Copy link
Member

I'm with Paul. Let's keep it simple.

In general, having precedence at all is sort of a "hack". It lets users omit explicit parentheses when composing some expressions, at the expense of making the actual order of evaluation completely hidden in the source text.

Unless you've memorized the precedence table, you simply don't know how this will evaluate:

a * b + c

Any syntax feature that relies on material already being in the reader's head should be approached with caution. Keywords are somewhat OK because there's at least a word the reader can maybe infer some semantics from. If I've never seen await foo before, I can at least guess that something related to time and pausing is involved. Operators are worse because you can't "read" punctuation. No English dictionary is going to help you guess at what, say, .. does. Precedence is possibly the worst of all because there is no source text at all to look at. You're trying to understand what the absence of explicit grouping characters means.

Thus, I think we should try very very hard to stick to well-established precedence rules and avoid "innovating". We already got burned really badly with cascades.

Parentheses aren't intrinsically bad. They make the code a little more verbose, but they make the precedence explicit, which is good for any reader who doesn't know the precedence. So, in this case, I think keeping the familiar precedence of other unspaced-postfix operators (++, --, ., [...], ?.) is a good thing. The goal isn't to maximize brevity, but clarity.

@leafpetersen
Copy link
Member Author

This all seems reasonable to me. Thanks for the discussion! Closing this, resolving in favor of treating this uniformly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
nnbd NNBD related issues
Projects
None yet
Development

No branches or pull requests

4 participants