Bug 1186409 - Add the ability to use NSVOs on the scope chain in place of globals. draft
authorAndrew McCreight <continuation@gmail.com>
Tue, 18 Jul 2017 14:45:46 -0700
changeset 656076 3f1d6eeb21ee24db85ef334a1ca988315a879fad
parent 656075 2f7ba785adf7fde13c2a43de1c1109fadb631754
child 656077 75c7d7223099d8ee45ff76f2887f25a1cfafef71
child 656107 e3dc817a8ff578646297881225d35b787eb2c40a
push id77055
push userbmo:continuation@gmail.com
push dateWed, 30 Aug 2017 18:10:37 +0000
bugs1186409
milestone57.0a1
Bug 1186409 - Add the ability to use NSVOs on the scope chain in place of globals. This patch contains the JS engine changes required to support sharing a single global between multiple JSMs, in order to reduce memory usage. Each JSM gets its own NonSyntacticVariablesObject (NSVO), which is used for top level variable bindings and as the value of |this| within the JSM. Note that this will not work unless JSMs are all run in strict mode, due to the way |this| works inside a function. Existing code maps an NSVO to a JS object in nonSyntacticLexicalEnvironments_, but for JSMs they are mapped to the NSVO itself, so things can be stored directly on it. To aid the calculation of the value of |this| with a non syntactic scope, this patch adds a bit to the NSVO object to indicate if it is to be used as |this| or not. There can only be a single NSVO on the environment chain, and it must be the first thing after the global. MozReview-Commit-ID: EOh3nksaqDa
js/src/jsfriendapi.cpp
js/src/jsfriendapi.h
js/src/jsobj.cpp
js/src/shell/js.cpp
js/src/tests/js1_8_5/extensions/non_syntactic.js
js/src/vm/EnvironmentObject.cpp
js/src/vm/EnvironmentObject.h
--- a/js/src/jsfriendapi.cpp
+++ b/js/src/jsfriendapi.cpp
@@ -411,16 +411,43 @@ js::IsObjectInContextCompartment(JSObjec
 }
 
 JS_FRIEND_API(bool)
 js::RunningWithTrustedPrincipals(JSContext* cx)
 {
     return cx->runningWithTrustedPrincipals();
 }
 
+JS_FRIEND_API(JSObject*)
+js::CreateNonSyntacticVariablesObject(JSContext* cx)
+{
+    return NonSyntacticVariablesObject::create(cx, true);
+}
+
+JS_FRIEND_API(bool)
+js::IsNonSyntacticVariablesObject(JSContext* cx, JS::HandleObject obj)
+{
+    assertSameCompartment(cx, obj);
+    return obj->is<NonSyntacticVariablesObject>();
+}
+
+JS_FRIEND_API(JSObject*)
+js::GetVariablesObjectOfScriptedCaller(JSContext* cx)
+{
+    ScriptFrameIter iter(cx);
+
+    if (iter.done())
+        return nullptr;
+
+    RootedObject env(cx, iter.environmentChain(cx));
+    while (env && !env->is<NonSyntacticVariablesObject>())
+        env = env->enclosingEnvironment();
+    return env;
+}
+
 JS_FRIEND_API(JSFunction*)
 js::DefineFunctionWithReserved(JSContext* cx, JSObject* objArg, const char* name, JSNative call,
                                unsigned nargs, unsigned attrs)
 {
     RootedObject obj(cx, objArg);
     MOZ_ASSERT(!cx->runtime()->isAtomsCompartment(cx->compartment()));
     CHECK_REQUEST(cx);
     assertSameCompartment(cx, obj);
--- a/js/src/jsfriendapi.h
+++ b/js/src/jsfriendapi.h
@@ -702,16 +702,25 @@ JS_FRIEND_API(void)
 AssertSameCompartment(JSObject* objA, JSObject* objB);
 #else
 inline void AssertSameCompartment(JSObject* objA, JSObject* objB) {}
 #endif
 
 JS_FRIEND_API(void)
 NotifyAnimationActivity(JSObject* obj);
 
+JS_FRIEND_API(JSObject*)
+CreateNonSyntacticVariablesObject(JSContext* cx);
+
+JS_FRIEND_API(bool)
+IsNonSyntacticVariablesObject(JSContext* cx, JS::HandleObject obj);
+
+JS_FRIEND_API(JSObject*)
+GetVariablesObjectOfScriptedCaller(JSContext* cx);
+
 JS_FRIEND_API(JSFunction*)
 DefineFunctionWithReserved(JSContext* cx, JSObject* obj, const char* name, JSNative call,
                            unsigned nargs, unsigned attrs);
 
 JS_FRIEND_API(JSFunction*)
 NewFunctionWithReserved(JSContext* cx, JSNative call, unsigned nargs, unsigned flags,
                         const char* name);
 
--- a/js/src/jsobj.cpp
+++ b/js/src/jsobj.cpp
@@ -3266,18 +3266,21 @@ js::GetThisValue(JSObject* obj)
     }
 
     if (obj->is<ModuleEnvironmentObject>())
         return UndefinedValue();
 
     if (obj->is<WithEnvironmentObject>())
         return ObjectValue(*obj->as<WithEnvironmentObject>().withThis());
 
-    if (obj->is<NonSyntacticVariablesObject>())
-        return GetThisValue(obj->enclosingEnvironment());
+    if (obj->is<NonSyntacticVariablesObject>()) {
+        if (!obj->as<NonSyntacticVariablesObject>().isThis()) {
+            return GetThisValue(obj->enclosingEnvironment());
+        }
+    }
 
     return ObjectValue(*obj);
 }
 
 class GetObjectSlotNameFunctor : public JS::CallbackTracer::ContextFunctor
 {
     JSObject* obj;
 
--- a/js/src/shell/js.cpp
+++ b/js/src/shell/js.cpp
@@ -1603,16 +1603,30 @@ Evaluate(JSContext* cx, unsigned argc, V
                 }
             } else {
                 JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_UNEXPECTED_TYPE,
                                           "\"envChainObject\" passed to evaluate()", "not an object");
                 return false;
             }
         }
 
+        if (!JS_GetProperty(cx, opts, "nsvoThis", &v))
+            return false;
+        if (!v.isUndefined() && ToBoolean(v)) {
+            RootedObject nsvo(cx, js::CreateNonSyntacticVariablesObject(cx));
+            if (!nsvo) {
+                JS_ReportOutOfMemory(cx);
+                return false;
+            }
+            if (!envChain.append(nsvo)) {
+                JS_ReportOutOfMemory(cx);
+                return false;
+            }
+        }
+
         // We cannot load or save the bytecode if we have no object where the
         // bytecode cache is stored.
         if (loadBytecode || saveBytecode || saveIncrementalBytecode) {
             if (!cacheEntry) {
                 JS_ReportErrorNumberASCII(cx, my_GetErrorMessage, nullptr, JSSMSG_INVALID_ARGS,
                                           "evaluate");
                 return false;
             }
@@ -6242,16 +6256,18 @@ static const JSFunctionSpecWithHelp shel
 "         the bytecode would be encoded and saved into the cache entry after\n"
 "         the script execution.\n"
 "      assertEqBytecode: if true, and if both loadBytecode and saveBytecode are \n"
 "         true, then the loaded bytecode and the encoded bytecode are compared.\n"
 "         and an assertion is raised if they differ.\n"
 "      envChainObject: object to put on the scope chain, with its fields added\n"
 "         as var bindings, akin to how elements are added to the environment in\n"
 "         event handlers in Gecko.\n"
+"      nsvoThis: Use a fresh non-syntactic variables object for top level\n"
+"         lexical bindings, as well as the global 'this'.\n"
 ),
 
     JS_FN_HELP("run", Run, 1, 0,
 "run('foo.js')",
 "  Run the file named by the first argument, returning the number of\n"
 "  of milliseconds spent compiling and executing it."),
 
     JS_FN_HELP("readline", ReadLine, 0, 0,
--- a/js/src/tests/js1_8_5/extensions/non_syntactic.js
+++ b/js/src/tests/js1_8_5/extensions/non_syntactic.js
@@ -28,9 +28,37 @@ evaluate("assertEq(someVar, 2);", evalOp
 evaluate("assertEq(this.someVar, 2);", evalOpt);
 evaluate("assertEq(this, alsoSomeObject);", evalOpt);
 
 // With an object on the scope, inside a function.
 evaluate("(function() { assertEq(someVar, 2);})()", evalOpt);
 evaluate("(function() { assertEq(this !== alsoSomeObject, true);})()", evalOpt);
 evaluate("(function() { assertEq(this.someVar, 1);})()", evalOpt);
 
+// Setting a variable on an NSVO on the scope chain should get the right value, and not affect the
+// global.
+var x = 7;
+let y = 8;
+const z = 9;
+let nsvoOpt = { nsvoThis: true };
+evaluate("var x = 33; assertEq(x, 33);", nsvoOpt);
+assertEq(x, 7);
+evaluate("let y = 33; assertEq(y, 33);", nsvoOpt);
+assertEq(y, 8);
+evaluate("const z = 33; assertEq(z, 33);", nsvoOpt);
+assertEq(z, 9);
+evaluate("var x = 33; (function() { assertEq(x, 33);})()", nsvoOpt);
+assertEq(x, 7);
+evaluate("let y = 33; (function() { assertEq(y, 33);})()", nsvoOpt);
+assertEq(y, 8);
+evaluate("const z = 33; (function() { assertEq(z, 33);})()", nsvoOpt);
+assertEq(z, 9);
+
+// Basic check that |this| evaluates correctly with an NSVO.
+evaluate("(function() { assertEq(this.someVar, 1); })()", { nsvoThis: true, envChainObject: someObject });
+
+// Code with an NSVO on the scope chain should still be able to see globals.
+evaluate("assertEq(x, 7)", nsvoOpt);
+evaluate("assertEq(y, 8)", nsvoOpt);
+evaluate("assertEq(z, 9)", nsvoOpt);
+
+
 reportCompare(true, true);
--- a/js/src/vm/EnvironmentObject.cpp
+++ b/js/src/vm/EnvironmentObject.cpp
@@ -868,32 +868,40 @@ const Class WithEnvironmentObject::class
     JSCLASS_IS_ANONYMOUS,
     JS_NULL_CLASS_OPS,
     JS_NULL_CLASS_SPEC,
     JS_NULL_CLASS_EXT,
     &WithEnvironmentObjectOps
 };
 
 /* static */ NonSyntacticVariablesObject*
-NonSyntacticVariablesObject::create(JSContext* cx)
+NonSyntacticVariablesObject::create(JSContext* cx, bool isThis)
 {
     Rooted<NonSyntacticVariablesObject*> obj(cx,
         NewObjectWithNullTaggedProto<NonSyntacticVariablesObject>(cx, TenuredObject,
                                                                   BaseShape::DELEGATE));
     if (!obj)
         return nullptr;
 
     MOZ_ASSERT(obj->isUnqualifiedVarObj());
     if (!JSObject::setQualifiedVarObj(cx, obj))
         return nullptr;
 
+    obj->initReservedSlot(IS_THIS_SLOT, Int32Value(int32_t(isThis)));
+
     obj->initEnclosingEnvironment(&cx->global()->lexicalEnvironment());
     return obj;
 }
 
+bool
+NonSyntacticVariablesObject::isThis() const
+{
+    return getReservedSlot(IS_THIS_SLOT).toInt32();
+}
+
 const Class NonSyntacticVariablesObject::class_ = {
     "NonSyntacticVariablesObject",
     JSCLASS_HAS_RESERVED_SLOTS(NonSyntacticVariablesObject::RESERVED_SLOTS) |
     JSCLASS_IS_ANONYMOUS
 };
 
 /*****************************************************************************/
 
@@ -3184,25 +3192,32 @@ js::CreateObjectsForEnvironmentChain(JSC
 {
 #ifdef DEBUG
     for (size_t i = 0; i < chain.length(); ++i) {
         assertSameCompartment(cx, chain[i]);
         MOZ_ASSERT(!chain[i]->is<GlobalObject>());
     }
 #endif
 
-    // Construct With object wrappers for the things on this environment chain
-    // and use the result as the thing to scope the function to.
+    // Construct With object wrappers and NSVOs for the things on this
+    // environment chain and use the result as the thing to scope the function to.
+    RootedObject obj(cx);
     Rooted<WithEnvironmentObject*> withEnv(cx);
     RootedObject enclosingEnv(cx, terminatingEnv);
     for (size_t i = chain.length(); i > 0; ) {
-        withEnv = WithEnvironmentObject::createNonSyntactic(cx, chain[--i], enclosingEnv);
-        if (!withEnv)
-            return false;
-        enclosingEnv = withEnv;
+        obj = chain[--i];
+        if (obj->is<NonSyntacticVariablesObject>()) {
+            MOZ_ASSERT(i == chain.length() - 1);
+            enclosingEnv = obj;
+        } else {
+            withEnv = WithEnvironmentObject::createNonSyntactic(cx, obj, enclosingEnv);
+            if (!withEnv)
+                return false;
+            enclosingEnv = withEnv;
+        }
     }
 
     envObj.set(enclosingEnv);
     return true;
 }
 
 JSObject&
 WithEnvironmentObject::object() const
--- a/js/src/vm/EnvironmentObject.h
+++ b/js/src/vm/EnvironmentObject.h
@@ -176,24 +176,23 @@ EnvironmentCoordinateFunctionScript(JSSc
  * The embedding (Gecko) uses non-syntactic envs for various things, some of
  * which are detailed below. All env chain listings below are, from top to
  * bottom, outermost to innermost.
  *
  * A. Component loading
  *
  * Components may be loaded in "reuse loader global" mode, where to save on
  * memory, all JSMs and JS-implemented XPCOM modules are loaded into a single
- * global. Each individual JSMs are compiled as functions with their own
- * FakeBackstagePass. They have the following env chain:
+ * global. They have the following env chain:
  *
  *   BackstagePass global
  *       |
  *   Global lexical scope
  *       |
- *   WithEnvironmentObject wrapping FakeBackstagePass
+ *   NonSyntacticVariablesObject
  *       |
  *   LexicalEnvironmentObject
  *
  * B. Subscript loading
  *
  * Subscripts may be loaded into a target object. They have the following
  * env chain:
  *
@@ -576,23 +575,30 @@ class NamedLambdaObject : public Lexical
 // bindings. That is, a scope object that captures both qualified var
 // assignments and unqualified bareword assignments. Its parent is always the
 // global lexical environment.
 //
 // This is used in ExecuteInGlobalAndReturnScope and sits in front of the
 // global scope to store 'var' bindings, and to store fresh properties created
 // by assignments to undeclared variables that otherwise would have gone on
 // the global object.
+//
+// If |isThis| is true, then the NSVO object is also used as the |this| object,
+// instead of the global.
 class NonSyntacticVariablesObject : public EnvironmentObject
 {
+    static const unsigned IS_THIS_SLOT = 1;
+
   public:
-    static const unsigned RESERVED_SLOTS = 1;
+    static const unsigned RESERVED_SLOTS = 2;
     static const Class class_;
 
-    static NonSyntacticVariablesObject* create(JSContext* cx);
+    static NonSyntacticVariablesObject* create(JSContext* cx, bool isThis = false);
+
+    bool isThis() const;
 };
 
 // With environment objects on the run-time environment chain.
 class WithEnvironmentObject : public EnvironmentObject
 {
     static const unsigned OBJECT_SLOT = 1;
     static const unsigned THIS_SLOT = 2;
     static const unsigned SCOPE_SLOT = 3;