Extending ParenthesizedExpression to allow Statement
In the throw expressions proposal we've been discussing the possibility of changing ParenthesizedExpression to use a subset of Statement rather than Expression as a solution to the issues blocking Stage 3 for throw expressions. The grammar would be something like this (with existing references to Statement specifying ~Paren):
CoverParenthesizedExpressionAndArrowParameterList[Yield, Await, Return] :
`(` Statement[?Yield, ?Await, ?Return, +Paren] `)`
...
ParenthesizedExpression[Yield, Await, Return] :
`(` Statement[?Yield, ?Await, ?Return, +Paren] `)`
...
Statement[Yield, Await, Return, Paren] :
[~Paren] BlockStatement[?Yield, ?Await, ?Return]
[~Paren] VariableStatement[?Yield, ?Await, ?Return]
[~Paren] ExpressionStatement[?Yield, ?Await, ?Return]
IfStatement[?Yield, ?Await, ?Return]
BreakableStatement[?Yield, ?Await, ?Return]
ContinueStatement[?Yield, ?Await]
BreakStatement[?Yield, ?Await]
[+Return] ReturnStatement[?Yield, ?Await]
WithStatement[?Yield, ?Await, ?Return]
LabelledStatement[?Yield, ?Await, ?Return]
ThrowStatement[?Yield, ?Await, ?Return]
TryStatement[?Yield, ?Await, ?Return]
DebuggerStatement
EmptyStatement
This would allow most of the statements in Statement (excluding Block (since it would still be parsed as ObjectLiteral), declarations, and ExpressionStatement (since it's handled by the Expression case). We could even replace `(` Expression `)` in ParenthesizedExpression and allow a modified ExpressionStatement instead (as ASI rules would allow us to elide the trailing ;). In effect, the only differences between this and do expressions are that it only allows one statement and cannot create a block scope.
If we were to propose this, we would still need a mechanism to handle block scope. That would either be:
do {}(possibly as adostatement with an impliedwhile (false))(;{ })- block with;before{(odd since we would parse EmptyStatement first and don't allow StatementList here)({; })- block with;after{(definitely not an ObjectLiteral, still a bit strange)
My questions are as follows:
- Should we propose
`(` Statement `)`? - Should it be part of this proposal?
- Should it be its own proposal that subsumes the
doandthrowexpression proposals?
I think it would be more useful to enable using statements as expressions anywhere they would currently result in an error. Including but not limited to:
- Right side of arrow function (
cond => if (cond) { 1 } else { 2 }) - Right side of equals sign (
x = if (cond) { 1 } else { 2 }) - Argument to function (
String(if (cond) { 1 } else { 2 }))
etc
We ran into issues with this in committee when promoting throw expressions. An arbitrary statement in an expression position most likely won't make it to Stage 3. After discussing this outside of committee, those dissenting opinions to throw expressions would be more permissive of this change assuming the rest of the committee also reaches consensus.
Why would using parens be preferred over do { }?
@ljharb: no unnecessary block scope, less boilerplate.
do () could avoid the scope; I find the explicit do pretty important to avoid confusion and make intentions clear.
If do can be used without braces (do if (cond) { 1 } else { 2 }) then it seems like less boilerplate than parenthesis. That is my opinion based on how you must add parenthesis to both ends of the "expression" unlike do.
Edit: on the other hand, parenthesis are used as grouping anyways, so in some case requiring do would result in more characters, since the parenthesis would already be there.
do ()could avoid the scope; I find the explicitdopretty important to avoid confusion and make intentions clear.
Consider that there's little difference between `(` Expression `)` and `(` ExpressionStatement `)` (other than FunctionExpression, ClassExpression, and ObjectLiteral), and you can see how other statements could easily slot in. do {} would likely still be necessary for Block, however. I don't see the do as important in those cases, because const x = do (if (y) z) seems no more or less clear than const x = (if (y) z).
If
docan be used without braces (do if (cond) { 1 } else { 2 }) then it seems like less boilerplate than parenthesis. That is my opinion based on how you must add parenthesis to both ends of the "expression" unlikedo.
do if (cond) { 1 } else { 2 }
(if (cond) { 1 } else { 2 })
If it matters, using () results in one fewer character. The other problem about do without {} or () is confusion about where a statement might end:
do if (cond) 1; else 2; // is this semi part of `else` or ending the ExpressionStatement?
[x]
vs.
(if (cond) 1; else 2;) // definitely ends the `else`
[x]
If we were to propose this, we would still need a mechanism to handle block scope. That would either be:
do {}(possibly as a do statement with an implied while (false))(;{ })- block with ; before { (odd since we would parse EmptyStatement first and don't allow StatementList here)({; })- block with ; after { (definitely not an ObjectLiteral, still a bit strange)
How about (={ }) or ({= }) ?
- block with = before { - looks maybe a bit less odd than ;
let x = (={ }); - block with = after { - its also definitely not an object literal
let x = ({= });
As someone who makes use of do BLOCK in Perl (which functions as described here), I am hoping it gets ratified as proposed. Granted that the mere mention of it also being a Perl feature may just amount to a kiss-of-death.
Where I would find the feature particularly useful is in being able to initialise a const value resulting from a conditional expression that could be more readable when expressed as if ... else if ... else, rather than as a shifty-looking ternary.
const foo = do {
if (...) {
'outcomeA';
} else if (...) {
'outcomeB';
} else {
'outcomeC';
}
};
Without the do expression, I have to resort to using something mutable:
let foo;
if (...) {
foo = 'outcomeA';
} else if (...) {
foo = 'outcomeB';
} else {
foo = 'outcomeC';
}
One less line, but now I feel kind of dirty.
I've seen similar examples use let but I feel these don't convey the potential value.
I think introducing a block scope is a major use case for this proposal, so there would need to be syntax for it. And if there is syntax for that, we don't need syntax for anything else, because any statement can be put in a block. So I don't see much advantage to this over the current proposal.
@bakkot I guess this issue could be adjusted to using const x = (let x = Math.random(); x * x) instead of const x = do { let x = Math.random(); x * x }?
@hax The major problem with that is that it would not be clear whether you were parsing a statement list or an expression until arbitrarily far in. For example, consider
const x = (
foo(/* arbitrary complexity here */);
function foo(){}
);
That's a problem both for readers, who now have to look substantially ahead to understand what they're looking at, and for engines, which for performance reasons really would like to be able to parse in a single pass.
It also gets really complicated with object literals: consider the distinction between
const x = (
{
foo()
{
bar()
}
}
) // legal today, makes an object with a `foo` method
and
const x = (
{
foo()
{
bar()
}
};
) // would be a block with two function calls, presumably
It also gets really complicated with object literals
Great example! I always caught by object literals vs block statement :)
In my original post above, I explicitly call out block scope as I definitely would not want a cover grammar for block vs object literal. Especially since such a cover grammar would be impossible:
{ while(foo) {} }
In a StatementList that's a block with a while loop. In an Expression that's an object literal with a method. There's no cover grammar for that.
Above I mentioned that introducing a true Block would still require do or some other syntax like (; StatementList ).