Bug 1271649 - Implement a C++ interface for Debugger.Environment instances;r=shu
authorEddy Bruel <ejpbruel@mozilla.com>
Mon, 27 Jun 2016 18:04:09 +0200
changeset 381526 f5b378eddb42ded3553b4199d47c328ee8e5dbed
parent 381525 7a6d37ee074f25418bd9ea12309e3a6460022ed3
child 381527 c9ee3b0069e4841323c512d563ea84df73e5c313
push id21504
push userbmo:npang@mozilla.com
push dateMon, 27 Jun 2016 18:10:09 +0000
reviewersshu
bugs1271649
milestone50.0a1
Bug 1271649 - Implement a C++ interface for Debugger.Environment instances;r=shu
js/src/vm/Debugger-inl.h
js/src/vm/Debugger.cpp
js/src/vm/Debugger.h
--- a/js/src/vm/Debugger-inl.h
+++ b/js/src/vm/Debugger-inl.h
@@ -75,15 +75,22 @@ js::Debugger::onNewWasmInstance(JSContex
     if (!wasmInstances.putNew(wasmInstance))
         return;
 
     if (cx->compartment()->isDebuggee())
         slowPathOnNewWasmInstance(cx, wasmInstance);
 }
 
 inline js::Debugger*
+js::DebuggerEnvironment::owner() const
+{
+    JSObject* dbgobj = &getReservedSlot(OWNER_SLOT).toObject();
+    return Debugger::fromJSObject(dbgobj);
+}
+
+inline js::Debugger*
 js::DebuggerObject::owner() const
 {
     JSObject* dbgobj = &getReservedSlot(OWNER_SLOT).toObject();
     return Debugger::fromJSObject(dbgobj);
 }
 
 #endif /* vm_Debugger_inl_h */
--- a/js/src/vm/Debugger.cpp
+++ b/js/src/vm/Debugger.cpp
@@ -102,41 +102,36 @@ enum {
     JSSLOT_DEBUGARGUMENTS_COUNT
 };
 
 static const Class DebuggerArguments_class = {
     "Arguments",
     JSCLASS_HAS_RESERVED_SLOTS(JSSLOT_DEBUGARGUMENTS_COUNT)
 };
 
-enum {
-    JSSLOT_DEBUGENV_OWNER,
-    JSSLOT_DEBUGENV_COUNT
-};
-
-static const ClassOps DebuggerEnv_classOps = {
+const ClassOps DebuggerEnvironment::classOps_ = {
     nullptr,    /* addProperty */
     nullptr,    /* delProperty */
     nullptr,    /* getProperty */
     nullptr,    /* setProperty */
     nullptr,    /* enumerate   */
     nullptr,    /* resolve     */
     nullptr,    /* mayResolve  */
     nullptr,    /* finalize    */
     nullptr,    /* call        */
     nullptr,    /* hasInstance */
     nullptr,    /* construct   */
     DebuggerEnv_trace
 };
 
-static const Class DebuggerEnv_class = {
+const Class DebuggerEnvironment::class_ = {
     "Environment",
     JSCLASS_HAS_PRIVATE |
-    JSCLASS_HAS_RESERVED_SLOTS(JSSLOT_DEBUGENV_COUNT),
-    &DebuggerEnv_classOps
+    JSCLASS_HAS_RESERVED_SLOTS(DebuggerEnvironment::RESERVED_SLOTS),
+    &classOps_
 };
 
 enum {
     JSSLOT_DEBUGOBJECT_OWNER,
     JSSLOT_DEBUGOBJECT_COUNT
 };
 
 static const ClassOps DebuggerObject_classOps = {
@@ -687,26 +682,26 @@ Debugger::init(JSContext* cx)
     if (!ok)
         ReportOutOfMemory(cx);
     return ok;
 }
 
 JS_STATIC_ASSERT(unsigned(JSSLOT_DEBUGFRAME_OWNER) == unsigned(JSSLOT_DEBUGSCRIPT_OWNER));
 JS_STATIC_ASSERT(unsigned(JSSLOT_DEBUGFRAME_OWNER) == unsigned(JSSLOT_DEBUGSOURCE_OWNER));
 JS_STATIC_ASSERT(unsigned(JSSLOT_DEBUGFRAME_OWNER) == unsigned(JSSLOT_DEBUGOBJECT_OWNER));
-JS_STATIC_ASSERT(unsigned(JSSLOT_DEBUGFRAME_OWNER) == unsigned(JSSLOT_DEBUGENV_OWNER));
+JS_STATIC_ASSERT(unsigned(JSSLOT_DEBUGFRAME_OWNER) == unsigned(DebuggerEnvironment::OWNER_SLOT));
 
 /* static */ Debugger*
 Debugger::fromChildJSObject(JSObject* obj)
 {
     MOZ_ASSERT(obj->getClass() == &DebuggerFrame_class ||
                obj->getClass() == &DebuggerScript_class ||
                obj->getClass() == &DebuggerSource_class ||
                obj->getClass() == &DebuggerObject::class_ ||
-               obj->getClass() == &DebuggerEnv_class);
+               obj->getClass() == &DebuggerEnvironment::class_);
     JSObject* dbgobj = &obj->as<NativeObject>().getReservedSlot(JSSLOT_DEBUGOBJECT_OWNER).toObject();
     return fromJSObject(dbgobj);
 }
 
 bool
 Debugger::hasMemory() const
 {
     return object->getReservedSlot(JSSLOT_DEBUG_MEMORY_INSTANCE).isObject();
@@ -1023,58 +1018,75 @@ Debugger::slowPathOnExceptionUnwind(JSCo
 
       default:
         MOZ_CRASH("Invalid onExceptionUnwind trap status");
     }
 
     return status;
 }
 
+// TODO: Remove Remove this function when all properties/methods returning a
+///      DebuggerEnvironment have been given a C++ interface (bug 1271649).
 bool
 Debugger::wrapEnvironment(JSContext* cx, Handle<Env*> env, MutableHandleValue rval)
 {
     if (!env) {
         rval.setNull();
         return true;
     }
 
+    Rooted<DebuggerEnvironment*> envobj(cx);
+
+    if (!wrapEnvironment(cx, env, &envobj))
+        return false;
+
+    rval.setObject(*envobj);
+    return true;
+}
+
+bool
+Debugger::wrapEnvironment(JSContext* cx, Handle<Env*> env,
+                          MutableHandle<DebuggerEnvironment*> result)
+{
+    MOZ_ASSERT(env);
+
     /*
      * DebuggerEnv should only wrap a debug scope chain obtained (transitively)
      * from GetDebugScopeFor(Frame|Function).
      */
     MOZ_ASSERT(!IsSyntacticScope(env));
 
-    NativeObject* envobj;
     DependentAddPtr<ObjectWeakMap> p(cx, environments, env);
     if (p) {
-        envobj = &p->value()->as<NativeObject>();
+        result.set(&p->value()->as<DebuggerEnvironment>());
     } else {
         /* Create a new Debugger.Environment for env. */
         RootedObject proto(cx, &object->getReservedSlot(JSSLOT_DEBUG_ENV_PROTO).toObject());
-        envobj = NewNativeObjectWithGivenProto(cx, &DebuggerEnv_class, proto,
-                                               TenuredObject);
+        RootedNativeObject debugger(cx, object);
+
+        Rooted<DebuggerEnvironment*> envobj(cx,
+            DebuggerEnvironment::create(cx, proto, env, debugger));
         if (!envobj)
             return false;
-        envobj->setPrivateGCThing(env);
-        envobj->setReservedSlot(JSSLOT_DEBUGENV_OWNER, ObjectValue(*object));
 
         if (!p.add(cx, environments, env, envobj)) {
             NukeDebuggerWrapper(envobj);
             return false;
         }
 
         CrossCompartmentKey key(object, env, CrossCompartmentKey::DebuggerEnvironment);
         if (!object->compartment()->putWrapper(cx, key, ObjectValue(*envobj))) {
             NukeDebuggerWrapper(envobj);
             environments.remove(env);
             return false;
         }
 
-    }
-    rval.setObject(*envobj);
+        result.set(envobj);
+    }
+
     return true;
 }
 
 bool
 Debugger::wrapDebuggeeValue(JSContext* cx, MutableHandleValue vp)
 {
     assertSameCompartment(cx, object.get());
 
@@ -9539,35 +9551,35 @@ DebuggerEnv_trace(JSTracer* trc, JSObjec
      */
     if (Env* referent = (JSObject*) obj->as<NativeObject>().getPrivate()) {
         TraceManuallyBarrieredCrossCompartmentEdge(trc, obj, &referent,
                                                    "Debugger.Environment referent");
         obj->as<NativeObject>().setPrivateUnbarriered(referent);
     }
 }
 
-static NativeObject*
-DebuggerEnv_checkThis(JSContext* cx, const CallArgs& args, const char* fnname,
-                      bool requireDebuggee = true)
+/* static */ DebuggerEnvironment*
+DebuggerEnvironment::checkThis(JSContext* cx, const CallArgs& args, const char* fnname,
+                               bool requireDebuggee)
 {
     JSObject* thisobj = NonNullObject(cx, args.thisv());
     if (!thisobj)
         return nullptr;
-    if (thisobj->getClass() != &DebuggerEnv_class) {
+    if (thisobj->getClass() != &DebuggerEnvironment::class_) {
         JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_INCOMPATIBLE_PROTO,
                              "Debugger.Environment", fnname, thisobj->getClass()->name);
         return nullptr;
     }
 
     /*
-     * Forbid Debugger.Environment.prototype, which is of class DebuggerEnv_class
+     * Forbid Debugger.Environment.prototype, which is of class DebuggerEnvironment::class_
      * but isn't a real working Debugger.Environment. The prototype object is
      * distinguished by having no referent.
      */
-    NativeObject* nthisobj = &thisobj->as<NativeObject>();
+    DebuggerEnvironment* nthisobj = &thisobj->as<DebuggerEnvironment>();
     if (!nthisobj->getPrivate()) {
         JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_INCOMPATIBLE_PROTO,
                              "Debugger.Environment", fnname, "prototype object");
         return nullptr;
     }
 
     /*
      * Forbid access to Debugger.Environment objects that are not debuggee
@@ -9580,31 +9592,24 @@ DebuggerEnv_checkThis(JSContext* cx, con
                                  "Debugger.Environment", "environment");
             return nullptr;
         }
     }
 
     return nthisobj;
 }
 
-#define THIS_DEBUGENV(cx, argc, vp, fnname, args, envobj, env)                \
-    CallArgs args = CallArgsFromVp(argc, vp);                                 \
-    NativeObject* envobj = DebuggerEnv_checkThis(cx, args, fnname);           \
-    if (!envobj)                                                              \
-        return false;                                                         \
-    Rooted<Env*> env(cx, static_cast<Env*>(envobj->getPrivate()));            \
-    MOZ_ASSERT(env);                                                          \
-    MOZ_ASSERT(!IsSyntacticScope(env));
-
- #define THIS_DEBUGENV_OWNER(cx, argc, vp, fnname, args, envobj, env, dbg)    \
-     THIS_DEBUGENV(cx, argc, vp, fnname, args, envobj, env);                  \
-    Debugger* dbg = Debugger::fromChildJSObject(envobj)
-
-static bool
-DebuggerEnv_construct(JSContext* cx, unsigned argc, Value* vp)
+#define THIS_DEBUGGER_ENVIRONMENT(cx, argc, vp, fnname, args, environment)                                 \
+    CallArgs args = CallArgsFromVp(argc, vp);                                                              \
+    Rooted<DebuggerEnvironment*> environment(cx, DebuggerEnvironment::checkThis(cx, args, fnname, false)); \
+    if (!environment)                                                                                      \
+        return false;                                                                                      \
+
+/* static */ bool
+DebuggerEnvironment::construct(JSContext* cx, unsigned argc, Value* vp)
 {
     JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_NO_CONSTRUCTOR,
                          "Debugger.Environment");
     return false;
 }
 
 static bool
 IsDeclarative(Env* env)
@@ -9615,312 +9620,491 @@ IsDeclarative(Env* env)
 template <typename T>
 static bool
 IsDebugScopeWrapper(Env* env)
 {
     return env->is<DebugScopeObject>() &&
            env->as<DebugScopeObject>().scope().is<T>();
 }
 
-static bool
-DebuggerEnv_getType(JSContext* cx, unsigned argc, Value* vp)
-{
-    THIS_DEBUGENV(cx, argc, vp, "get type", args, envobj, env);
-
-    /* Don't bother switching compartments just to check env's class. */
+bool
+DebuggerEnvironment::typeGetter(JSContext* cx, unsigned argc, Value* vp)
+{
+    THIS_DEBUGGER_ENVIRONMENT(cx, argc, vp, "get type", args, environment);
+
+    if (!environment->requireDebuggee(cx))
+        return false;
+
+    DebuggerEnvironmentType type = environment->type();
+
     const char* s;
-    if (IsDeclarative(env))
+    switch (type) {
+      case DebuggerEnvironmentType::Declarative:
         s = "declarative";
-    else if (IsDebugScopeWrapper<DynamicWithObject>(env))
+        break;
+      case DebuggerEnvironmentType::With:
         s = "with";
-    else
+        break;
+      case DebuggerEnvironmentType::Object:
         s = "object";
+        break;
+    }
 
     JSAtom* str = Atomize(cx, s, strlen(s), PinAtom);
     if (!str)
         return false;
+
     args.rval().setString(str);
     return true;
 }
 
-static bool
-DebuggerEnv_getParent(JSContext* cx, unsigned argc, Value* vp)
-{
-    THIS_DEBUGENV_OWNER(cx, argc, vp, "get parent", args, envobj, env, dbg);
-
-    /* Don't bother switching compartments just to get env's parent. */
-    Rooted<Env*> parent(cx, env->enclosingScope());
-    return dbg->wrapEnvironment(cx, parent, args.rval());
-}
-
-static bool
-DebuggerEnv_getObject(JSContext* cx, unsigned argc, Value* vp)
-{
-    THIS_DEBUGENV_OWNER(cx, argc, vp, "get type", args, envobj, env, dbg);
-
-    /*
-     * Don't bother switching compartments just to check env's class and
-     * possibly get its proto.
-     */
-    if (IsDeclarative(env)) {
+/* static */ bool
+DebuggerEnvironment::parentGetter(JSContext* cx, unsigned argc, Value* vp)
+{
+    THIS_DEBUGGER_ENVIRONMENT(cx, argc, vp, "get type", args, environment);
+
+    if (!environment->requireDebuggee(cx))
+        return false;
+
+    Rooted<DebuggerEnvironment*> result(cx);
+    if (!environment->getParent(cx, &result))
+        return false;
+
+    args.rval().setObjectOrNull(result);
+    return true;
+}
+
+/* static */ bool
+DebuggerEnvironment::objectGetter(JSContext* cx, unsigned argc, Value* vp)
+{
+    THIS_DEBUGGER_ENVIRONMENT(cx, argc, vp, "get type", args, environment);
+
+    if (!environment->requireDebuggee(cx))
+        return false;
+
+    if (environment->type() == DebuggerEnvironmentType::Declarative) {
         JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_DEBUG_NO_SCOPE_OBJECT);
         return false;
     }
 
-    JSObject* obj;
-    if (IsDebugScopeWrapper<DynamicWithObject>(env)) {
-        obj = &env->as<DebugScopeObject>().scope().as<DynamicWithObject>().object();
-    } else if (IsDebugScopeWrapper<NonSyntacticVariablesObject>(env)) {
-        obj = &env->as<DebugScopeObject>().scope().as<NonSyntacticVariablesObject>();
-    } else {
-        obj = env;
-        MOZ_ASSERT(!obj->is<DebugScopeObject>());
-    }
+    Rooted<DebuggerObject*> result(cx);
+    if (!environment->getObject(cx, &result))
+        return false;
+
+    args.rval().setObject(*result);
+    return true;
+}
+
+/* static */ bool
+DebuggerEnvironment::calleeGetter(JSContext* cx, unsigned argc, Value* vp)
+{
+    THIS_DEBUGGER_ENVIRONMENT(cx, argc, vp, "get callee", args, environment);
+
+    if (!environment->requireDebuggee(cx))
+        return false;
+
+    Rooted<DebuggerObject*> result(cx);
+    if (!environment->getCallee(cx, &result))
+        return false;
+
+    args.rval().setObjectOrNull(result);
+    return true;
+}
+
+/* static */ bool
+DebuggerEnvironment::inspectableGetter(JSContext* cx, unsigned argc, Value* vp)
+{
+    THIS_DEBUGGER_ENVIRONMENT(cx, argc, vp, "get inspectable", args, environment);
+
+    args.rval().setBoolean(environment->isDebuggee());
+    return true;
+}
+
+/* static */ bool
+DebuggerEnvironment::optimizedOutGetter(JSContext* cx, unsigned argc, Value* vp)
+{
+    THIS_DEBUGGER_ENVIRONMENT(cx, argc, vp, "get optimizedOut", args, environment);
+
+    args.rval().setBoolean(environment->isOptimized());
+    return true;
+}
+
+/* static */ bool
+DebuggerEnvironment::namesMethod(JSContext* cx, unsigned argc, Value* vp)
+{
+    THIS_DEBUGGER_ENVIRONMENT(cx, argc, vp, "names", args, environment);
+
+    if (!environment->requireDebuggee(cx))
+        return false;
+
+    Rooted<IdVector> ids(cx, IdVector(cx));
+    if (!DebuggerEnvironment::getNames(cx, environment, &ids))
+        return false;
+
+    RootedObject obj(cx, IdVectorToArray(cx, ids));
+    if (!obj)
+        return false;
 
     args.rval().setObject(*obj);
-    if (!dbg->wrapDebuggeeValue(cx, args.rval()))
-        return false;
-    return true;
-}
-
-static bool
-DebuggerEnv_getCallee(JSContext* cx, unsigned argc, Value* vp)
-{
-    THIS_DEBUGENV_OWNER(cx, argc, vp, "get callee", args, envobj, env, dbg);
-
-    args.rval().setNull();
-
-    if (!env->is<DebugScopeObject>())
-        return true;
-
-    JSObject& scope = env->as<DebugScopeObject>().scope();
-    if (!scope.is<CallObject>())
-        return true;
-
-    CallObject& callobj = scope.as<CallObject>();
-    if (callobj.isForEval())
-        return true;
-
-    JSFunction& callee = callobj.callee();
-    if (IsInternalFunctionObject(callee))
-        return true;
-
-    args.rval().setObject(callee);
-    if (!dbg->wrapDebuggeeValue(cx, args.rval()))
-        return false;
-    return true;
-}
-
-static bool
-DebuggerEnv_getInspectable(JSContext* cx, unsigned argc, Value* vp)
-{
-    CallArgs args = CallArgsFromVp(argc, vp);
-    NativeObject* envobj = DebuggerEnv_checkThis(cx, args, "get inspectable", false);
-    if (!envobj)
-        return false;
-    Rooted<Env*> env(cx, static_cast<Env*>(envobj->getPrivate()));
-    MOZ_ASSERT(env);
-    MOZ_ASSERT(!env->is<ScopeObject>());
-
-    Debugger* dbg = Debugger::fromChildJSObject(envobj);
-
-    args.rval().setBoolean(dbg->observesGlobal(&env->global()));
-    return true;
-}
-
-static bool
-DebuggerEnv_getOptimizedOut(JSContext* cx, unsigned argc, Value* vp)
-{
-    CallArgs args = CallArgsFromVp(argc, vp);
-    NativeObject* envobj = DebuggerEnv_checkThis(cx, args, "get optimizedOut", false);
-    if (!envobj)
-        return false;
-    Rooted<Env*> env(cx, static_cast<Env*>(envobj->getPrivate()));
-    MOZ_ASSERT(env);
-    MOZ_ASSERT(!env->is<ScopeObject>());
-
-    args.rval().setBoolean(env->is<DebugScopeObject>() &&
-                           env->as<DebugScopeObject>().isOptimizedOut());
-    return true;
-}
-
-static bool
-DebuggerEnv_names(JSContext* cx, unsigned argc, Value* vp)
-{
-    THIS_DEBUGENV(cx, argc, vp, "names", args, envobj, env);
-
-    AutoIdVector keys(cx);
-    {
-        Maybe<AutoCompartment> ac;
-        ac.emplace(cx, env);
-        ErrorCopier ec(ac);
-        if (!GetPropertyKeys(cx, env, JSITER_HIDDEN, &keys))
-            return false;
-    }
-
-    RootedObject arr(cx, NewDenseEmptyArray(cx));
-    if (!arr)
-        return false;
+    return true;
+}
+
+/* static */ bool
+DebuggerEnvironment::findMethod(JSContext* cx, unsigned argc, Value* vp)
+{
+    THIS_DEBUGGER_ENVIRONMENT(cx, argc, vp, "find", args, environment);
+    if (!args.requireAtLeast(cx, "Debugger.Environment.find", 1))
+        return false;
+
+    if (!environment->requireDebuggee(cx))
+        return false;
+
     RootedId id(cx);
-    for (size_t i = 0, len = keys.length(); i < len; i++) {
-        id = keys[i];
-        if (JSID_IS_ATOM(id) && IsIdentifier(JSID_TO_ATOM(id))) {
-            if (!NewbornArrayPush(cx, arr, StringValue(JSID_TO_STRING(id))))
-                return false;
-        }
-    }
-    args.rval().setObject(*arr);
-    return true;
-}
-
-static bool
-DebuggerEnv_find(JSContext* cx, unsigned argc, Value* vp)
-{
-    THIS_DEBUGENV_OWNER(cx, argc, vp, "find", args, envobj, env, dbg);
-    if (!args.requireAtLeast(cx, "Debugger.Environment.find", 1))
+    if (!ValueToIdentifier(cx, args[0], &id))
+        return false;
+
+    Rooted<DebuggerEnvironment*> result(cx);
+    if (!DebuggerEnvironment::find(cx, environment, id, &result))
+        return false;
+
+    args.rval().setObjectOrNull(result);
+    return true;
+}
+
+/* static */ bool
+DebuggerEnvironment::getVariableMethod(JSContext* cx, unsigned argc, Value* vp)
+{
+    THIS_DEBUGGER_ENVIRONMENT(cx, argc, vp, "getVariable", args, environment);
+    if (!args.requireAtLeast(cx, "Debugger.Environment.getVariable", 1))
+        return false;
+
+    if (!environment->requireDebuggee(cx))
+        return false;
+
+    RootedId id(cx);
+    if (!ValueToIdentifier(cx, args[0], &id))
+        return false;
+
+    return DebuggerEnvironment::getVariable(cx, environment, id, args.rval());
+}
+
+/* static */ bool
+DebuggerEnvironment::setVariableMethod(JSContext* cx, unsigned argc, Value* vp)
+{
+    THIS_DEBUGGER_ENVIRONMENT(cx, argc, vp, "setVariable", args, environment);
+    if (!args.requireAtLeast(cx, "Debugger.Environment.setVariable", 2))
+        return false;
+
+    if (!environment->requireDebuggee(cx))
         return false;
 
     RootedId id(cx);
     if (!ValueToIdentifier(cx, args[0], &id))
         return false;
 
+    if (!DebuggerEnvironment::setVariable(cx, environment, id, args[1]))
+        return false;
+
+    args.rval().setUndefined();
+    return true;
+}
+
+bool
+DebuggerEnvironment::requireDebuggee(JSContext* cx) const
+{
+    if (!isDebuggee()) {
+        JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_DEBUG_NOT_DEBUGGEE,
+                             "Debugger.Environment", "environment");
+
+        return false;
+    }
+
+    return true;
+}
+
+const JSPropertySpec DebuggerEnvironment::properties_[] = {
+    JS_PSG("type", DebuggerEnvironment::typeGetter, 0),
+    JS_PSG("parent", DebuggerEnvironment::parentGetter, 0),
+    JS_PSG("object", DebuggerEnvironment::objectGetter, 0),
+    JS_PSG("callee", DebuggerEnvironment::calleeGetter, 0),
+    JS_PSG("inspectable", DebuggerEnvironment::inspectableGetter, 0),
+    JS_PSG("optimizedOut", DebuggerEnvironment::optimizedOutGetter, 0),
+    JS_PS_END
+};
+
+const JSFunctionSpec DebuggerEnvironment::methods_[] = {
+    JS_FN("names", DebuggerEnvironment::namesMethod, 0, 0),
+    JS_FN("find", DebuggerEnvironment::findMethod, 1, 0),
+    JS_FN("getVariable", DebuggerEnvironment::getVariableMethod, 1, 0),
+    JS_FN("setVariable", DebuggerEnvironment::setVariableMethod, 2, 0),
+    JS_FS_END
+};
+
+/* static */ NativeObject*
+DebuggerEnvironment::initClass(JSContext* cx, HandleObject dbgCtor, HandleObject obj)
+{
+    Rooted<GlobalObject*> global(cx, &obj->as<GlobalObject>());
+    RootedObject objProto(cx, global->getOrCreateObjectPrototype(cx));
+
+    return InitClass(cx, dbgCtor, objProto, &DebuggerEnvironment::class_, construct, 0,
+                     properties_, methods_, nullptr, nullptr);
+}
+
+/* static */ DebuggerEnvironment*
+DebuggerEnvironment::create(JSContext* cx, HandleObject proto, HandleObject referent,
+                            HandleNativeObject debugger)
+{
+    RootedObject obj(cx, NewObjectWithGivenProto(cx, &DebuggerEnvironment::class_, proto,
+                                                 TenuredObject));
+    if (!obj)
+        return nullptr;
+
+    DebuggerEnvironment& environment = obj->as<DebuggerEnvironment>();
+    environment.setPrivateGCThing(referent);
+    environment.setReservedSlot(OWNER_SLOT, ObjectValue(*debugger));
+
+    return &environment;
+}
+
+/* static */ DebuggerEnvironmentType
+DebuggerEnvironment::type() const
+{
+    /* Don't bother switching compartments just to check env's type. */
+    if (IsDeclarative(referent()))
+        return DebuggerEnvironmentType::Declarative;
+    else if (IsDebugScopeWrapper<DynamicWithObject>(referent()))
+        return DebuggerEnvironmentType::With;
+    return DebuggerEnvironmentType::Object;
+}
+
+bool
+DebuggerEnvironment::getParent(JSContext* cx, MutableHandle<DebuggerEnvironment*> result) const
+{
+    /* Don't bother switching compartments just to get env's parent. */
+    Rooted<Env*> parent(cx, referent()->enclosingScope());
+    if (!parent) {
+        result.set(nullptr);
+        return true;
+    }
+
+    return owner()->wrapEnvironment(cx, parent, result);
+}
+
+bool
+DebuggerEnvironment::getObject(JSContext* cx, MutableHandle<DebuggerObject*> result) const
+{
+    MOZ_ASSERT(type() != DebuggerEnvironmentType::Declarative);
+
+    /* Don't bother switching compartments just to get env's object. */
+    RootedObject object(cx);
+    if (IsDebugScopeWrapper<DynamicWithObject>(referent())) {
+        object.set(&referent()->as<DebugScopeObject>().scope().as<DynamicWithObject>().object());
+    } else if (IsDebugScopeWrapper<NonSyntacticVariablesObject>(referent())) {
+        object.set(&referent()->as<DebugScopeObject>().scope().as<NonSyntacticVariablesObject>());
+    } else {
+        object.set(referent());
+        MOZ_ASSERT(!object->is<DebugScopeObject>());
+    }
+
+    return owner()->wrapDebuggeeObject(cx, object, result);
+}
+
+bool
+DebuggerEnvironment::getCallee(JSContext* cx, MutableHandle<DebuggerObject*> result) const
+{
+    if (!referent()->is<DebugScopeObject>()) {
+        result.set(nullptr);
+        return true;
+    }
+
+    JSObject& scope = referent()->as<DebugScopeObject>().scope();
+    if (!scope.is<CallObject>()) {
+        result.set(nullptr);
+        return true;
+    }
+
+    CallObject& callobj = scope.as<CallObject>();
+    if (callobj.isForEval()) {
+        result.set(nullptr);
+        return true;
+    }
+
+    RootedObject callee(cx, &callobj.callee());
+    if (IsInternalFunctionObject(*callee)) {
+        result.set(nullptr);
+        return true;
+    }
+
+    return owner()->wrapDebuggeeObject(cx, callee, result);
+}
+
+bool
+DebuggerEnvironment::isDebuggee() const
+{
+    MOZ_ASSERT(referent());
+    MOZ_ASSERT(!referent()->is<ScopeObject>());
+
+    return owner()->observesGlobal(&referent()->global());
+}
+
+bool
+DebuggerEnvironment::isOptimized() const
+{
+    return referent()->is<DebugScopeObject>() && referent()->as<DebugScopeObject>().isOptimizedOut();
+}
+
+/* static */ bool
+DebuggerEnvironment::getNames(JSContext* cx, Handle<DebuggerEnvironment*> environment,
+                              MutableHandle<IdVector> result)
+{
+    MOZ_ASSERT(environment->isDebuggee());
+
+    Rooted<Env*> referent(cx, environment->referent());
+
+    AutoIdVector ids(cx);
+    {
+        Maybe<AutoCompartment> ac;
+        ac.emplace(cx, referent);
+
+        ErrorCopier ec(ac);
+        if (!GetPropertyKeys(cx, referent, JSITER_HIDDEN, &ids))
+            return false;
+    }
+
+    for (size_t i = 0; i < ids.length(); ++i) {
+        jsid id = ids[i];
+        if (JSID_IS_ATOM(id) && IsIdentifier(JSID_TO_ATOM(id))) {
+            if (!result.append(id))
+                return false;
+        } 
+    }
+
+    return true;
+}
+
+/* static */ bool
+DebuggerEnvironment::find(JSContext* cx, Handle<DebuggerEnvironment*> environment, HandleId id,
+                          MutableHandle<DebuggerEnvironment*> result)
+{
+    MOZ_ASSERT(environment->isDebuggee());
+
+    Rooted<Env*> env(cx, environment->referent());
+    Debugger* dbg = environment->owner();
+
     {
         Maybe<AutoCompartment> ac;
         ac.emplace(cx, env);
 
         /* This can trigger resolve hooks. */
         ErrorCopier ec(ac);
-        bool found;
         for (; env; env = env->enclosingScope()) {
+            bool found;
             if (!HasProperty(cx, env, id, &found))
                 return false;
             if (found)
                 break;
         }
     }
 
-    return dbg->wrapEnvironment(cx, env, args.rval());
-}
-
-static bool
-DebuggerEnv_getVariable(JSContext* cx, unsigned argc, Value* vp)
-{
-    THIS_DEBUGENV_OWNER(cx, argc, vp, "getVariable", args, envobj, env, dbg);
-    if (!args.requireAtLeast(cx, "Debugger.Environment.getVariable", 1))
-        return false;
-
-    RootedId id(cx);
-    if (!ValueToIdentifier(cx, args[0], &id))
-        return false;
-
-    RootedValue v(cx);
+    if (!env) {
+        result.set(nullptr);
+        return true;
+    }
+
+    return dbg->wrapEnvironment(cx, env, result);
+}
+
+/* static */ bool
+DebuggerEnvironment::getVariable(JSContext* cx, Handle<DebuggerEnvironment*> environment,
+                                 HandleId id, MutableHandleValue result)
+{
+    MOZ_ASSERT(environment->isDebuggee());
+
+    Rooted<Env*> referent(cx, environment->referent());
+    Debugger* dbg = environment->owner();
+
     {
         Maybe<AutoCompartment> ac;
-        ac.emplace(cx, env);
+        ac.emplace(cx, referent);
 
         /* This can trigger getters. */
         ErrorCopier ec(ac);
 
         bool found;
-        if (!HasProperty(cx, env, id, &found))
+        if (!HasProperty(cx, referent, id, &found))
             return false;
         if (!found) {
-            args.rval().setUndefined();
+            result.setUndefined();
             return true;
         }
 
         // For DebugScopeObjects, we get sentinel values for optimized out
         // slots and arguments instead of throwing (the default behavior).
         //
         // See wrapDebuggeeValue for how the sentinel values are wrapped.
-        if (env->is<DebugScopeObject>()) {
-            if (!env->as<DebugScopeObject>().getMaybeSentinelValue(cx, id, &v))
+        if (referent->is<DebugScopeObject>()) {
+            if (!referent->as<DebugScopeObject>().getMaybeSentinelValue(cx, id, result))
                 return false;
         } else {
-            if (!GetProperty(cx, env, env, id, &v))
+            if (!GetProperty(cx, referent, referent, id, result))
                 return false;
         }
     }
 
     // When we've faked up scope chain objects for optimized-out scopes,
     // declarative environments may contain internal JSFunction objects, which
     // we shouldn't expose to the user.
-    if (v.isObject()) {
-        RootedObject obj(cx, &v.toObject());
+    if (result.isObject()) {
+        RootedObject obj(cx, &result.toObject());
         if (obj->is<JSFunction>() &&
             IsInternalFunctionObject(obj->as<JSFunction>()))
-            v.setMagic(JS_OPTIMIZED_OUT);
-    }
-
-    if (!dbg->wrapDebuggeeValue(cx, &v))
-        return false;
-    args.rval().set(v);
-    return true;
-}
-
-static bool
-DebuggerEnv_setVariable(JSContext* cx, unsigned argc, Value* vp)
-{
-    THIS_DEBUGENV_OWNER(cx, argc, vp, "setVariable", args, envobj, env, dbg);
-    if (!args.requireAtLeast(cx, "Debugger.Environment.setVariable", 2))
-        return false;
-
-    RootedId id(cx);
-    if (!ValueToIdentifier(cx, args[0], &id))
-        return false;
-
-    RootedValue v(cx, args[1]);
-    if (!dbg->unwrapDebuggeeValue(cx, &v))
+            result.setMagic(JS_OPTIMIZED_OUT);
+    }
+
+    return dbg->wrapDebuggeeValue(cx, result);
+}
+
+/* static */ bool
+DebuggerEnvironment::setVariable(JSContext* cx, Handle<DebuggerEnvironment*> environment,
+                                 HandleId id, HandleValue value_)
+{
+    MOZ_ASSERT(environment->isDebuggee());
+
+    Rooted<Env*> referent(cx, environment->referent());
+    Debugger* dbg = environment->owner();
+
+    RootedValue value(cx, value_);
+    if (!dbg->unwrapDebuggeeValue(cx, &value))
         return false;
 
     {
         Maybe<AutoCompartment> ac;
-        ac.emplace(cx, env);
-        if (!cx->compartment()->wrap(cx, &v))
+        ac.emplace(cx, referent);
+        if (!cx->compartment()->wrap(cx, &value))
             return false;
 
         /* This can trigger setters. */
         ErrorCopier ec(ac);
 
         /* Make sure the environment actually has the specified binding. */
-        bool has;
-        if (!HasProperty(cx, env, id, &has))
-            return false;
-        if (!has) {
+        bool found;
+        if (!HasProperty(cx, referent, id, &found))
+            return false;
+        if (!found) {
             JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_DEBUG_VARIABLE_NOT_FOUND);
             return false;
         }
 
         /* Just set the property. */
-        if (!SetProperty(cx, env, id, v))
-            return false;
-    }
-
-    args.rval().setUndefined();
-    return true;
-}
-
-static const JSPropertySpec DebuggerEnv_properties[] = {
-    JS_PSG("type", DebuggerEnv_getType, 0),
-    JS_PSG("object", DebuggerEnv_getObject, 0),
-    JS_PSG("parent", DebuggerEnv_getParent, 0),
-    JS_PSG("callee", DebuggerEnv_getCallee, 0),
-    JS_PSG("inspectable", DebuggerEnv_getInspectable, 0),
-    JS_PSG("optimizedOut", DebuggerEnv_getOptimizedOut, 0),
-    JS_PS_END
-};
-
-static const JSFunctionSpec DebuggerEnv_methods[] = {
-    JS_FN("names", DebuggerEnv_names, 0, 0),
-    JS_FN("find", DebuggerEnv_find, 1, 0),
-    JS_FN("getVariable", DebuggerEnv_getVariable, 1, 0),
-    JS_FN("setVariable", DebuggerEnv_setVariable, 2, 0),
-    JS_FS_END
-};
-
+        if (!SetProperty(cx, referent, id, value))
+            return false;
+    }
+
+    return true;
+}
 
 
 /*** JS::dbg::Builder ****************************************************************************/
 
 Builder::Builder(JSContext* cx, js::Debugger* debugger)
   : debuggerObject(cx, debugger->toJSObject().get()),
     debugger(debugger)
 { }
@@ -10057,20 +10241,17 @@ JS_DefineDebuggerObject(JSContext* cx, H
                             nullptr, nullptr);
     if (!sourceProto)
         return false;
 
     objectProto = DebuggerObject::initClass(cx, obj, debugCtor);
     if (!objectProto)
         return false;
 
-    envProto = InitClass(cx, debugCtor, objProto, &DebuggerEnv_class,
-                         DebuggerEnv_construct, 0,
-                         DebuggerEnv_properties, DebuggerEnv_methods,
-                         nullptr, nullptr);
+    envProto = DebuggerEnvironment::initClass(cx, debugCtor, obj);
     if (!envProto)
         return false;
 
     memoryProto = InitClass(cx, debugCtor, objProto, &DebuggerMemory::class_,
                             DebuggerMemory::construct, 0, DebuggerMemory::properties,
                             DebuggerMemory::methods, nullptr, nullptr);
     if (!memoryProto)
         return false;
--- a/js/src/vm/Debugger.h
+++ b/js/src/vm/Debugger.h
@@ -240,16 +240,17 @@ typedef JSObject Env;
 //   2. A wasm JSFunction, denoting a synthesized wasm function script.
 //      NYI!
 typedef mozilla::Variant<JSScript*, WasmInstanceObject*> DebuggerScriptReferent;
 
 // Either a ScriptSourceObject, for ordinary JS, or a WasmInstanceObject,
 // denoting the synthesized source of a wasm module.
 typedef mozilla::Variant<ScriptSourceObject*, WasmInstanceObject*> DebuggerSourceReferent;
 
+class DebuggerEnvironment;
 class DebuggerObject;
 
 class Debugger : private mozilla::LinkedListElement<Debugger>
 {
     friend class Breakpoint;
     friend class DebuggerMemory;
     friend class SavedStacks;
     friend class mozilla::LinkedListElement<Debugger>;
@@ -903,16 +904,18 @@ class Debugger : private mozilla::Linked
     bool observesScript(JSScript* script) const;
 
     /*
      * If env is nullptr, call vp->setNull() and return true. Otherwise, find
      * or create a Debugger.Environment object for the given Env. On success,
      * store the Environment object in *vp and return true.
      */
     MOZ_MUST_USE bool wrapEnvironment(JSContext* cx, Handle<Env*> env, MutableHandleValue vp);
+    MOZ_MUST_USE bool wrapEnvironment(JSContext* cx, Handle<Env*> env,
+                                      MutableHandle<DebuggerEnvironment*> result);
 
     /*
      * Like cx->compartment()->wrap(cx, vp), but for the debugger compartment.
      *
      * Preconditions: *vp is a value from a debuggee compartment; cx is in the
      * debugger's compartment.
      *
      * If *vp is an object, this produces a (new or existing) Debugger.Object
@@ -1052,16 +1055,86 @@ class Debugger : private mozilla::Linked
      */
     JSObject* wrapWasmSource(JSContext* cx, Handle<WasmInstanceObject*> wasmInstance);
 
   private:
     Debugger(const Debugger&) = delete;
     Debugger & operator=(const Debugger&) = delete;
 };
 
+enum class DebuggerEnvironmentType {
+    Declarative,
+    With,
+    Object
+};
+
+class DebuggerEnvironment : public NativeObject
+{
+  public:
+    enum {
+        OWNER_SLOT
+    };
+
+    static const unsigned RESERVED_SLOTS = 1;
+
+    static const Class class_;
+
+    static NativeObject* initClass(JSContext* cx, HandleObject dbgCtor, HandleObject objProto);
+    static DebuggerEnvironment* create(JSContext* cx, HandleObject proto, HandleObject referent,
+                                       HandleNativeObject debugger);
+
+    DebuggerEnvironmentType type() const;
+    MOZ_MUST_USE bool getParent(JSContext* cx, MutableHandle<DebuggerEnvironment*> result) const;
+    MOZ_MUST_USE bool getObject(JSContext* cx, MutableHandle<DebuggerObject*> result) const;
+    MOZ_MUST_USE bool getCallee(JSContext* cx, MutableHandle<DebuggerObject*> result) const;
+    bool isDebuggee() const;
+    bool isOptimized() const;
+
+    static MOZ_MUST_USE bool getNames(JSContext* cx, Handle<DebuggerEnvironment*> environment,
+                                      MutableHandle<IdVector> result);
+    static MOZ_MUST_USE bool find(JSContext* cx, Handle<DebuggerEnvironment*> environment,
+                                  HandleId id, MutableHandle<DebuggerEnvironment*> result);
+    static MOZ_MUST_USE bool getVariable(JSContext* cx, Handle<DebuggerEnvironment*> environment,
+                                         HandleId id, MutableHandleValue result);
+    static MOZ_MUST_USE bool setVariable(JSContext* cx, Handle<DebuggerEnvironment*> environment,
+                                         HandleId id, HandleValue value);
+
+  private:
+    static const ClassOps classOps_;
+
+    static const JSPropertySpec properties_[];
+    static const JSFunctionSpec methods_[];
+
+    Env* referent() const {
+        Env* env = static_cast<Env*>(getPrivate());
+        MOZ_ASSERT(env);
+        return env;
+    }
+
+    Debugger* owner() const;
+
+    static DebuggerEnvironment* checkThis(JSContext* cx, const CallArgs& args, const char* fnname,
+                                          bool requireDebuggee);
+    bool requireDebuggee(JSContext* cx) const;
+
+    static MOZ_MUST_USE bool construct(JSContext* cx, unsigned argc, Value* vp);
+
+    static MOZ_MUST_USE bool typeGetter(JSContext* cx, unsigned argc, Value* vp);
+    static MOZ_MUST_USE bool parentGetter(JSContext* cx, unsigned argc, Value* vp);
+    static MOZ_MUST_USE bool objectGetter(JSContext* cx, unsigned argc, Value* vp);
+    static MOZ_MUST_USE bool calleeGetter(JSContext* cx, unsigned argc, Value* vp);
+    static MOZ_MUST_USE bool inspectableGetter(JSContext* cx, unsigned argc, Value* vp);
+    static MOZ_MUST_USE bool optimizedOutGetter(JSContext* cx, unsigned argc, Value* vp);
+
+    static MOZ_MUST_USE bool namesMethod(JSContext* cx, unsigned argc, Value* vp);
+    static MOZ_MUST_USE bool findMethod(JSContext* cx, unsigned argc, Value* vp);
+    static MOZ_MUST_USE bool getVariableMethod(JSContext* cx, unsigned argc, Value* vp);
+    static MOZ_MUST_USE bool setVariableMethod(JSContext* cx, unsigned argc, Value* vp);
+};
+
 class DebuggerObject : public NativeObject
 {
   public:
     static const Class class_;
 
     static NativeObject* initClass(JSContext* cx, HandleObject obj, HandleObject debugCtor);
     static DebuggerObject* create(JSContext* cx, HandleObject proto, HandleObject obj,
                                   HandleNativeObject debugger);