Throwing errors on mutating immutable bindings

# Shu-yu Guo (10 years ago)

In the current draft, I see 2 different places where assigning to an immutable binding (const) throws an error:

  1. Dynamically throwing a TypeError in SetMutableBinding
  2. Statically throwing a SyntaxError in assignment expressions

1. throws only in strict mode code, while 2. throws regardless. 2. is also best effort; seems to be implementation-dependent what "can statically determine" entails.

Is the intention that assigning to consts silently nops if the implementation cannot determine the assignment to be to a const statically, in non-strict code, but implementations should make a best effort to report such cases eagerly, regardless of strictness? Seems kind of odd to me; perhaps I am misreading?

# Allen Wirfs-Brock (10 years ago)

On Sep 30, 2014, at 5:09 PM, Shu-yu Guo wrote:

In the current draft, I see 2 different places where assigning to an immutable binding (const) throws an error:

see bug ecmascript#3148 the "can" in that sentence isn't meant to be interpreted as "best effort" but instead more along the lines of "it is provable".

We need to refine that language, but the test is approximately that there are no with blocks inside the scope of the const declaration and surrounding the reference to the const. binding

1. throws only in strict mode code, while 2. throws regardless. 2. is also best effort; seems to be implementation-dependent what "can statically determine" entails.

Is the intention that assigning to consts silently nops if the implementation cannot determine the assignment to be to a const statically, in non-strict code, but implementations should make a best effort to report such cases eagerly, regardless of strictness? Seems kind of odd to me; perhaps I am misreading?

1. looks like a bug to me. I pretty sure it was never the intent for assignments to const binding to silently fail in non-strict code. The current semantics of SetMutableBinding is a carry over from ES5 where immutable bindings were only used (I have to double check this) for FunctionExpression function name bindings. The legacy of ES3 (hence non-strict ES5) was to did not throw on assignments to such function name bindings.

I'll probably have to do some extra special casing to preserve the ES3/5 semantics for assignment to function names and make the throw unconditional to other immutable bindings

# Erik Arvidsson (10 years ago)

The static error is problematic. I'm pretty sure that engines that do lazy parsing of functions is not going to report static errors before doing a full parse of the function.

I think we need to either enforce this or remove this restriction. Anything in between will lead to inconsistent behavior between engines.

# Oliver Hunt (10 years ago)

JSC does lazy parsing of nested functions and we have no problem reporting static errors, so i’m not sure what you believe is the problem here.

# Mark S. Miller (10 years ago)

On Wed, Oct 1, 2014 at 8:20 AM, Oliver Hunt <oliver at apple.com> wrote:

JSC does lazy parsing of nested functions and we have no problem reporting static errors, so i’m not sure what you believe is the problem here.

First, I agree with you. This shouldn't be a problem.

However, both JSC and v8 cause confusion when they use the term "lazy parsing". There are plenty of static errors that could not be reported with a truly lazy parser, i.e., one which actually postpones parsing. v8 for example uses a lightweight but accurate parser that is supposed to catch early errors early, but, afaik, doesn't construct an actual ast.

See

(Oliver, I note that the last bug above is still open, which continues to cause SES to use an expensive workaround when run on Safari.)

So the question is not "Can this early error be reported accurately without parsing?", since essentially none can, so no engines postpone parsing. The question is whether a particular new early error would require these lightweight early parsers to do something that they can't do while preserving their current efficiency.

Statically apparent assignment to const variables does not seem like a burden.

If there is an intervening "with" or sloppy direct eval, then there is not a statically apparent assignment to a const variable. Since this can only occur in sloppy code anyway, it seems more consistent with the rest of sloppy mode for this failed assignment to be silent, rather than dynamically throwing an error.

# Allen Wirfs-Brock (10 years ago)

On Oct 1, 2014, at 7:09 AM, Erik Arvidsson wrote:

The static error is problematic. I'm pretty sure that engines that do lazy parsing of functions is not going to report static errors before doing a full parse of the function.

I think we need to either enforce this or remove this restriction. Anything in between will lead to inconsistent behavior between engines.

There is nothing in between about the intent of the early error in the specification. It is mandatory, just like all other specified early errors. We do need to improve the specification language and ideally we should have an algorithmic specification of the error check. However, the latter is probably not going to be done for ES6 unless somebody has time to make a suitable contribution to the spec.

# Allen Wirfs-Brock (10 years ago)

On Oct 1, 2014, at 8:39 AM, Mark S. Miller wrote:

...

I was with you until you got to the following point

If there is an intervening "with" or sloppy direct eval, then there is not a statically apparent assignment to a const variable. Since this can only occur in sloppy code anyway, it seems more consistent with the rest of sloppy mode for this failed assignment to be silent, rather than dynamically throwing an error.

const is a new kind of declaration unlike any declaration form that previous existed in ES, so I don't think its handling introduces any legacy consistency issues. If somebody is using const, regard less of mode, they pretty clearly expect assignments to any const bindings to be illegal. And, I don't think any body wants new silent failure errors, even in sloppy mode. The most consistent thing is for runtime detected assignments to const bindings to always be noisy errors. Early where possible, at runtime in the rest of the cases.

# Till Schneidereit (10 years ago)

On Wed, Oct 1, 2014 at 5:39 PM, Mark S. Miller <erights at google.com> wrote:

However, both JSC and v8 cause confusion when they use the term "lazy parsing". There are plenty of static errors that could not be reported with a truly lazy parser, i.e., one which actually postpones parsing. v8 for example uses a lightweight but accurate parser that is supposed to catch early errors early, but, afaik, doesn't construct an actual ast.

SpiderMonkey in some places uses the term "lazy parsing", too, somewhat unfortunately. Mostly, though, we refer to bytecode-less functions as lazy, and call the initial parsing "syntax parsing". Which should be more indicative of what it actually does.

Also, I'm pretty sure that we correctly report these static errors even when they occur in syntax-only parsed functions (modulo bugs in the WIP implementation in bug 611388).

# Mark S. Miller (10 years ago)

Good point. If we can require all such assignments to be rejected statically, why is a runtime assignment to a const variable even possible? Can't we just assert that this cannot occur?

# Allen Wirfs-Brock (10 years ago)

On Oct 1, 2014, at 9:05 AM, Mark S. Miller wrote:

Good point. If we can require all such assignments to be rejected statically, why is a runtime assignment to a const variable even possible? Can't we just assert that this cannot occur?

The runtime cases I meant are the ones you mentioned. Sloppy with or eval dynamically shadowing a sloppy a [[Set]] reference to a const binding. Can't be a early error, should be a runtime error.

# Oliver Hunt (10 years ago)

On Oct 1, 2014, at 9:05 AM, Mark S. Miller <erights at google.com> wrote:

Good point. If we can require all such assignments to be rejected statically, why is a runtime assignment to a const variable even possible? Can't we just assert that this cannot occur?

You mean duplicate assignment? IIRC the problem is code that does

const x;
x=blah;

or

if (foo)
    const x = bar
else
    const x = wiffle

etc

Whether this is actually still something that exists - I recall in JSC many years ago we had to allow:

for (......) {
    const x = ...
    ...
}

Even though that is technically duplicate assignment.

But i guess that last case is less relevant as const is now always block scoped isn't it?

# Allen Wirfs-Brock (10 years ago)

On Oct 1, 2014, at 9:43 AM, Oliver Hunt wrote:

const x;

syntax error: no initializer

if (foo)
    const x = bar

syntax error: not a statement

else
    const x = wiffle

syntax error: not a statement

# Mark Miller (10 years ago)

On Wed, Oct 1, 2014 at 9:15 AM, Allen Wirfs-Brock <allen at wirfs-brock.com> wrote:

On Oct 1, 2014, at 9:05 AM, Mark S. Miller wrote:

Good point. If we can require all such assignments to be rejected statically, why is a runtime assignment to a const variable even possible? Can't we just assert that this cannot occur?

The runtime cases I meant are the ones you mentioned. Sloppy with or eval dynamically shadowing a sloppy a [[Set]] reference to a const binding. Can't be a early error, should be a runtime error.

Although it is a bit late to suggest it ;) ...

Couldn't we have "with" and sloppy direct eval ignore/skip const and let bindings? Then these errors could always be early.

I have no argument with the answer "too late", but still curious if there's another reason.

# Allen Wirfs-Brock (10 years ago)

Actually we've already agreed that eval puts const/let/class bindings into a separate lexical scope. But, an eval can still var shadow an outer const declaration.

# Jason Orendorff (10 years ago)

I think there is a way that the error could occur at runtime even in all-strict-mode code: when a new const is added at toplevel in a second script.

<script>
  "use strict";
  function f(value) { x = value; }
</script>
<script>
  "use strict";
  const x = 0;
  f(1);
</script>
# Brendan Eich (10 years ago)

Yup, and we've talked about this at TC39 meetings. We need a simple-enough static analysis, and runtime errors for the residue that escapes that analysis. I hope this is not controversial!

# Brendan Eich (10 years ago)

Jason Orendorff wrote:

I think there is a way that the error could occur at runtime even in all-strict-mode code: when a new const is added at toplevel in a second script.

Right.

Need a complete semantics: static and runtime. News at 11.

# Andreas Rossberg (10 years ago)

On 1 October 2014 16:09, Erik Arvidsson <erik.arvidsson at gmail.com> wrote:

The static error is problematic. I'm pretty sure that engines that do lazy parsing of functions is not going to report static errors before doing a full parse of the function.

Well, it is no harder than reporting reference errors for unbound variables in strict mode, which is already required for ES5. ...However, at least V8 does not report that correctly either, as soon as lazy parsing kicks in.

On 1 October 2014 17:39, Mark S. Miller <erights at google.com> wrote:

So the question is not "Can this early error be reported accurately without parsing?", since essentially none can, so no engines postpone parsing. The question is whether a particular new early error would require these lightweight early parsers to do something that they can't do while preserving their current efficiency.

Statically apparent assignment to const variables does not seem like a burden.

This analysis (as well as diagnosing unbound variables in strict mode) requires more than just parsing -- it requires full binding analysis. You have to construct all scope environments, analyse all declarations, and track all variable uses, even during "lazy" parsing. That has non-trivial complexity (although I don't know how costly it is).

# Andreas Rossberg (10 years ago)

On 1 October 2014 20:32, Jason Orendorff <jason.orendorff at gmail.com> wrote:

I think there is a way that the error could occur at runtime even in all-strict-mode code: when a new const is added at toplevel in a second script.

<script>
  "use strict";
  function f(value) { x = value; }
</script>

That's an early ReferenceError right there, AFAICT, regardless of what follows in a later script.

# Andreas Rossberg (10 years ago)

I take that back, I just realised it's not. I somehow thought that strict mode would also fix unbound assignments, but apparently that was wishful thinking on my part. Not happy. :(

# Mark Miller (10 years ago)

On Thu, Oct 2, 2014 at 12:22 AM, Andreas Rossberg <rossberg at google.com> wrote:

Well, it is no harder than reporting reference errors for unbound variables in strict mode, which is already required for ES5. ...However, at least V8 does not report that correctly either, as soon as lazy parsing kicks in.

Hi Andreas, can you show an example where v8 observably does the wrong thing here? Thanks.

# Andreas Rossberg (10 years ago)

No, I was confused, again. :) To set that straight:

  • ES5 of course does not require early errors for unbound variables.
  • So the good news is that V8 is currently correct.
  • The bad news is that requiring const assignments to be early errors would indeed be a big new hurdle for lazy compilation.

And I now remember that I have brought this up myself at some meeting. :)

So, yes, please let us remove the requirement to make const assignments an early error. A single, relatively unimportant diagnostics like that is not worth the considerable complication for VMs, especially given that all similar errors are not early errors either.

# Andreas Rossberg (10 years ago)

On 2 October 2014 16:06, Andreas Rossberg <rossberg at google.com> wrote:

So, yes, please let us remove the requirement to make const assignments an early error. A single, relatively unimportant diagnostics like that is not worth the considerable complication for VMs, especially given that all similar errors are not early errors either.

And to be sure, this is not just a problem that implementers can deal with somehow. It might very well have measurable impact on startup times for all larger JavaScript programs on all VMs. (AFAIK, nobody has tried to implement and measure it, though, so we cannot be sure either way.)

# Andreas Rossberg (10 years ago)

On 2 October 2014 17:17, Allen Wirfs-Brock <allen at wirfs-brock.com> wrote:

I believe that the module champions have always wanted static unresolvablse reference rejection to be part of the module semantics. But we've never had actual specification for that semantics.

Yes, I had always hoped for a "stricter mode" applied to modules. But I don't think that's something that's still possible at this (or a later) point. If it had been, then it would have made total sense for it to check against const assignments as well, along with a number of other things.

Personally, I think it would be fine for all of these sort of conditions to be specified as runtime errors and leave it to linters to find them early and JS implementations to optimize the runtime checks away.

Outside modules, with the language being as it is, adding one minor new static check definitely does not seem worth the trouble and the cost risk.

# David Herman (10 years ago)

On Oct 2, 2014, at 8:31 AM, Andreas Rossberg <rossberg at google.com> wrote:

Yes, I had always hoped for a "stricter mode" applied to modules. But I don't think that's something that's still possible at this (or a later) point. If it had been, then it would have made total sense for it to check against const assignments as well, along with a number of other things.

Yeah, I did wish for it, but we conceded a long time ago that it wasn't going to work out to have additional checks in modules. It's especially problematic because whether something is unbound or global is entirely dynamic (globals can be added and removed), scripts can be loaded at dynamically unpredictable times (so predicting the state of the global at the time code will be loaded is hard), in some modes HTML even throws element ids on the global and removes them again ...

Outside modules, with the language being as it is, adding one minor new static check definitely does not seem worth the trouble and the cost risk.

Agreed. The consolation prize is that modules now make it possible for linters to do free variable analysis and errors without any user-configuration at all. (By contrast, JSHint has historically required programmers to annotate the set of globals they intend to use so that they aren't reported as free variable errors.)

# Isiah Meadows (10 years ago)

Yes, I had always hoped for a "stricter mode" applied to modules. But I don't think that's something that's still possible at this (or a later) point. If it had been, then it would have made total sense for it to check against const assignments as well, along with a number of other things.

Personally, I think it would be fine for all of these sort of conditions to be specified as runtime errors and leave it to linters to find them early and JS implementations to optimize the runtime checks away.

More likely, they will be even more quickly optimized and potentially inlined as static constants.

Outside modules, with the language being as it is, adding one minor new static check definitely does not seem worth the trouble and the cost risk.

True, and I don't know of a decently fast ES3/5 parser. ES6 will be even more complicated, and thus, slower than an equivalence ES3/5 one

# Brendan Eich (10 years ago)

Isiah Meadows wrote:

True, and I don't know of a decently fast ES3/5 parser.

What is your use-case? For a parser in JS, is marijnhaverbeke.nl/blog/acorn.html too slow? For C++ hand-coded parser, what's your not-decently-fast benchmark basis code?

ES6 will be even more complicated, and thus, slower than an equivalence ES3/5 one

I don't think ES6 adds anything that changes asymptotic complexity. Recognizing keywords is still O(k), for example (and k is small and stays small in ES6 -- number of leading characters to distinguish, e.g.). More productions in a grammar does not make parsers for that grammar slower, apart from indirect effects in small memory systems.

What do you mean here?

# Isiah Meadows (10 years ago)

I mean that I wasn't thinking straight when I sent that. I'm incorrect in every detail of that email.

# Brendan Eich (10 years ago)

No worries -- I'm interested in parser benchmarks, both in-JS and C++-to-the-metal. Anyone else have any?

# Isiah Meadows (10 years ago)

If I can find a native parser (that only parses), I would run benchmarks. Shouldn't take that long to time a few rounds of parsing a large library like jQuery or React. I would be more than willing to accept pointers on where to find one.

I know one exists in Java (Closure Compiler), but I'm not sure about truly native ones.

# Till Schneidereit (10 years ago)

On Sun, Oct 5, 2014 at 7:24 AM, Isiah Meadows <impinball at gmail.com> wrote:

If I can find a native parser (that only parses), I would run benchmarks. Shouldn't take that long to time a few rounds of parsing a large library like jQuery or React. I would be more than willing to accept pointers on where to find one.

The SpiderMonkey shell lets you do both syntax parsing - the fast initial parse we do to detect static errors, and full parsing; both using the same parser Firefox uses. You can download the latest version here: ftp.mozilla.org/pub/mozilla.org/firefox/nightly/latest-trunk

Or, for non-dev build versions, check one directory up.

The shell has a parse function for full parsing and a parseSyntax for, well, syntax parsing. More information available using help().

# Isiah Meadows (10 years ago)

Thank you. I was looking for something I could benchmark in native code, but this works. It shouldn't be hard to test side by side. I'll get back with my results.

# Isiah Meadows (10 years ago)

Here's what I got from 4 consecutive runs (label prefixes self-explanatory):

(SM Native) Time to complete: 15584 microseconds.
(SM Shell)  Time to complete: 512053 microseconds.
(Node V8)   Time to complete: 238188 microseconds.
(SM Native) Time to complete: 17418 microseconds.
(SM Shell)  Time to complete: 556189 microseconds.
(Node V8)   Time to complete: 179156 microseconds.
(SM Native) Time to complete: 16119 microseconds.
(SM Shell)  Time to complete: 528309 microseconds.
(Node V8)   Time to complete: 157015 microseconds.
(SM Native) Time to complete: 16457 microseconds.
(SM Shell)  Time to complete: 470544 microseconds.
(Node V8)   Time to complete: 166123 microseconds.

Clearly, we still have a long way to go before beating a C++ parser. Pretty interesting to think about, though.

(Also, as a side note, should this be profiled for SpiderMonkey? V8 is averaging about 10x native, SpiderMonkey about 3-5x V8. My spidey senses sense a growing gap...;-))


Here's the source for each:

// sm-parser.js
if (typeof require !== 'undefined' && typeof process !== 'undefined') {
  throw new Error('Not for Node. Try with SpiderMonkey.');
}

load('./acorn.js');

function getMicroseconds(end, start) {
  return end * 1000 - start * 1000;
}

var jQuerySrc = read('./jquery-2.1.1.min.js');
var parsed, end;
var start = dateNow();
parsed = parse(jQuerySrc);
end = dateNow();
print('(SM Native) Time to complete: ' + getMicroseconds(end, start) +
' microseconds.');
// js-parser.js
if (typeof require !== 'undefined' && typeof process !== 'undefined') {
  // Boilerplate for Node.js
  var acorn = require('./acorn.js');
  var dateNow = process.hrtime;
  var read = require('fs').readFileSync;
  var print = console.log;
  var getMicroseconds = function (end, start) {
    // Microsecond precision to match SpiderMonkey
    end = end[0] * 1e9 + end[1];
    start = start[0] * 1e9 + start[1];
    return Math.floor((end - start) / 1000);
  };
  var prefix = '(Node V8)  ';
} else {
  load('./acorn.js');
  var getMicroseconds = function (end, start) {
    return end * 1000 - start * 1000;
  };
  var prefix = '(SM Shell) ';
}

var jsParse = acorn.parse;
var jQuerySrc = read('./jquery-2.1.1.min.js');
var parsed, end;
var start = dateNow();
parsed = jsParse(jQuerySrc);
end = dateNow();
print(prefix + ' Time to complete: ' + getMicroseconds(end, start) + '
microseconds.');
# Till Schneidereit (10 years ago)

On Mon, Oct 6, 2014 at 9:22 AM, Isiah Meadows <impinball at gmail.com> wrote:

Clearly, we still have a long way to go before beating a C++ parser. Pretty interesting to think about, though.

This comparison, while quite informative, isn't a full answer to the question at hand, for at least two reasons:

For startup speed, the syntax parsing mode is more important, because that is the one that gets run over the whole file before anything executes. The full parser only kicks in for functions that are actually executed. (Well, there are some features that force us on a slow, full-parsing path, but they don't apply here.) I'm pretty sure if you use syntaxParse instead of parse, that'll somewhat widen the gap. (In a quick test, syntaxParse is about 40% faster for the test file.)

On the other hand, the Acorn parser probably does a lot more work than the builtin one: I didn't look into it, but my guess is that it builds and returns an AST, so it has to create a substantial graph of JS objects in addition to parsing the source. I'm not sure how easy it'd be to hack Acorn to just do the parsing, but if doable, that'd probably close the gap considerably.

(Also, as a side note, should this be profiled for SpiderMonkey? V8 is averaging about 10x native, SpiderMonkey about 3-5x V8. My spidey senses sense a growing gap...;-))

By all our tests (and external ones that I'm aware of) we've all but closed the gap; check arewefastyet.com. This test case is quite interesting, though - I filed bugzilla.mozilla.org/show_bug.cgi?id=1078273 to investigate.

# Isiah Meadows (10 years ago)

On Oct 6, 2014 9:58 AM, "Till Schneidereit" <till at tillschneidereit.net>

wrote:

On Mon, Oct 6, 2014 at 9:22 AM, Isiah Meadows <impinball at gmail.com>

wrote:

Clearly, we still have a long way to go before beating a C++ parser. Pretty interesting to think about, though.

This comparison, while quite informative, isn't a full answer to the

question at hand, for at least two reasons:

For startup speed, the syntax parsing mode is more important, because

that is the one that gets run over the whole file before anything executes. The full parser only kicks in for functions that are actually executed. (Well, there are some features that force us on a slow, full-parsing path, but they don't apply here.) I'm pretty sure if you use syntaxParse instead of parse, that'll somewhat widen the gap. (In a quick test, syntaxParse is about 40% faster for the test file.)

That is effectively access to the tokenizer. Most dedicated parsers out there have such access.

On the other hand, the Acorn parser probably does a lot more work than

the builtin one: I didn't look into it, but my guess is that it builds and returns an AST, so it has to create a substantial graph of JS objects in addition to parsing the source. I'm not sure how easy it'd be to hack Acorn to just do the parsing, but if doable, that'd probably close the gap considerably.

They both return the same exact format. And Acorn does the least out of most of the JS parsers (which is also why it has the most ES6 support). If you simply want to check out the tokenizer, Acorn has a .tokenize() method.

(Also, as a side note, should this be profiled for SpiderMonkey? V8 is averaging about 10x native, SpiderMonkey about 3-5x V8. My spidey senses sense a growing gap...;-))

By all our tests (and external ones that I'm aware of) we've all but

closed the gap; check arewefastyet.com. This test case is quite interesting, though - I filed bugzilla.mozilla.org/show_bug.cgi?id=1078273 to investigate.

Yeah...part of why I mentioned it.