Toplevel 'let' binding can be left permanently uninitialized after an error

# Jason Orendorff (10 years ago)

SpiderMonkey hacker Jeff Walden noticed this. Consider a web page that loads and runs this script:

throw 0;
let x;

This leaves the binding 'x' uninitialized. There's no way to get rid of a lexical binding or to initialize it later. The binding is just permanently hosed; any attempt to read or write it will throw.

That by itself isn't necessarily a problem. I've never written a web page where I wanted to recover after a toplevel script threw an exception (or timed out). But I dunno, the impossibility of any kind of self-healing here gives me pause.

No action required on my account; I'm posting this because we were all surprised and it seems vaguely unaesthetic.

# Rick Waldron (10 years ago)

Can you clarify "write"? Does this mean assignment? Why would assignment throw?

# Allen Wirfs-Brock (10 years ago)

TDZ

# Jason Orendorff (10 years ago)

On Mon, Sep 29, 2014 at 4:14 PM, Rick Waldron <waldron.rick at gmail.com> wrote:

Can you clarify "write"? Does this mean assignment?

Yes.

Why would assignment throw?

Assigning to an uninitialized variable is an error in ES6. A let-binding is initialized when its declaration is evaluated. So this is OK:

let x;  // no Initializer, so it's initialized to undefined
console.log(x);  // logs undefined

but this is not:

init();
let answer;
function init() {
    answer = 42;  // throws ReferenceError
}
# Jason Orendorff (10 years ago)

I just realized this has an unfortunate implication for REPLs. Suppose you make this typo:

js> let x = Math.cso(a)    // oops, TypeError, should be Math.cos

Now x is irreparably hosed in your REPL. That seems bad.

I guess we can fix this by making the REPL bend the rules of the language. But this is rather hard to do for REPLs implemented in JS. Maybe the rules should just be a little more forgiving.

# Andreas Rossberg (10 years ago)

On 30 September 2014 12:52, Jason Orendorff <jason.orendorff at gmail.com> wrote:

I just realized this has an unfortunate implication for REPLs. Suppose you make this typo:

js> let x = Math.cso(a)    // oops, TypeError, should be Math.cos

Now x is irreparably hosed in your REPL. That seems bad.

No surprise. One of the reasons why I always favoured a nested scopes model for multiple scripts...

# Mark S. Miller (10 years ago)

Likewise. E is also a scripting repl language with dynamic types and static scopes. We tried a variety of different ways to handle the top level repl and nested scopes were best.

# Rick Waldron (10 years ago)

On Tue, Sep 30, 2014 at 6:38 AM, Jason Orendorff <jason.orendorff at gmail.com>

wrote:

On Mon, Sep 29, 2014 at 4:14 PM, Rick Waldron <waldron.rick at gmail.com> wrote:

Can you clarify "write"? Does this mean assignment?

Yes.

Why would assignment throw?

Assigning to an uninitialized variable is an error in ES6. A let-binding is initialized when its declaration is evaluated. So this is OK:

let x;  // no Initializer, so it's initialized to undefined
console.log(x);  // logs undefined

but this is not:

init();
let answer;
function init() {
    answer = 42;  // throws ReferenceError
}

My original response questions were poorly asked. I understand the TDZ semantics, but I couldn't reproduce anything meaningful from your original example, because I don't have the SpiderMonkey build that includes the let updates (presumably Nightly doesn't either, because none of these can be reproduced there). I'm trying to understand when/where/why/how the original example could happen and what the potential consequences are in terms of practical application.

# Allen Wirfs-Brock (10 years ago)

On Sep 30, 2014, at 10:00 AM, Rick Waldron wrote:

My original response questions were poorly asked. I understand the TDZ semantics, but I couldn't reproduce anything meaningful from your original example, because I don't have the SpiderMonkey build that includes the let updates (presumably Nightly doesn't either, because none of these can be reproduced there). I'm trying to understand when/where/why/how the original example could happen and what the potential consequences are in terms of practical application.

Also, note that IE11 apparently implements the ES6 let semantics so it may be useful to look at its experience in this regard.

# Rick Waldron (10 years ago)

On Tue, Sep 30, 2014 at 1:08 PM, Allen Wirfs-Brock <allen at wirfs-brock.com>

wrote:

On Sep 30, 2014, at 10:00 AM, Rick Waldron wrote:

My original response questions were poorly asked. I understand the TDZ semantics, but I couldn't reproduce anything meaningful from your original example, because I don't have the SpiderMonkey build that includes the let updates (presumably Nightly doesn't either, because none of these can be reproduced there). I'm trying to understand when/where/why/how the original example could happen and what the potential consequences are in terms of practical application.

Also, note that IE11 apparently implements the ES6 let semantics so it may be useful to look at its experience in this regard.

As a Mac user, I completely forgot about this. Using Browserstack.com, I was able to reproduce the original example's behavior by running a script and then attempting to access or assign to x from the console. The behavior I observe is exactly what I expect. I'm still unable to provide any further insight regarding Jason's original comments and practical implications of reported behavior.

"That by itself isn't necessarily a problem. I've never written a web page where I wanted to recover after a toplevel script threw an exception (or timed out)."

I agree with the first assertion for the same reason stated.

# Brian Genisio (10 years ago)

FYI, you can also see this behavior in Node.js (v0.11.14)

node

# Erik Arvidsson (10 years ago)

On Tue, Sep 30, 2014 at 2:08 PM, Brian Genisio <briangenisio at gmail.com>

wrote:

FYI, you can also see this behavior in Node.js (v0.11.14)

node --harmony --strict-mode

V8's support of let is far from spec compliant. Stuff under --harmony is incomplete, buggy and may have security holes. Do not use!

# David Herman (10 years ago)

On Sep 30, 2014, at 4:03 AM, Andreas Rossberg <rossberg at google.com> wrote:

On 30 September 2014 12:52, Jason Orendorff <jason.orendorff at gmail.com> wrote:

I just realized this has an unfortunate implication for REPLs. Suppose you make this typo:

js> let x = Math.cso(a) // oops, TypeError, should be Math.cos

Now x is irreparably hosed in your REPL. That seems bad.

No surprise. One of the reasons why I always favoured a nested scopes model for multiple scripts...

We've had this debate a million times, but for anyone who's unfamiliar with the arguments: nested scopes makes recursive bindings across scripts impossible and is also hard to understand with things like asynchronous script execution order (such as <script async>). And it's totally different from the existing mental model of global code.

But since we're rehashing, this is an example of why I've always thought new binding forms should just go in the global object just like vars and functions do. JS's global scope is extremely complex and subtle, and you can't undo that complexity by adding additional layers. And the metal model, while unfortunate, already occupies programmers' brainspace -- adding an extra layer of scope for some binding forms makes the mental model more complex.

We basically have the following set of constraints that various people have tossed into the mixing bowl. It seems no matter which way you mix it, at least one of the constraints falls back out of the bowl:

  1. Make let mean the same thing at top-level that it means locally.
  2. Enable recursive bindings across multiple scripts.
  3. Avoid the spec/design work of reifying new binding forms' reification in the global object.
  4. Preserve the mental model of the global object.

Note that, as so often happens, we have one dimension of consistency (scoping semantics of let in local vs. global contexts) in tension with another dimension of consistency (globals going in the global object vs. globals going in a new scope layer).

I'm still convinced we made the wrong call, i.e., we chose the wrong dimension of consistency. The global scope was always a mess and we aren't going to make it less of a mess no matter what we do. We favored a philosophical consistency over a historical consistency, with predictable results. But it is what it is. We've talked before about exposing a reflection of the extra global layer, maybe via the Reflect API but certainly through the Realm API. We may need to add controls to that to allow undoing these kinds of annoyances Jason brings up.

I'm usually less concerned about REPLs, since they can decide for themselves what kind of context they want to execute in -- or even invent new non-standard non-terminals, frankly -- although in this case it's not quite clear what a let declaration should do in the REPL. Maybe developer consoles should actually treat let the way IMO we should've done for scripts. :)

# Jason Orendorff (10 years ago)

On Tue, Sep 30, 2014 at 12:00 PM, Rick Waldron <waldron.rick at gmail.com> wrote:

My original response questions were poorly asked. I understand the TDZ semantics, but I couldn't reproduce anything meaningful from your original example, because I don't have the SpiderMonkey build that includes the let updates (presumably Nightly doesn't either, because none of these can be reproduced there).

SpiderMonkey doesn't conform to the draft spec yet. We implemented TDZ in functions, but not at toplevel. 'const' isn't lexical yet. There's a lot more work to do.

I'm trying to understand when/where/why/how the original example could happen and what the potential consequences are in terms of practical application.

Purely hypothetical for us, for now. It came up on IRC. Some engineers were discussing the breaking changes we're going to have to make.

# Brendan Eich (10 years ago)

David Herman wrote:

I'm usually less concerned about REPLs, since they can decide for themselves what kind of context they want to execute in -- or even invent new non-standard non-terminals, frankly -- although in this case it's not quite clear what a let declarationshould do in the REPL. Maybe developer consoles should actually treat let the way IMO we should've done for scripts.:)

Agree that nesting is a bad idea for <script> top level scopes.

But a REPL might be the one place nesting works to avoid the issue Jason raised. Any gotchas there?

# Andreas Rossberg (10 years ago)

On 30 September 2014 22:38, David Herman <dherman at mozilla.com> wrote:

On Sep 30, 2014, at 4:03 AM, Andreas Rossberg <rossberg at google.com> wrote:

On 30 September 2014 12:52, Jason Orendorff <jason.orendorff at gmail.com> wrote:

I just realized this has an unfortunate implication for REPLs. Suppose you make this typo:

js> let x = Math.cso(a) // oops, TypeError, should be Math.cos

Now x is irreparably hosed in your REPL. That seems bad.

No surprise. One of the reasons why I always favoured a nested scopes model for multiple scripts...

We've had this debate a million times, but for anyone who's unfamiliar with the arguments: nested scopes makes recursive bindings across scripts impossible and is also hard to understand with things like asynchronous script execution order (such as <script async>). And it's totally different from the existing mental model of global code.

Well, to reiterate:

  • Cross-script recursion via declarations does not work in strict mode anyway, so is not a solution moving forward.

  • Nothing is easier about async scripts in a mutable scope model -- you just trade unpredictable shadowing for unpredictable overwrites. It's hard to argue that the latter is better by any reasonable metric.

But since we're rehashing, this is an example of why I've always thought new binding forms should just go in the global object just like vars and functions do. JS's global scope is extremely complex and subtle, and you can't undo that complexity by adding additional layers. And the metal model, while unfortunate, already occupies programmers' brainspace -- adding an extra layer of scope for some binding forms makes the mental model more complex.

For the record, nobody has ever come up with an acceptable explanation for realising TDZ directly on the global object without breaking (or grossly extending) the object model. And TDZ is required, at least for const bindings, to be self-consistent.

(I suggested an explanation at one point: installing lexical bindings as getters/setters on the global object, mirroring what happens with module instance objects. But that was considered too complicated.)

We basically have the following set of constraints that various people have tossed into the mixing bowl. It seems no matter which way you mix it, at least one of the constraints falls back out of the bowl:

  1. Make let mean the same thing at top-level that it means locally.
  2. Enable recursive bindings across multiple scripts.
  3. Avoid the spec/design work of reifying new binding forms' reification in the global object.
  4. Preserve the mental model of the global object.

Note that, as so often happens, we have one dimension of consistency (scoping semantics of let in local vs. global contexts) in tension with another dimension of consistency (globals going in the global object vs. globals going in a new scope layer).

You forgot that other dimension of consistency that I mention above, which was the real road block. In the light of that, there was a clear lesser evil, which is what we chose.

# Joseph (6 years ago)

You can still do {x}.

# T.J. Crowder (6 years ago)

On Tue, Nov 28, 2017 at 5:05 PM, Joseph <pacerier at gmail.com> wrote:

You can still do {x}.

Can you expand on that? It doesn't seem to me you can. I mean, if even x = 42; won't work (jsfiddle.net/tw3ohac6), I fail to see how anything else using x would work, including {x} ( jsfiddle.net/tw3ohac6/1, jsfiddle.net/tw3ohac6/2). x is permanently in the TDZ as far as I can tell.

-- T.J. Crowder

# Joseph (6 years ago)

Re "x is irreparably hosed in your REPL"; you can still use it in subscope, eg <{let x=1;console.log(1)}>.

# T.J. Crowder (6 years ago)

On Tue, Nov 28, 2017 at 7:31 PM, Joseph <pacerier at gmail.com> wrote:

Re "x is irreparably hosed in your REPL"; you can still use it in subscope, eg <{let x=1;console.log(1)}>.

Well yes, of course you can. You can also use it in nested functions. Or even whole other scripts, which is nearly as relevant.

-- T.J. Crowder

# Isiah Meadows (6 years ago)

And this is why I use var instead of let in REPLs. They're doing what they're supposed to do; it's just unintuitive.

As a secondary proposal, I feel let/const in scripts should be allowed to shadow existing globals at the top level provided they are not declared in the same script. It'd solve the globals issue as well as not require the parser to make calls to the runtime environment. Of course, this means engines can't assume global const is immutable, but they already do similar global checks, anyways (like if the variable was not defined in that script).

# Steve Fink (6 years ago)

The spidermonkey REPL shell has a special cut-out for this:

js> throw 0; let x;

uncaught exception: 0 (Unable to print stack trace) Warning: According to the standard, after the above exception, Warning: the global bindings should be permanently uninitialized. Warning: We have non-standard-ly initialized them to undefinedfor you. Warning: This nicety only happens in the JS shell.

It looks like the Firefox console does something similar, just silently. The Chrome console and Node REPLs wedge you permanently, from my brief testing. I don't have anything else within easy reach to test on.

Separately, I ran into it with a JS debugger REPL that also runs under the spidermonkey shell -- I have a 'run' command that reruns the toplevel script, which fails if you have any toplevel let/const. And the above cutout doesn't help; there is no error.

The bindings are created and exist, they're just set to undefined. So if you repeat the above line, you'll get

typein:3:1 SyntaxError: redeclaration of let x Stack:   @typein:3:1

These days, if I have a script that I might want to debug with my hacky debugger REPL, I'm careful to use only 'var' at the toplevel.

All REPLs and REPL-like things run into this. Perhaps it would be useful to agree on a common behavior? Or at least share coping strategies.

# pacerier at gmail.com (6 years ago)

Re "..in subscope" and "..relevant"; As repl's can be fixed by that---all snippets treated as curlybraced.

# Jason Orendorff (6 years ago)

On Wed, Nov 29, 2017 at 4:13 AM, pacerier at gmail.com <pacerier at gmail.com>

wrote:

Re "..in subscope" and "..relevant"; As repl's can be fixed by that---all snippets treated as curlybraced.

Hi everyone.

For those of you who weren't on this list three years ago, here's the rest of the thread: esdiscuss.org/topic/toplevel-let-binding-can-be-left-permanently- uninitialized-after-an-error

Ever-deeper-nested-blocks is one possible behavior for a repl, and it does solve this problem by allowing you to shadow your broken let x with a fresh one, after a typo. It has some other drawbacks, though; see Dave Herman's post here: esdiscuss.org/topic/toplevel-let-binding-can-be-left-permanently- uninitialized-after-an-error#content

# pacerier at gmail.com (6 years ago)

Mmeta:

Pprevious edit code:

This' a reply to <CALuakQg6U55UC9XaNYZWf2Fu6Phw+jaHWBq_nw1nTPfX=jT0Aw at mail.gmail.com>.

# pacerier at gmail.com (6 years ago)

Mmeta:

<CANEHTz-db0S_JptCa-0HEeQrTk==SnZYKY9DVYvNXaMF_tri3A at mail.gmail.com> edit code:

This' a reply to <CAPh8+ZpbbnpDQ++o3OuTYBrsF2sz=Mkig4pZyBeUkQbd_c4drQ at mail.gmail.com>.

# pacerier at gmail.com (6 years ago)

Mmeta:

<CANEHTz-db0S_JptCa-0HEeQrTk==SnZYKY9DVYvNXaMF_tri3A at mail.gmail.com> edit code:

This's subject is <Re: Toplevel 'let' binding can be left permanently uninitialized after an error>.

# pacerier at gmail.com (6 years ago)

Mmeta:

TwicePrevious edit code:

This's subject is <Re: Toplevel 'let' binding can be left permanently uninitialized after an error>.

# pacerier at gmail.com (6 years ago)

Mmeta:

Pprevious edit code:

This's subject is <Re: Toplevel 'let' binding can be left permanently uninitialized after an error>.

# pacerier at gmail.com (6 years ago)

TwiceMeta:

FourfoldPrevious edit code:

This's subject is <Re: Toplevel 'let' binding can be left permanently uninitialized after an error>.

# pacerier at gmail.com (6 years ago)

Mmeta:

Pparent edit code:

This's subject is <Re: Toplevel 'let' binding can be left permanently uninitialized after an error>.

# pacerier at gmail.com (6 years ago)

TwiceMeta:

Annul Previous till ThricePrevious.