proposal-do-expressions: Completion value is complex 10 times harder when `break` is involved

As many issues have pointed out, completion values are hard to find out. Although the status quo disallows for loop, it’s still not enough.

I was implementing the type checker for the do expression. It’s an intuitive idea that I only need to recursively check the last statement and check every branch. But when break comes in, it becomes a mess.

expr = do {
a: {
    for (const a of [1, 2]) {
        if (a === 2) { a; break a; }
    }
    3
}
}

For example, the complete value of the expression above is 2. It needs much more work to analyze them correctly (not only for the type checker but also for developers).

Is it possible to ban LabelledStatement and SwitchStatement as the last statement?

SwitchStatement is useful before we have pattern matching, instead of ban it as a whole, maybe we can introduce a “well-formed” version like what we did for if (the last if must have else branch).

  • Each clause either be a fallthrough case with no statement inside
  • Either be a clause that only has an exact 1 break statement and that statement must be the last statement.
  • Must have a default clause.

About this issue

  • Original URL
  • State: open
  • Created 3 years ago
  • Reactions: 2
  • Comments: 17 (7 by maintainers)

Most upvoted comments

@55Cancri Currently the delay is to give us time to assess whether the behavior in this proposal (or aspects of it, like in the OP) is too confusing. I’m hopeful on that front, but it’s going to take a bit more time to work through.

The delay from 2016 is mostly because the person who was originally championing it went to go do other things with his life. I only recently picked it up.

I think it’s a problem more or less, so I implemented IDE support to solve this problem a little.

Regardless of one’s feelings on both explicit and implicit return, imo the only thing worse than ${the one i like less} is users being able to arbitrarily pick one or the other. I think we should either force implicit completion, always, or force explicit completion, always.

The main idea of this issue is to keep the mind model simple.

In most cases, we can find “exits” of a do expression via recursively find the last meaningful statement.

For example,

const val = do {
    try {
        if (expr) expr2
        else expr3
    } catch (e) {
        expr4
    }
}

In this expression,

  1. In the do expression, the last statement that will produce completion value is TryStatement.
    1. In the try block, the last statement that will produce completion value is IfStatement.
      1. In the if block, the last statement that will produce completion value is ExpressionStatement (expr2). So this is an exit point.
      2. In the else block, the last statement that will produce completion value is ExpressionStatement (expr3). So this is an exit point.
    2. In the catch block, the last statement that will produce completion value is ExpressionStatement (expr4). So this is an exit point.
  2. Therefore, expr2, expr3 and expr4 is the exit point of this do expression.

This mental model is not how completion value works in JavaScript, but it’s a good approximation. It’s easier for programmers to understand how do expression works. If we can follow this convention.

The following cases break this analysis with the break statement.

  • LabelledStatement
  • SwitchStatement
switch (true) {
    case true:
         if (Math.random() > 0.5) { 1; break }
         2;
    case false:
}
expr = do {
    a: {
        if (Math.random() > 0.5) { 1; break  }
        2;
    }
}

I think we should try to maintain this mental model instead of allowing some subtle cases that leak how completion value actually works in JavaScript.

Pretty sure the labeled block case you have there is already invalid.

From the readme:

More formally, the completion value of the StatementList can’t rely on the completion value of a loop or declaration. See EndsInIterationOrDeclaration in the proposed specification for details.