Background
The language spec currently has an "allow list" indicating the circumstances in which an expression is allowed to have a static type of void; any other circumstance in which an expression ever has a static type of void is considered to be a compile-time error.
Although the spec's allow list is quite long, it doesn't cover all the situations in which the implementations allow void. For example:
- the front end and analyzer allow the branches of a switch expression to have type
void; the spec's allow list doesn't say anything about switch expressions.
- the front end and analyzer allow the RHS of a pattern assignment to be
void. The spec doesn't allow this.
There are also several cases where the front end and analyzer disagree about whether void should be allowed:
- The front end allows the receiver of
[] and []= to have a static type of void (assuming a suitable extension exists that defines these operators, e.g. extension on Object? { operator [](index) => 0; }). The spec and the analyzer do not allow this.
- The front end allows the scrutinee of a switch statement to have a static type of
void. The spec and the analyzer do not allow this.
- The front end doesn't allow the index of a compound index set to be void (e.g.,
foo[voidValue]++), even if the corresponding operator [] and operator []= declarations define the type of their index parameter to be void. The spec and the analyzer do allow this.
Part of the reason for the discrepancies is that the spec's "allow list" approach doesn't correspond very closely with how the "void can't be used here" errors are implemented in the analyzer and the front end:
- In the front end, the code for analyzing a subexpression accepts an
isVoidAllowed parameter that defaults to false (so the exceptions to this default do, in effect, constitute an allow list), but there are a large number of cases where true is passed to isVoidAllowed, even though void is actually not allowed, in order to avoid issuing redundant error messages. For example, the visitDoStatement method passes isVoidAllowed: true when analyzing the condition of the do/while loop, because the invalid use of void is caught by a later call to ensureAssignableResult (which makes sure the loop condition is a boolean). Because of this approach, there's a danger than when adding new features to the language, we will accidentally copy isVoidAllowed: true to places where it doesn't belong. (This appears to have happened during the implementation of patterns, for example.)
- In the analyzer, the code for reporting invalid uses of
void exists in many ad hoc places in the resolver, one for each circumstance in which an expression is not allowed to have a static type of void. Essentially this acts as a "deny list"; this has the effect that when adding new features to the language, we are liable to forgot to add the appropriate void checks. (This also appears to have happened during the implementation of patterns.)
Also possibly of interest: project https://github.com/orgs/dart-lang/projects/111/views/1
After talking with several folks about the situation, I believe we would be better served if we agreed upon a general guiding principle for when an expression is allowed to have static type void. The implementations should implement that general principle directly, rather than having a bunch of "allow" or "deny" cases. The spec should also state the general principle directly.
Proposal
Here's my proposal for what the general principle could be:
- Every place where an expression may appear in the grammar is considered to be an assigned expression, a dispatched expression, a discarded expression, or a relayed expression, based on the runtime semantics of the containing construct:
- When the runtime semantics of the containing construct are to assign the value of the expression to a variable or parameter, or store it in a data structure, the expression is considered an assigned expression. Examples include the initializer of a variable declaration, the argument to a method call, the RHS of a user-definable binary operator, and the elements of a collection literal. Assigned expressions are always associated with an explicit assignability check. They are permitted to have a static type of
void if and only if the destination type in the assignability check is void. So, for example, in void x = foo;, foo may have a static type of void, but in Object? x = bar;, bar may not.
- When the runtime semantics of the containing construct depend on the specific value of the expression (or its type), the expression is considered a dispatched expression. Examples include the receivers of method invocations, the boolean conditions of flow control constructs, and the operands of
is, as, and await expressions. Dispatched expressions may never have a static type of void.
- When the runtime semantics of the containing construct are to ignore the value of the expression, the expression is considered a discarded expression. Examples include expression in an expression statement, and the updaters in a
for loop. Discarded expressions are permitted to have a static type of void (in the absence of other errors of course).
- When the runtime semantics of the containing construct are for the value of the (sub)expression to become the value of the containing expression, the subexpression is considered a relayed expression. Examples include the subexpression of a parenthesized expression, the branches of a
switch or conditional expression, and the RHS of ??. Relayed expressions are permitted to have a static type of void (in the absence of other errors). For example, x ?? voidExpr; is ok, but Object? y = x ?? voidExpr; is not (because x ?? voidExpr has type void).
I'm circulating this to get an initial impression from the language team of whether this sounds like a good idea before embarking on additional work. Before going through with the idea, I would want to prototype it and run it through google3 to make sure it's not too breaking.
Background
The language spec currently has an "allow list" indicating the circumstances in which an expression is allowed to have a static type of
void; any other circumstance in which an expression ever has a static type ofvoidis considered to be a compile-time error.Although the spec's allow list is quite long, it doesn't cover all the situations in which the implementations allow
void. For example:void; the spec's allow list doesn't say anything about switch expressions.void. The spec doesn't allow this.There are also several cases where the front end and analyzer disagree about whether
voidshould be allowed:[]and[]=to have a static type ofvoid(assuming a suitable extension exists that defines these operators, e.g.extension on Object? { operator [](index) => 0; }). The spec and the analyzer do not allow this.void. The spec and the analyzer do not allow this.foo[voidValue]++), even if the correspondingoperator []andoperator []=declarations define the type of theirindexparameter to bevoid. The spec and the analyzer do allow this.Part of the reason for the discrepancies is that the spec's "allow list" approach doesn't correspond very closely with how the "void can't be used here" errors are implemented in the analyzer and the front end:
isVoidAllowedparameter that defaults tofalse(so the exceptions to this default do, in effect, constitute an allow list), but there are a large number of cases wheretrueis passed toisVoidAllowed, even though void is actually not allowed, in order to avoid issuing redundant error messages. For example, thevisitDoStatementmethod passesisVoidAllowed: truewhen analyzing the condition of the do/while loop, because the invalid use ofvoidis caught by a later call toensureAssignableResult(which makes sure the loop condition is a boolean). Because of this approach, there's a danger than when adding new features to the language, we will accidentally copyisVoidAllowed: trueto places where it doesn't belong. (This appears to have happened during the implementation of patterns, for example.)voidexists in many ad hoc places in the resolver, one for each circumstance in which an expression is not allowed to have a static type ofvoid. Essentially this acts as a "deny list"; this has the effect that when adding new features to the language, we are liable to forgot to add the appropriatevoidchecks. (This also appears to have happened during the implementation of patterns.)Also possibly of interest: project https://github.com/orgs/dart-lang/projects/111/views/1
After talking with several folks about the situation, I believe we would be better served if we agreed upon a general guiding principle for when an expression is allowed to have static type
void. The implementations should implement that general principle directly, rather than having a bunch of "allow" or "deny" cases. The spec should also state the general principle directly.Proposal
Here's my proposal for what the general principle could be:
voidif and only if the destination type in the assignability check isvoid. So, for example, invoid x = foo;,foomay have a static type ofvoid, but inObject? x = bar;,barmay not.is,as, andawaitexpressions. Dispatched expressions may never have a static type ofvoid.forloop. Discarded expressions are permitted to have a static type ofvoid(in the absence of other errors of course).switchor conditional expression, and the RHS of??. Relayed expressions are permitted to have a static type ofvoid(in the absence of other errors). For example,x ?? voidExpr;is ok, butObject? y = x ?? voidExpr;is not (becausex ?? voidExprhas typevoid).I'm circulating this to get an initial impression from the language team of whether this sounds like a good idea before embarking on additional work. Before going through with the idea, I would want to prototype it and run it through google3 to make sure it's not too breaking.