let/const in switch cases

# Luke Hoban (13 years ago)

According to the current draft spec text 'let'/'const' are allowed in the statement list of a switch case, but contribute to the block scope of the outer block. This can lead to some confusing situations:

function(x) {    do {         switch(x) {             case 0:                 return x;             case 1:                 let x = 'let';         }     } while (foo()); }

The 'x' in 'case 0' here will bind to the later 'let x', but depending on which case executes first, this may or may not trigger an error on accessing x before it is defined. This is the only place in the language where a reference to a let binding declared in the same block scope as the reference cannot be statically understood to be a valid vs. invalid read. This is both likely to cause confusion for developers, and adds implementation complexity.

There's an argument to be made that switch case StatementLists should not be allowed to include Declarations. This would make the code above a syntax error, and require putting {} around the case if 'let'/'const' were needed. This also aligns with all other occurrences of nested statements that are not surrounded by {} in the grammar, for example, 'if(true) let x = 3'. Current Chrome builds appear to follow this approach, reporting that the 'let' in the initial code sample above appears in an 'unprotected statement context'.

Luke

# Brendan Eich (13 years ago)

Luke Hoban wrote:

According to the current draft spec text 'let'/'const' are allowed in the statement list of a switch case, but contribute to the block scope of the outer block. This can lead to some confusing situations:

function(x) { do { switch(x) { case 0: return x; case 1: let x = 'let'; } } while (foo()); }

The 'x' in 'case 0' here will bind to the later 'let x',

That's right, let hoists to braced body or block top -- explicit is better than implicit.

I did this in JS1.7 and we've had years of experience with it, in SpiderMonkey and Rhino. Alternative of implicit per-"case" scope does not work due to C-like fall-through inherited via Java. Programmers do write let in case- and default-labeled statement lists in switches in Mozilla-specific code and I've never heard of a usability problem.

We could be restrictive (more below), but you have to make a usability or implementation hardship case. I don't think you have.

but depending on which case executes first, this may or may not trigger an error on accessing x before it is defined. This is the only place in the language where a reference to a let binding declared in the same block scope as the reference cannot be statically understood to be a valid vs. invalid read.

Sure, but why did you exclude closures?

Closures can nest in blocks and capture let and const, and use a let or const binding before it has been initialized. Closures are a big part of JS. The "declared in the same block ... as the reference" is therefore too narrow as an objection to the infeasibility of statically analyzing let-in-switch.

Without such narrow special pleading, we have with the temporal dead zone a use-before-init rule that cannot be statically analyzed, period. Closures are enough, switch is not novel. If it has other issues, let's argue about them, but this one is off target due to closures.

This is both likely to cause confusion for developers, and adds implementation complexity.

Not in our actual, >5-years-long experience. Examples:

dxr.mozilla.org/mozilla-central/b2g/chrome/content/forms.js.html#l50, dxr.mozilla.org/mozilla-central/b2g/chrome/content/shell.js.html#l290, dxr.mozilla.org/mozilla-central/b2g/chrome/content/shell.js.html#l300, dxr.mozilla.org/mozilla-central/b2g/chrome/content/shell.js.html#l301 etc. dxr.mozilla.org/mozilla-central/toolkit/components/places/nsPlacesExpiration.js#l931, dxr.mozilla.org/mozilla-central/services/sync/modules/engines/tabs.js#l303 and many others...

Programmers can do this with var and they expect to do the same with let.

There's an argument to be made that switch case StatementLists should not be allowed to include Declarations. This would make the code above a syntax error, and require putting {} around the case if 'let'/'const' were needed.

We considered that back in the ES4 era, but it is onerous. Lots of vars in sub-statements, and people reasonably expect to write let declarations the same. There is a braced body-block to scope such let declarations. Typically the case "arms" have disjoint declarations, and programmers definitely know to use a block to avoid colliding.

This also aligns with all other occurrences of nested statements that are not surrounded by {} in the grammar, for example, 'if(true) let x = 3'.

Yes, but switch is different. It has a braced body. Its statement-lists have fall through so the more relevant "if" comparison would be something like

{ let x = 42; if (y) x = 43; alert(x); }

which is perfectly legal in ES6 and any Harmony or ES4-era proposal.

Current Chrome builds appear to follow this approach, reporting that the 'let' in the initial code sample above appears in an 'unprotected statement context'.

Are we all trying to follow the draft spec, or not? Thanks for proposing to change it first. Implementations deviating without talking first = disharmony.

# Andreas Rossberg (13 years ago)

On 10 August 2012 05:48, Brendan Eich <brendan at mozilla.org> wrote:

Luke Hoban wrote:

Current Chrome builds appear to follow this approach, reporting that the 'let' in the initial code sample above appears in an 'unprotected statement context'.

Are we all trying to follow the draft spec, or not? Thanks for proposing to change it first. Implementations deviating without talking first = disharmony.

To be fair, IIRC we implemented that before there was much of a spec covering details like that. So we picked what we thought makes most sense (and is most conservative).

Having said that, I fully agree with Luke. The switch statement is what it is, unfortunately, and beyond repair, but to maintain scoping sanity, we should rule out examples that mistake its body for a proper block.

There is precedent for that in C++, where you cannot place non-trivial declarations into a switch block either.

# Brendan Eich (13 years ago)

Andreas Rossberg wrote:

On 10 August 2012 05:48, Brendan Eich<brendan at mozilla.org> wrote:

Luke Hoban wrote:

Current Chrome builds appear to follow this approach, reporting that the 'let' in the initial code sample above appears in an 'unprotected statement context'. Are we all trying to follow the draft spec, or not? Thanks for proposing to change it first. Implementations deviating without talking first = disharmony.

To be fair, IIRC we implemented that before there was much of a spec covering details like that. So we picked what we thought makes most sense (and is most conservative).

Fair enough.

Having said that, I fully agree with Luke. The switch statement is what it is, unfortunately, and beyond repair,

In a "make it look like Java" regime, it could not be otherwise, although it came in a year later, 1996. The alternative of leaving it out was at that point "hard" because else-if trees are a pain and retest the discriminant. The alternative of different syntax with better semantics was out because of (a) the C (C++ -> Java)

pedagogy/code-reuse/brainprint "gravity well"; (b) still not enough time on my side to do better.

ECMA-262 Edition 1 lacked switch but the committee was not in a good state to invent, so in '98 and '99, ES3 drafts codified switch with fall through, break and continue to label, do-while, try/catch/finally -- all pretty safe bets based on Java (some too much so, e.g. instanceof).

but to maintain scoping sanity, we should rule out examples that mistake its body for a proper block.

I gave lots of examples (there are many more) of Mozilla JS that uses let in switch-case statement lists without issue. That code is not insane, and it doesn't need braces. But if TC39 agrees to require braces, some poor soul is gonna have to go waste some minutes to hours adding them. Seems a shame.

There is precedent for that in C++, where you cannot place non-trivial declarations into a switch block either.

C++ broke Duff's-device use-cases where in C one can declare in the braced body of the switch, yes. Is C++ the exemplar here?

If we expect guards on declarations, perhaps so. I can give this one up, but againt the people who will have to add gratuitous braces to extant JS code will want your head and mine, for a few minutes anyway.

# Allen Wirfs-Brock (13 years ago)

On Aug 9, 2012, at 8:48 PM, Brendan Eich wrote:

Luke Hoban wrote:

According to the current draft spec text 'let'/'const' are allowed in the statement list of a switch case, but contribute to the block scope of the outer block. This can lead to some confusing situations:

function(x) { do { switch(x) { case 0: return x; case 1: let x = 'let'; } } while (foo()); }

The 'x' in 'case 0' here will bind to the later 'let x',

That's right, let hoists to braced body or block top -- explicit is better than implicit.

I did this in JS1.7 and we've had years of experience with it, in SpiderMonkey and Rhino. Alternative of implicit per-"case" scope does not work due to C-like fall-through inherited via Java. Programmers do write let in case- and default-labeled statement lists in switches in Mozilla-specific code and I've never heard of a usability problem.

We could be restrictive (more below), but you have to make a usability or implementation hardship case. I don't think you have.

A more illustrative example for Luke's concerns would be:

function(x) { do { switch(x) { case 0: return x; //always a runtime error case 1: let x; x = 'let'; //never a runtime error case 2: return x; //sometimes a runtime error } } while (foo()); }

The classifications (always, never, sometimes) of references can be determined by a fairly straightforward static analysis. You only need to do a runtime TDZ check on the sometimes case. You can issue compile-time warnings on the always and sometimes cases.

From a usability case we have to consider which is more likely something like the above or something like:

 switch (x) {
   case 0:
       let a = ...;
       // lots of lines of code using a
       break;
   case 1:
        let b = ...;
        // lots of lines of code using b
        break;
    case 2:
        ...
}

My guess was that the disjoint case would be more common and more annoying if it was disallowed. I considered spec'ing each case alternative as a block but I concluded it would be unwise to introduce a block scope that was not associated with { }'s.

Basically, the switch body is a block and all the static semantic rules for blocks apply to it as do the same dynamic initialize-before-reference TDZ rules. This seems straightforward enough to explain to users. The only oddity is that it has multiple entry points which means some TDZ violations can't be be statically predicted, but that is an optimization issue rather than one of user understanding.

# Brendan Eich (13 years ago)

Allen Wirfs-Brock wrote:

From a usability case we have to consider which is more likely something like the above or something like:

  switch (x) {
    case 0:
        let a = ...;
        // lots of lines of code using a
        break;
    case 1:
         let b = ...;
         // lots of lines of code using b
         break;
     case 2:
         ...
 }

My guess was that the disjoint case would be more common and more annoying if it was disallowed.

Exactly!

Did anyone read the Mozilla JS at those DXR links I gave? :-P

I considered spec'ing each case alternative as a block but I concluded it would be unwise to introduce a block scope that was not associated with { }'s.

No can do with fall-through.

# Allen Wirfs-Brock (13 years ago)

On Aug 14, 2012, at 5:05 AM, Andreas Rossberg wrote:

On 10 August 2012 05:48, Brendan Eich <brendan at mozilla.org> wrote:

Luke Hoban wrote:

Current Chrome builds appear to follow this approach, reporting that the 'let' in the initial code sample above appears in an 'unprotected statement context'.

Are we all trying to follow the draft spec, or not? Thanks for proposing to change it first. Implementations deviating without talking first = disharmony.

To be fair, IIRC we implemented that before there was much of a spec covering details like that. So we picked what we thought makes most sense (and is most conservative).

As a point of process, it would be great if when implementors found themselves in this situations that they write up a wiki stawman for the semantics they decided to implement. That way we can all review them and discuss whether they are the semantics we want to adopt for the specification. Even better, do that before deciding to implement.

# Brendan Eich (13 years ago)

Allen Wirfs-Brock wrote:

As a point of process, it would be great if when implementors found themselves in this situations that they write up a wiki stawman for the semantics they decided to implement. That way we can all review them and discuss whether they are the semantics we want to adopt for the specification. Even better, do that before deciding to implement.

Even better still, let's talk about editing the existing proposal so we all agree. If possible, which seems quite possible in this case (we argue vigorously on es-discuss but try to reach stable conclusions for publicly understood reasons).