Toplevel 'let' binding can be left permanently uninitialized after an error
Can you clarify "write"? Does this mean assignment? Why would assignment throw?
TDZ
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
}
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.
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...
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.
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.
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.
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.
FYI, you can also see this behavior in Node.js (v0.11.14)
node
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!
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:
- Make
let
mean the same thing at top-level that it means locally. - Enable recursive bindings across multiple scripts.
- Avoid the spec/design work of reifying new binding forms' reification in the global object.
- 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. :)
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.
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?
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:
- Make
let
mean the same thing at top-level that it means locally.- Enable recursive bindings across multiple scripts.
- Avoid the spec/design work of reifying new binding forms' reification in the global object.
- 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.
You can still do {x}
.
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
Re "x is irreparably hosed in your REPL"; you can still use it in subscope, eg <{let x=1;console.log(1)}>.
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
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).
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 undefined
for 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.
Re "..in subscope" and "..relevant"; As repl's can be fixed by that---all snippets treated as curlybraced.
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
Mmeta:
Pprevious edit code:
This' a reply to <CALuakQg6U55UC9XaNYZWf2Fu6Phw+jaHWBq_nw1nTPfX=jT0Aw at mail.gmail.com>.
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>.
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>.
Mmeta:
TwicePrevious edit code:
This's subject is <Re: Toplevel 'let' binding can be left permanently uninitialized after an error>.
Mmeta:
Pprevious edit code:
This's subject is <Re: Toplevel 'let' binding can be left permanently uninitialized after an error>.
TwiceMeta:
FourfoldPrevious edit code:
This's subject is <Re: Toplevel 'let' binding can be left permanently uninitialized after an error>.
Mmeta:
Pparent edit code:
This's subject is <Re: Toplevel 'let' binding can be left permanently uninitialized after an error>.
TwiceMeta:
Annul Previous till ThricePrevious.
SpiderMonkey hacker Jeff Walden noticed this. Consider a web page that loads and runs this script:
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.