Skip to content

Do null aware operators use promotion semantics? #290

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

Closed
leafpetersen opened this issue Mar 25, 2019 · 6 comments
Closed

Do null aware operators use promotion semantics? #290

leafpetersen opened this issue Mar 25, 2019 · 6 comments
Labels
nnbd NNBD related issues

Comments

@leafpetersen
Copy link
Member

We have decided that null aware operators shall short-circuit. That is, the following is valid code:

class A {
  int foz() => 3;
}
void main() {
  A? x = A();
  x?.foz.isEven;
}

Notionally, we treat x?.foz.isEven as de-sugaring (x == null) ? null : x.foz.isEven. Note that this interpretation relies on promotion of x to int to make the de-sugaring produce the appropriate errors (or lack thereof).

If we take this interpretation as normative then the following should be an error:

void main() {
  FutureOr<Future<int>?> x = ...;
  x?.then((Object x) => x);
}

This is an error since no promotion rule applies to x, and so it remains potentially nullable. Alternatively, we can simply specify the errors and warnings for this directly, thereby permitting this code.

cc @lrhn @eernstg @munificent

@leafpetersen leafpetersen added the nnbd NNBD related issues label Mar 25, 2019
@lrhn
Copy link
Member

lrhn commented Mar 26, 2019

We need to define exactly how far the null-detection reaches.

I suggest using the same limits as a cascade: a sequence of selectors, potentially trailed by an assignment.

That is x?.foo.bar.baz = 24 is entirely elided if x is null, but x?.foo + bar is not, and you will get an error because x?.foo has a nullable type which does not allow +.

Using the same extent as a cascade makes it easier for users to learn and reason about. We already have the syntax in the grammar, but we might have to tweak it a little because ?. does not have the same "precedence" as ...

The new ! suffix operator should be a selector as well, so x?.foo!.bar is valid.

That means that x?.foo! + bar would still not be allowed, you have to write (x?.foo)! + bar to override the nullable type. That's not a big issue because the code is probably a mistake - if x can be null, then (x?.foo) can also be null, and you should only use ! when you know (for reasons unknowable to the type system) that the value is most likely not null. If x cannot be null, you should have written x!.foo + bar to begin with.

We should consider introducing ?.[expr] as a valid initial selector, like it is for cascades. It's a common request to be allowed to do null-aware indexing, and being symmetric with cascades is probably a good idea.

I'm fine with nothing working for FutureOr except is-checks and await. Calling then on something which is either Future<Object> or Future<Future<Object>?> does not have a well-defined method signature to call. Negative promotion (if (x is! Future<Object>) or if (x == null) ... else ...) will only work when the original type can be represented as a two-element union, and we check against one of them (or at least when what remains can be represented easily in the type system).

@munificent
Copy link
Member

I suggest using the same limits as a cascade: a sequence of selectors, potentially trailed by an assignment.

I'm not sure I follow this. I don't exactly what you mean by "limits as a cascade", but cascaded setters certainly make that a fuzzy concept:

a..b = c..d = e;

This (for better or worse) is a single series of cascaded selectors. But the corresponding null-aware chain means something entirely different:

a?.b = c?.d = e;

we might have to tweak it a little because ?. does not have the same "precedence" as ...

That's putting it lightly. .. is all the way on the other end of the precedence table.

main() {
  print(true ? [1] : [2]..add(3)); // "[1, 3]".
}

The new ! suffix operator should be a selector as well, so x?.foo!.bar is valid.

SGTM. Though I would describe it more as a "high precedence postfix operator" than a "selector", though I guess the latter is just the grammar's name for the former.

That means that x?.foo! + bar would still not be allowed, you have to write (x?.foo)! + bar to override the nullable type.

This feels like a weird syntactic restriction to me. My mental model is that you have a primary expression followed by any number of "call-ish" postfix things: (...) for function/method calls, identifier for getters, [...] for index operators, etc. We'd just add ! to that list.

I'm digging around in the grammar and I find it pretty confusing, though that's somewhat to be expected because of cascades. I see:

expression:
  assignableExpression assignmentOperator expression |
  conditionalExpression cascadeSection* |
  throwExpression

assignableExpression:
  primary (argumentPart* assignableSelector)+ |
  "super" unconditionalAssignableSelector |
  identifier

unconditionalAssignableSelector:
  "[" expression "]" |
  "." identifier

If I'm reading that right, it would disallow:

super.x.y = 1;

Since only a single unconditionalAssignableSelector can follow super. But that's clearly not right. :-/

In my mind, the grammar with ! would be something like:

expression: 
  assignableExpression assignmentOperator expression |
  cascadeExpression |
  throwExpression

fieldInitializer:
  ("this" ".")? identifier = cascadeExpression

cascadeExpression:
  conditionalExpression ( cascadeSection+ | invocationSection* )

cascadeSection: 
  ".." cascadeSelector invocationSection*
      (assignmentOperator expressionWithoutCascade)? 

cascadeSelector: 
  "[" expression "]" |
  identifier argumentPart?

invocationSection:
  "?." identifier |
  unconditionalInvocationSection

unconditionalInvocationSection:
  argumentPart |
  "." identifier |
  "[" expression "]" |
  "!"

postfixExpression:
  invocation postfixOperator?

invocation: 
  primary invocationSection*

assignableExpression:
  invocation
  "super" unconditionalInvocationSection
  identifier

But I'm probably missing some subtlety. Either way, we should be able to slot
! into the grammar such that it can follow both a primary expression and an invocation chain if that's what we want.

@eernstg
Copy link
Member

eernstg commented Mar 28, 2019

PS: This CL updates the spec parser to handle ui-as-code collections. This would make grammar experiments easier to sanity check.

@lrhn
Copy link
Member

lrhn commented Mar 28, 2019 via email

@munificent
Copy link
Member

No, primary can be super.id as well.

Ah, thanks for walking through the derivation.

It's probably best to let the c?.d be its own expression,
nested inside the outer ?..

Yes, definitely.

@leafpetersen
Copy link
Member Author

Decided, yes.

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