Open jorendorff opened 6 years ago
This is definitely a tough one. I'll just say my expectation would be that break
breaks out of the while
, not the do
. Assuming we'd want consistent semantics to allow continue
, I feel like I could easily see myself wanting to do
while (getThing()) {
items.push(do {
const value = getItem();
if (!value) continue;
value;
});
}
or something along those lines to bail out part-way through some calculation the same way I'd assume a return;
there would also bail out.
break
to me breaks out of the curly braces. I would expect both break
and continue
to exit the do
, especially since return
doesn't.
Interesting, I don't think I've ever associated it with braces, but rather with loops and switch specifically.
You can just as easily do
while (true) {
while (true) break;
}
which breaks the inner loop even though it doesn't have braces
or
while (true) {
if (true) { break; }
}
which breaks the loop, not the if
even through it has braces.
The only place where unlabelled break
isn't tied to a loop is switch
which I generally see as a special case.
I guess at the end of the day you could always do
while (true) {
var foo = do { label: {
4;
break label;
};};
}
if you wanted to break out of the do expression 😬 depending on wherever the spec settles...
sure, but do
can't omit the braces; i guess i'd say, it should break/continue out of the closest context/construct?
The braces don't mean anything to me essentially, so the definition of "closest construct" depends on what things you think are important constructs. It seems like the only viable rule is "the one we think people will use the most". switch
es are essentially grandfathered in since break
is critical for their functionality, but loops are common, and being able to break out of them with minimal syntax is important. So the question is, is being able to break out of a loop with minimal syntax (since most people don't know what labels are, or that they exist in JS) more important than being able to easily break out of the current do expression.
I personally feel like I struggle to think of cases where I'd want to break out of a do expression on its own, so the conversation may also be easier if there were examples.
If you try to use break;
outside loops or switch statements, it results in
SyntaxError: unlabeled break must be inside loop or switch
Here's an example:
{
console.log(1);
break;
console.log(2);
}
@ljharb
break
to me breaks out of the curly braces.
If you really think so, you’ll have trouble to understand that break
is illegal in the following case:
if (true) {
break;
}
and even that it breaks the infinite loop in the following case:
while (true) {
break;
}
because break
must act on the while-statement, not just on the block it controls.
In general, unlabelled break
acts on so-called breakable statements (for/while/do-while/switch), which are mostly loop statements, and labelled break
acts on labelled statements.
The question is: Will the do-expression be specced as a breakable expression or not? Since it is not a loop expression, the answer is a priori no, unless we find a good reason for it.
@claudepache that confusion suggests that break
/continue
should perhaps be illegal directly inside a do expression, since conceptually they're like inline IIFEs.
conceptually they're like inline IIFEs.
I feel like that's an assertion based on your mental model, but not reflected anywhere in this spec. It seems like a very limiting way to think of them and I'm not clear on how we'd benefit from promoting that model.
I conceptually think of them like they are in Rust. Rather than an IIFE, I envision them as just a sequence of statements like they would be anywhere else, which seems more in line with how this proposal is currently presented, meaning that break
and continue
and return
should work just like they would in any normal statement context, just like throw
they create an abrupt completion record that propagates upward skipping execution of later statements until they hit a block that has special behavior defined for abrupt completions, like loops and switches.
Indeed, if this whole feature is just some sort of IIFE, then it's pointless. Instead it's a generic way to turn statements into expressions, which is much more interesting and powerful.
Rather than an IIFE, I envision them as just a sequence of statements like they would be anywhere else, which seems more in line with how this proposal is currently presented, meaning that break and continue and return should work just like they would in any normal statement context, just like throw they create an abrupt completion record that propagates upward skipping execution of later statements until they hit a block that has special behavior defined for abrupt completions, like loops and switches.
At one time I had all the mechanisms in the ES6 spec. to handle do expression exactly this way. Took them out when it was clear that do expression weren't going to get in.
The only real question was situations like this:
for (;; do {if(cond) break}) body;
and
for (;; do {if(cond) contine)) body;
(note no labels) Seems reasonable that break
would break out of the for
loop. But what about continue
? I speculatively made continue
in for headers act like a break
but that is certainly debatable.
That's a fun one. I'd probably have assumed that continue
there would just cause an infinite loop, then leave it to linters and/or common sense to make people not put them there.
So to put it in spec text terms, my expectation for break
is
DoExpression: do { StatementList }
- Let stmtCompletion be the result of evaluating StatementList.
- Return Completion(UpdateEmpty(stmtCompletion, undefined)).
rather than special-casing break to result in a normal completion:
DoExpression: do { StatementList }
- Let stmtCompletion be the result of evaluating StatementList.
- If stmtCompletion.[[Type]] is break, return NormalCompletion(undefined).
- If stmtCompletion.[[Type]] is continue, return (an error probably?).
- Return stmtCompletion.
My expectation is that:
//statements
{ /* StatementList */ }
//statements
evaluates identically to:
//statements
do { /* StatementList */ } ;
//statements
So the grammar production for DoExpression would be:
DoExpression : do Block
(ignoring grammar parameters for now)
and the evaluation semantics for it would be:
The responsibility of dealing with abrupt completions (particularly continue completions) falls on the semantics of the iteration statements that have some sort of continue semantics, not on the statement/expression that generates the continue.
In the current language, there is no way for a continue abrupt completion to occurs in the head of a for statement so the semantics doesn't need to deal with that possibility. The addition of do statements (with full Tennant's correspondence with Block) means that iteration statements have to deal with unlabeled break and continue in all evaluative positions. The handling could be as simple as throwing an exception. It can't just be ignored (with the current factoring of the specification) because there is currently no mechanism to convert unhandled break/continue abrupt completions into exceptions.
@allenwb That's a fair point. I probably don't actually have the UpdateEmpty
call in the optimal place in my example. If we go the route of
Return the result of evaluating Block.
I'd imagine the spec would need to expand usage of UpdateEmpty
to include each expression's runtime semantics too, is that your expectation as well? It seems like we would have to in order to keep things semantically similar to the behavior of statements. For example, with no changes to expression runtime semantics
expressionStatement;
expressionStatement;
do { break ; }
behaves differently from
expressionStatement;
(expressionStatement, do { break ; });
which, to me at least, seem than idea, so it would require the addition of UpdateEmpty
calls in the runtime semantics of the comma operator.
Similarly
var result = do {
while(true) {
"prefix";
if (foo) 4;
else break;
}
};
result === 4 or undefined
but
var result = do {
while(true) {
"prefix";
foo ? 4 : do { break };
}
};
result === 4 or "prefix"
Note undefined
vs prefix
. So we'd need additional UpdateEmpty
usage in the ConditionalExpression runtime semantics.
And really this would end up applying to all expression types, since it's equally an issue for
var result = do {
while(true) {
"prefix";
4 + do {break};
}
};
I'm assuming we'd want a completion value of undefined
here instead of "prefix"
because logically "prefix"
is no longer the most recently executed item.
Hopefully I'm not misunderstanding somewhere.
I'm sure that we need to have Tennant's correspondence between:
expressionStatement;
expressionStatement;
do { break ;};
and
expressionStatement;
expressionStatement;
{ break ; }
But it it's at all obvious that we need or want it for:
expressionStatement;
(expressionStatement, do { break ; });
That latter isn't a direct Tennant's correspondence situation as a additional operator (,) has been added. As I mentioned in the other thread, we need to think carefully about whether we want completion value propagation to generally apply between subexpressions. My initial think is that it isn't needed/desirable.
I agree. On the whole, it would be nice to make (do { S1 }, do { S2 })
equivalent to (do { S1 S2 })
, but it is definitely not "Tennant's correspondence". In any case that is the topic of issue #22.
In any case, the question in this issue has been answered. (do { break; });
equals break;
.
To close the bug we just need a PR to clarify the proposal.
I read the whole thread and agree (do { break; });
should equals break;.
. But I also suspect maybe we should make do { break; }
syntax error just like current eval('break;')
if there is no strong use cases.
The only near-real use case in this thread is
while (getThing()) {
items.push(do {
const value = getItem();
if (!value) continue;
value;
});
}
But I feel this example just show the dark side of allowing direct continue/break in the do block. It's not easy to recognize there is a continue
statement inside and actually dismiss the push
operation. As a code reviewer, I would prefer the plain code like
while (getThing()) {
const value = getItem();
if (value) items.push(value);
}
Much simpler, shorter and clearer.
Hope someone can give some good use cases.
What does
break;
do inside ado
-expression inside a loop? @ljharb and I had opposite intuitions, both of which seem reasonable (to me). (See issue #22.)The proposal says,
I take this to mean that
(do { break; });
is equivalent tobreak;
and thereforedo
-expressions are invisible tobreak
. (To me, the use of the phrase “Tennant's Correspondence Principle” indicates that this is intentional; maybe @dherman can confirm or deny?)