10.01.11

New ES5 strict mode support: new vars created by strict mode eval code are local to that code only

tl;dr

Ideally you shouldn’t use eval, because it inhibits many optimizations and makes code run slower. new Function(argname1, argname2, ..., code) doesn’t inhibit optimizations, so it’s a better way to dynamically generate code. (This may require passing in arguments instead of using names for local variables. That’s the price you pay to play nice with compilers that could optimize but for eval.)

Nevertheless, it’s still possible to use eval in ES5. eval of normal code behaves as it always has. But eval of strict mode code behaves differently: any variables created by the code being evaluated affect only that code, not the enclosing scope. (The enclosing scope’s variables are still visible in the eval code if they aren’t shadowed.) Firefox now correctly binds variables created by strict mode eval code, completing the last major component of strict mode (though bugs remain). These programs demonstrate the idea:

var x = 2, y = 3;
print(eval("var x = 9; x"));               // prints 9
print(x);                                  // prints 9
print(eval("'use strict'; var x = 5; x")); // prints 5
print(eval("'use strict'; var x = 7; x")); // prints 7
print(eval("'use strict'; y"));            // prints 3
print(x);                                  // prints 9
"use strict";
var x = 2, y = 3;
// NB: Strictness propagates into eval code evaluated by a
//     direct call to eval — a call occurring through an
//     expression of the form eval(...).
print(eval("var x = 5; x")); // prints 5
print(eval("var x = 7; x")); // prints 7
print(eval("y"));            // prints 3
print(x);                    // prints 2

This partially defangs eval. But even strict mode eval inhibits optimizations, so you are still better off avoiding it.

eval is a double-edged sword

eval is one of the most powerful parts of JavaScript: it enables runtime code generation. You can compile code to perform specific operations, avoiding unnecessary general-purpose overhead — a powerful concept. (But you’d be better off using new Function(argname1, argname2, ..., code), which doesn’t inhibit optimizations and still enables code generation, at loss of the ability to capture the local scope. Code using eval may see considerable speedups: for example, roc’s CPU emulator sped up ~14% switching from eval to Function. Less beefy code won’t see that magnitude of win, yet why give up performance when you have a ready alternative?)

Yet at the same time, eval is too powerful. As inline assembly is to C or C++ (at least without the information gcc‘s asm syntax requires), so is eval to JavaScript. In both instances a powerful construct inhibits many optimizations. Even if you don’t care about optimizations or performance, eval‘s ability to introduce and delete bindings makes code that uses it much harder to reason about.

eval arbitrarily mutates variables

At its simplest, eval can change the value of any variable:

function test(code)
{
  var v = 1;
  eval(code);
  return v;
}
assert(test("v = 2") === 2);

Thus you can’t reorder or constant-fold assignments past eval: eval forces everything to be “written to disk” so that the eval code can observe it, and likewise it forces everything to be read back “from disk” when needed next. Without costly analysis you can’t store v in a register across the call.

eval can insert bindings after compilation

eval‘s ability to add bindings is worse. This can make it impossible to say what a name refers to until runtime:

var v;
function test(code)
{
  eval(code);
  return v;
}

Does the v in the return statement mean the global variable? You can’t know without knowing the code eval will compile and run. If that code is "var v = 17;" it refers to a new variable. If that code is "/* psych! */" it refers to the global variable. eval in a function will deoptimize any name in that function which refers to a variable in an enclosing scope. (And don’t forget that the name test itself is in an enclosing scope: if the function returned test instead of v, you couldn’t say whether that test referred to the enclosing function or to a new variable without knowing code.)

eval can remove bindings added after compilation

You can also delete bindings introduced by eval (but not any other variables):

var v = 42;
function test(code)
{
  eval(code);
  function f(code2)
  {
    eval(code2);
    return function g(code3) { eval(code3); return v; };
  }
  return f;
}
var f = test("var v = 17;");
var g = f("var v = 8675309;");
assert(g("/* nada */") === 8675309);
assert(g("var v = 5;") === 5);
assert(g("delete v") === 17);
assert(g("delete v") === 42);
assert(g("delete v") === 42); // can't delete non-eval var (thankfully)

So not only can you not be sure what binding a name refers to given eval, you can’t even be sure what binding it refers to over time! (Throw generators into the game and you also have to account for a scope without a binding containing that binding even after a function has “returned”.)

eval can affect enclosing scopes

Worst, none of these complications (and I’ve listed only a few) are limited to purely local variables. eval can affect any variable it can see at runtime, whether in its immediate function or in any enclosing function or globally. eval is the fruit of the poisonous tree: it taints not just the scope containing it, but all scopes containing it.

Save us, ES5!

ES5 brings some relief from this madness: strict mode eval can no longer introduce or delete bindings. (Normal eval remains unchanged.) Deleting a binding is impossible in strict mode because delete name is a syntax error. And instead of introducing bindings in the calling scope, eval of strict mode code introduces bindings for that code only:

var x = 2, y = 3;
print(eval("var x = 9; x"));               // prints 9
print(x);                                  // prints 9
print(eval("'use strict'; var x = 5; x")); // prints 5
print(eval("'use strict'; var x = 7; x")); // prints 7
print(eval("'use strict'; y"));            // prints 3
print(x);                                  // prints 9

This works best if you have strict mode all the way down, so that eval can never affect the bindings of any scope (and so you don’t need "use strict" at the start of every eval code):

"use strict";
var x = 2, y = 3;
// NB: Strictness propagates into eval code evaluated by a
//     direct call to eval — a call occurring through an
//     expression of the form eval(...).
print(eval("var x = 5; x")); // prints 5
print(eval("var x = 7; x")); // prints 7
print(eval("y"));            // prints 3
print(x);                    // prints 2

Names in strict mode code can thus be associated without having to worry about eval in strict mode code altering bindings, preserving additional optimization opportunities.

Firefox now correctly implements strict mode eval code binding semantics (modulo bugs, of course).

So if I write strict mode code, should I use eval?

eval‘s worst aspects are gone in strict mode, but using it still isn’t a good idea. It can still change variables in ways the JavaScript compiler can’t detect, so strict mode eval still generally forces every variable to be saved before it occurs and to be reloaded when needed. This deoptimization is unavoidable if runtime code generation can affect dynamically-determined local variables. It’s still better to use Function than to use eval.

Also, as a temporary SpiderMonkey-specific concern, we don’t perform many of the binding optimizations strict mode eval enables. Binding semantics might (I haven’t tested, and it’s entirely possible the extra work is unnoticeable in practice) slow down strict eval compared to normal eval. Strict mode eval performance in SpiderMonkey won’t be much better than that of regular eval, and it might be slightly worse. We’ll fix this over time, but for now don’t expect strict mode eval to improve performance. (If you really need performance, don’t use eval.)

Conclusion

eval is powerful — arguably too powerful. ES5’s strict mode blunts eval‘s sharpest corners to simplify it and permit typical optimizations in code using it. But while strict mode eval is better than regular eval, Function is still the best way to generate code at runtime. If you must use eval, consider using strict mode eval for a simpler binding model and eventual performance benefits.

You can experiment with a version of Firefox with these changes by downloading a nightly build. (Don’t forget to use the profile manager if you want to keep the settings you use with your primary Firefox installation pristine.)

9 Comments »

  1. (But you’d be better off using new Function(argname1, argname2, …, code)

    Well, except if you want to debug your code! I was able to hack around to fix eval(), but “new Function()” means no-debug-for-you!

    jjb

    Comment by johnjbarton — 10.01.11 @ 22:22

  2. I didn’t know that! That’s unfortunate. But I bet we can fix it, and we should fix it. Are you doing an awful hack of looking at script->getAtom(0) (or whatever the incantation is, you’ll know what I mean if that’s your approach) to make eval work? We should save the function code string somewhere for functions created by Function if that’s the sticking point — even atom 0 in the script, perhaps, for consistency with eval.

    Comment by Jeff — 11.01.11 @ 00:21

  3. …and you shouldn’t be jumping through hoops to fix eval(). It should be possible to debug in eval code without needing to resort to hacks to do it, assuming you meant “hack” in a moderately pejorative sense. File bugs, please.

    Comment by Jeff — 11.01.11 @ 00:22

  4. […] This post was mentioned on Twitter by Planet Mozilla, azu. azu said: ES5のstrict modeでevalで変数作ってもグローバルにならないんだ "Where's Walden? » New ES5 strict mode support: new vars created by strict mo…" http://goo.gl/WLf3B […]

    Pingback by Tweets that mention Where's Walden? » New ES5 strict mode support: new vars created by strict mode eval code are local to that code only -- Topsy.com — 11.01.11 @ 02:47

  5. […] Times by Ditching Web Workers – a case where not using web workers proved more performant ES5 strict mode support: new vars created by strict mode veal code are local to that code only ES5: Strict Mode (Rob Sayre’s Mozilla Blog) – Firefox 4 now supports strict mode more […]

    Pingback by JavaScript Magazine Blog for JSMag » Blog Archive » News roundup: Chrome ditches H.264, Mobile Perf bookmarklet, Reddit’s list of JavaScript game engines — 14.01.11 @ 12:00

  6. […] eval of strict mode code does not introduce new variables into the surrounding code. In normal code eval("var x;") introduces a variable x into the surrounding function or the global […]

    Pingback by ECMAScript 5 strict mode in Firefox 4 ✩ Mozilla Hacks – the Web developer blog — 25.01.11 @ 07:40

  7. […] drugie, wykonanie poprzez eval kodu trybu ścisłego nie wprowadza nowych zmiennych do otaczającego kodu. W tradycyjnym kodzie eval("var x;") wstawia zmienną x do otaczającej funkcji lub zasięgu […]

    Pingback by Mozilla Hacks: ECMAScript 5 Strict Mode – tryb ścisły w ECMAScripcie 5 « marcoos.techblog — 25.01.11 @ 13:31

  8. […] the arbitrary execution of code insecure, new variables cannot be created by eval() in ES5’s strict mode. The solution will not scale over […]

    Pingback by JSSpy » Dynamic invocation part 1: methods — 26.10.12 @ 11:46

  9. […] 另外,严格模式下的eval方法调用不会给当前作用域上添加新的变量。在非严格模式下,eval("var x;")将会创建新的变量x在当前的方法作用域上或者全局作用域上。这就意味着,通常情况下如果一个方法包含了eval语句,并且eval语句创建了新的和方法中某个参数或变量同名的变量,有可能会影响到他它们的值。但是在严格模式下,eval创建出来的变量只属于eval中代码被执行时的作用域,所以eval并不会影响其他局部本两或者参数: […]

    Pingback by [转载]JavaScript严格模式 – WEB前端开发- 专注前端开发,关注用户体验 — 15.06.13 @ 05:24

RSS feed for comments on this post. TrackBack URI

Leave a comment

HTML: You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>