Initial support for Debug.Script instances. r=jorendorff, push=jorendorff.
authorJason Orendorff <jorendorff@mozilla.com>
Fri, 17 Jun 2011 16:28:32 -0500
changeset 74476 ac45a3dbfe3304dca3b9c9b0de4464f1e102a0b3
parent 74475 6885f4c66607709010eae5d47f39b6b22ee3f707
child 74477 e05d3da5866a594138b9678d12bc122a81551946
push id2
push userbsmedberg@mozilla.com
push dateFri, 19 Aug 2011 14:38:13 +0000
reviewersjorendorff
milestone7.0a1
Initial support for Debug.Script instances. r=jorendorff, push=jorendorff.
js/src/jit-test/tests/debug/Frame-script-01.js
js/src/jit-test/tests/debug/Frame-script-02.js
js/src/jit-test/tests/debug/Object-script.js
js/src/jit-test/tests/debug/Script-01.js
js/src/jit-test/tests/debug/Script-02.js
js/src/jit-test/tests/debug/Script-gc.js
js/src/js.msg
js/src/jsdbg.cpp
js/src/jsdbg.h
js/src/jsscript.cpp
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Frame-script-01.js
@@ -0,0 +1,36 @@
+// |jit-test| debug
+// Frame.prototype.script for eval frames.
+
+var g = newGlobal('new-compartment');
+var dbg = new Debug(g);
+
+// Apply |f| to each frame that is |skip| frames up from each frame that
+// executes a 'debugger' statement when evaluating |code| in the global g.
+function ApplyToFrameScript(code, skip, f) {
+    dbg.hooks = {
+        debuggerHandler: function (frame) {
+            while (skip-- > 0)
+                frame = frame.older;
+            assertEq(frame.type, "eval");
+            f(frame.script);
+        }
+    };
+    g.eval(code);
+}
+
+var savedScript;
+
+ApplyToFrameScript('debugger;', 0,
+                   function (script) {
+                       assertEq(script instanceof Debug.Script, true);
+                       assertEq(script.live, true);
+                       savedScript = script;
+                   });
+assertEq(savedScript.live, false);
+ApplyToFrameScript("(function () { eval('debugger;'); })();", 0,
+                   function (script) {
+                       assertEq(script instanceof Debug.Script, true);
+                       assertEq(script.live, true);
+                       savedScript = script;
+                   });
+assertEq(savedScript.live, false);
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Frame-script-02.js
@@ -0,0 +1,35 @@
+// |jit-test| debug
+// Frame.prototype.script for call frames.
+
+var g = newGlobal('new-compartment');
+var dbg = new Debug(g);
+
+// Apply |f| to each frame that is |skip| frames up from each frame that
+// executes a 'debugger' statement when evaluating |code| in the global g.
+function ApplyToFrameScript(code, skip, f) {
+    dbg.hooks = {
+        debuggerHandler: function (frame) {
+            while (skip-- > 0)
+                frame = frame.older;
+            assertEq(frame.type, "call");
+            f(frame.script);
+        }
+    };
+    g.eval(code);
+}
+
+var savedScript;
+
+ApplyToFrameScript('(function () { debugger; })();', 0,
+                   function (script) {
+                       assertEq(script instanceof Debug.Script, true);
+                       assertEq(script.live, true);
+                       savedScript = script;
+                   });
+assertEq(savedScript.live, true);
+
+// This would be nice, once we can get host call frames:
+// ApplyToFrameScript("(function () { debugger; }).call(null);", 1,
+//                    function (script) {
+//                        assertEq(script, null);
+//                   });
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Object-script.js
@@ -0,0 +1,17 @@
+// |jit-test| debug
+
+var g = newGlobal('new-compartment');
+var dbg = new Debug(g);
+var hits = 0;
+dbg.hooks = {
+    debuggerHandler: function (frame) {
+        var arr = frame.arguments;
+        assertEq(arr[0].script instanceof Debug.Script, true);
+        assertEq(arr[1].script, undefined);
+        assertEq(arr[2].script, undefined);
+        hits++;
+    }
+};
+
+g.eval("(function () { debugger; })(function g(){}, {}, Math.atan2);");
+assertEq(hits, 1);
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Script-01.js
@@ -0,0 +1,46 @@
+// |jit-test| debug
+// We get the same Debug.Script object instance each time we ask.
+
+var global = newGlobal('new-compartment');
+global.eval('function f() { debugger; }');
+global.eval('function g() { debugger; }');
+
+var debug = new Debug(global);
+
+function evalAndNoteScripts(prog) {
+    var scripts = {};
+    debug.hooks = {
+        debuggerHandler: function(frame) {
+            if (frame.type == "call")
+                assertEq(frame.script, frame.callee.script);
+            scripts.frame = frame.script;
+            if (frame.arguments[0])
+                scripts.argument = frame.arguments[0].script;
+        }
+    };
+    global.eval(prog);
+    return scripts;
+}
+
+// If we create a frame for a function and pass it as a value, those should
+// both yield the same Debug.Script instance.
+var scripts = evalAndNoteScripts('f(f)');
+assertEq(scripts.frame, scripts.argument);
+var fScript = scripts.argument;
+
+// If we call a second time, we should still get the same instance.
+scripts = evalAndNoteScripts('f(f)');
+assertEq(scripts.frame, fScript);
+assertEq(scripts.argument, fScript);
+
+// If we call with a different argument, we should get a different Debug.Script.
+scripts = evalAndNoteScripts('f(g)');
+assertEq(scripts.frame !== scripts.argument, true);
+assertEq(scripts.frame, fScript);
+var gScript = scripts.argument;
+
+// See if we can get g via the frame.
+scripts = evalAndNoteScripts('g(f)');
+assertEq(scripts.frame !== scripts.argument, true);
+assertEq(scripts.frame,    gScript);
+assertEq(scripts.argument, fScript);
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Script-02.js
@@ -0,0 +1,7 @@
+// |jit-test| debug
+// Debug.Script throws when applied as a constructor.
+
+load(libdir + 'asserts.js');
+
+assertThrowsInstanceOf(function() { Debug.Script(); }, TypeError);
+assertThrowsInstanceOf(function() { new Debug.Script(); }, TypeError);
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/debug/Script-gc.js
@@ -0,0 +1,31 @@
+// |jit-test| debug
+// The heldScripts table keeps Debug.Script instances with live referents alive.
+
+var N = 4;
+var g = newGlobal('new-compartment');
+var dbg = new Debug(g);
+var i;
+dbg.hooks = {
+    debuggerHandler: function (frame) {
+	assertEq(frame.script instanceof Debug.Script, true);
+        frame.script.id = i;
+    }
+};
+
+g.eval('var arr = [];')
+for (i = 0; i < N; i++)  // loop to defeat conservative GC
+    g.eval("arr.push(function () { debugger }); arr[arr.length - 1]();");
+
+gc();
+
+var hits;
+dbg.hooks = {
+    debuggerHandler: function (frame) {
+        hits++;
+        assertEq(frame.script.id, i);
+    }
+};
+hits = 0;
+for (i = 0; i < N; i++)
+    g.arr[i]();
+assertEq(hits, N);
--- a/js/src/js.msg
+++ b/js/src/js.msg
@@ -348,14 +348,14 @@ MSG_DEF(JSMSG_CANT_CLONE_OBJECT,      26
 MSG_DEF(JSMSG_NON_NATIVE_SCOPE,       266, 0, JSEXN_TYPEERR, "non-native scope object")
 MSG_DEF(JSMSG_STRICT_FUNCTION_STATEMENT, 267, 0, JSEXN_SYNTAXERR, "in strict mode code, functions may be declared only at top level or immediately within another function")
 MSG_DEF(JSMSG_INVALID_FOR_IN_INIT,    268, 0, JSEXN_SYNTAXERR, "for-in loop let declaration may not have an initializer")
 MSG_DEF(JSMSG_CLEARED_SCOPE,          269, 0, JSEXN_TYPEERR, "attempt to run compile-and-go script on a cleared scope")
 MSG_DEF(JSMSG_MALFORMED_ESCAPE,       270, 1, JSEXN_SYNTAXERR, "malformed {0} character escape sequence")
 MSG_DEF(JSMSG_CCW_REQUIRED,           271, 1, JSEXN_TYPEERR, "{0}: argument must be an object from a different compartment")
 MSG_DEF(JSMSG_DEBUG_BAD_RESUMPTION,   272, 0, JSEXN_TYPEERR, "debugger resumption value must be undefined, {throw: val}, {return: val}, or null")
 MSG_DEF(JSMSG_ASSIGN_FUNCTION_OR_NULL, 273, 1, JSEXN_TYPEERR, "value assigned to {0} must be a function or null")
-MSG_DEF(JSMSG_DEBUG_FRAME_NOT_LIVE,   274, 0, JSEXN_ERR, "stack frame is not live")
+MSG_DEF(JSMSG_DEBUG_NOT_LIVE,         274, 1, JSEXN_ERR, "{0} is not live")
 MSG_DEF(JSMSG_DEBUG_STREAMS_CROSSED,  275, 0, JSEXN_INTERNALERR, "the debuggee has a cross-compartment wrapper of an object in the debugger compartment; can't create a Debug.Object with that wrapper as its referent")
 MSG_DEF(JSMSG_DEBUG_OBJECT_WRONG_OWNER, 276, 0, JSEXN_TYPEERR, "Debug.Object belongs to a different Debug")
 MSG_DEF(JSMSG_DEBUG_OBJECT_PROTO,     277, 0, JSEXN_TYPEERR, "Debug.Object.prototype is not a valid Debug.Object")
 MSG_DEF(JSMSG_DEBUG_LOOP,             278, 0, JSEXN_TYPEERR, "cannot debug an object in same compartment as debugger or a compartment that is already debugging the debugger")
 MSG_DEF(JSMSG_DEBUG_NOT_IDLE,         279, 0, JSEXN_ERR, "cannot start debugging: a debuggee script is on the stack")
--- a/js/src/jsdbg.cpp
+++ b/js/src/jsdbg.cpp
@@ -73,16 +73,24 @@ enum {
 extern Class DebugObject_class;
 
 enum {
     JSSLOT_DEBUGOBJECT_OWNER,
     JSSLOT_DEBUGOBJECT_CCW,  // cross-compartment wrapper
     JSSLOT_DEBUGOBJECT_COUNT
 };
 
+extern Class DebugScript_class;
+
+enum {
+    JSSLOT_DEBUGSCRIPT_OWNER,
+    JSSLOT_DEBUGSCRIPT_HOLDER, // cross-compartment wrapper
+    JSSLOT_DEBUGSCRIPT_COUNT
+};
+
 
 // === Utils
 
 bool
 ReportMoreArgsNeeded(JSContext *cx, const char *name, uintN required)
 {
     JS_ASSERT(required > 0);
     JS_ASSERT(required <= 10);
@@ -140,22 +148,24 @@ CheckThisClass(JSContext *cx, Value *vp,
     js::classname *private = (classname *) thisobj->getPrivate();
 
 
 // === Debug hook dispatch
 
 enum {
     JSSLOT_DEBUG_FRAME_PROTO,
     JSSLOT_DEBUG_OBJECT_PROTO,
+    JSSLOT_DEBUG_SCRIPT_PROTO,
     JSSLOT_DEBUG_COUNT
 };
 
 Debug::Debug(JSObject *dbg, JSObject *hooks)
   : object(dbg), hooksObject(hooks), uncaughtExceptionHook(NULL), enabled(true),
-    hasDebuggerHandler(false), hasThrowHandler(false), objects(dbg->compartment()->rt)
+    hasDebuggerHandler(false), hasThrowHandler(false),
+    objects(dbg->compartment()->rt), heldScripts(dbg->compartment()->rt)
 {
     // This always happens within a request on some cx.
     JSRuntime *rt = dbg->compartment()->rt;
     AutoLockGC lock(rt);
     JS_APPEND_LINK(&link, &rt->debuggerList);
 }
 
 Debug::~Debug()
@@ -165,28 +175,35 @@ Debug::~Debug()
     // This always happens in the GC thread, so no locking is required.
     JS_ASSERT(object->compartment()->rt->gcRunning);
     JS_REMOVE_LINK(&link);
 }
 
 bool
 Debug::init(JSContext *cx)
 {
-    bool ok = frames.init() && objects.init() && debuggees.init();
+    bool ok = (frames.init() &&
+               objects.init() && 
+               debuggees.init() && 
+               heldScripts.init() &&
+               evalScripts.init());
     if (!ok)
         js_ReportOutOfMemory(cx);
     return ok;
 }
 
 JS_STATIC_ASSERT(uintN(JSSLOT_DEBUGFRAME_OWNER) == uintN(JSSLOT_DEBUGOBJECT_OWNER));
+JS_STATIC_ASSERT(uintN(JSSLOT_DEBUGFRAME_OWNER) == uintN(JSSLOT_DEBUGSCRIPT_OWNER));
 
 Debug *
 Debug::fromChildJSObject(JSObject *obj)
 {
-    JS_ASSERT(obj->clasp == &DebugFrame_class || obj->clasp == &DebugObject_class);
+    JS_ASSERT(obj->clasp == &DebugFrame_class ||
+              obj->clasp == &DebugObject_class ||
+              obj->clasp == &DebugScript_class);
     JSObject *dbgobj = &obj->getReservedSlot(JSSLOT_DEBUGOBJECT_OWNER).toObject();
     return fromJSObject(dbgobj);
 }
 
 bool
 Debug::getScriptFrame(JSContext *cx, StackFrame *fp, Value *vp)
 {
     JS_ASSERT(fp->isScriptFrame());
@@ -582,16 +599,30 @@ Debug::trace(JSTracer *trc)
     for (FrameMap::Range r = frames.all(); !r.empty(); r.popFront()) {
         JSObject *frameobj = r.front().value;
         JS_ASSERT(frameobj->getPrivate());
         MarkObject(trc, *frameobj, "live Debug.Frame");
     }
 
     // Trace the referent -> Debug.Object weak map.
     objects.trace(trc);
+
+    // Trace the weak map from JSFunctions and "Script" JSObjects to
+    // Debug.Script objects.
+    heldScripts.trace(trc);
+
+    // Trace the map for eval scripts, which are explicitly freed.
+    for (ScriptMap::Range r = evalScripts.all(); !r.empty(); r.popFront()) {
+        JSObject *scriptobj = r.front().value;
+
+        // evalScripts should only refer to Debug.Script objects for
+        // scripts that haven't been freed yet.
+        JS_ASSERT(scriptobj->getPrivate());
+        MarkObject(trc, *scriptobj, "live eval Debug.Script");
+    }
 }
 
 void
 Debug::sweepAll(JSContext *cx)
 {
     JSRuntime *rt = cx->runtime;
     for (JSCList *p = &rt->debuggerList; (p = JS_NEXT_LINK(p)) != &rt->debuggerList;) {
         Debug *dbg = (Debug *) ((unsigned char *) p - offsetof(Debug, link));
@@ -862,17 +893,17 @@ Debug::construct(JSContext *cx, uintN ar
     Value v;
     jsid prototypeId = ATOM_TO_JSID(cx->runtime->atomState.classPrototypeAtom);
     if (!vp[0].toObject().getProperty(cx, prototypeId, &v))
         return false;
     JSObject *proto = &v.toObject();
     JS_ASSERT(proto->getClass() == &Debug::jsclass);
 
     // Make the new Debug object. Each one has a reference to
-    // Debug.{Frame,Object}.prototype in reserved slots.
+    // Debug.{Frame,Object,Script}.prototype in reserved slots.
     JSObject *obj = NewNonFunction<WithProto::Given>(cx, &Debug::jsclass, proto, NULL);
     if (!obj || !obj->ensureClassReservedSlots(cx))
         return false;
     for (uintN slot = JSSLOT_DEBUG_FRAME_PROTO; slot < JSSLOT_DEBUG_COUNT; slot++)
         obj->setReservedSlot(slot, proto->getReservedSlot(slot));
 
     JSObject *hooks = NewBuiltinClassInstance(cx, &js_ObjectClass);
     if (!hooks)
@@ -1019,16 +1050,230 @@ JSFunctionSpec Debug::methods[] = {
     JS_FN("removeDebuggee", Debug::removeDebuggee, 1, 0),
     JS_FN("hasDebuggee", Debug::hasDebuggee, 1, 0),
     JS_FN("getDebuggees", Debug::getDebuggees, 0, 0),
     JS_FN("getYoungestFrame", Debug::getYoungestFrame, 0, 0),
     JS_FS_END
 };
 
 
+// === Debug.Script
+
+// JSScripts' lifetimes fall into to two categories:
+//
+// - "Held scripts": JSScripts belonging to JSFunctions and JSScripts created using JSAPI
+//   have lifetimes determined by the garbage collector. A JSScript itself has no mark bit
+//   of its own. Instead, its holding object manages the JSScript as part of its own
+//   structure: the holder has a mark bit; when the holder is marked it calls
+//   js_TraceScript on its JSScript; and when the holder is freed it explicitly frees its
+//   JSScript.
+//
+//   Debug.Script instances for held scripts are strong references to the holder (and thus
+//   to the script). Debug::heldScripts weakly maps CCWs for holding objects to the
+//   Debug.Script objects for their JSScripts. We needn't act on a destroyScript event for
+//   a held script: if we get such an event we know its Debug.Script is dead anyway, and
+//   its entry in Debug::heldScripts will be cleaned up by the standard weak table code.
+//
+// - "Eval scripts": JSScripts generated temporarily for a call to 'eval' or a related
+//   function live until the call completes, at which point the script is destroyed.
+//
+//   A Debug.Script instance for an eval script has no influence on the JSScript's
+//   lifetime. Debug::evalScripts maps live JSScripts to to their Debug.Script objects.
+//   When a destroyScript event tells us that an eval script is dead, we remove its table
+//   entry, and clear its Debug.Script object's script pointer, thus marking it dead.
+//
+// A Debug.Script's private pointer points directly to the JSScript, or is NULL if the
+// Debug.Script is dead. The JSSLOT_DEBUGSCRIPT_HOLDER slot refers to a CCW for the
+// holding object, or is null for eval-like JSScripts. The private pointer is not traced;
+// the holding object reference is traced, if present.
+//
+// (We consider a script saved in and retrieved from the eval cache to have been
+// destroyed, and then --- mirabile dictu --- re-created at the same address. The
+// newScriptHook and destroyScriptHook hooks cooperate with this view.)
+
+Class DebugScript_class = {
+    "Script", JSCLASS_HAS_PRIVATE | JSCLASS_HAS_RESERVED_SLOTS(JSSLOT_DEBUGSCRIPT_COUNT),
+    PropertyStub, PropertyStub, PropertyStub, StrictPropertyStub,
+    EnumerateStub, ResolveStub, ConvertStub
+};
+
+static inline JSScript *GetScriptReferent(JSObject *obj) {
+    JS_ASSERT(obj->getClass() == &DebugScript_class);
+    return (JSScript *) obj->getPrivate();
+}
+
+static inline void ClearScriptReferent(JSObject *obj) {
+    JS_ASSERT(obj->getClass() == &DebugScript_class);
+    obj->setPrivate(NULL);
+}
+
+static inline JSObject *GetScriptHolder(JSObject *obj) {
+    JS_ASSERT(obj->getClass() == &DebugScript_class);
+    return obj->getReservedSlot(JSSLOT_DEBUGSCRIPT_HOLDER).toObjectOrNull();
+}
+
+JSObject *
+Debug::newDebugScript(JSContext *cx, JSScript *script, JSObject *holder)
+{
+    JSObject *proto = &object->getReservedSlot(JSSLOT_DEBUG_SCRIPT_PROTO).toObject();
+    JS_ASSERT(proto);
+    JSObject *scriptobj = NewNonFunction<WithProto::Given>(cx, &DebugScript_class, proto, NULL);
+    if (!scriptobj || !scriptobj->ensureClassReservedSlots(cx))
+        return false;
+    scriptobj->setPrivate(script);
+    scriptobj->setReservedSlot(JSSLOT_DEBUGSCRIPT_OWNER, ObjectValue(*object));
+    scriptobj->setReservedSlot(JSSLOT_DEBUGSCRIPT_HOLDER, ObjectOrNullValue(holder));
+
+    return scriptobj;
+}
+
+JSObject *
+Debug::wrapHeldScript(JSContext *cx, JSScript *script, JSObject *obj)
+{
+    assertSameCompartment(cx, object);
+
+    // Our argument must be in a debuggee compartment; find its CCW, for use as the key in
+    // the table.
+    JS_ASSERT(cx->compartment != obj->compartment());
+    if (!cx->compartment->wrap(cx, &obj))
+        return NULL;
+
+    ScriptWeakMap::AddPtr p = heldScripts.lookupForAdd(obj);
+    if (!p) {
+        JSObject *scriptobj = newDebugScript(cx, script, obj);
+        // The allocation may have caused a GC, which can remove table entries.
+        if (!scriptobj || !heldScripts.relookupOrAdd(p, obj, scriptobj))
+            return NULL;
+    }
+
+    JS_ASSERT(GetScriptReferent(p->value) == script);
+    return p->value;
+}
+
+JSObject *
+Debug::wrapFunctionScript(JSContext *cx, JSFunction *fun)
+{
+    return wrapHeldScript(cx, fun->script(), fun);
+}
+
+JSObject *
+Debug::wrapJSAPIScript(JSContext *cx, JSObject *obj)
+{
+    JS_ASSERT(obj->isScript());
+    return wrapHeldScript(cx, obj->getScript(), obj);
+}
+
+JSObject *
+Debug::wrapEvalScript(JSContext *cx, JSScript *script)
+{
+    JS_ASSERT(cx->compartment != script->compartment);
+    ScriptMap::AddPtr p = evalScripts.lookupForAdd(script);
+    if (!p) {
+        JSObject *scriptobj = newDebugScript(cx, script, NULL);
+        // The allocation may have caused a GC, which can remove table entries.
+        if (!scriptobj || !evalScripts.relookupOrAdd(p, script, scriptobj))
+            return NULL;
+    }
+
+    JS_ASSERT(GetScriptReferent(p->value) == script);
+    return p->value;
+}
+
+void
+Debug::slowPathOnDestroyScript(JSScript *script)
+{
+    // Find all debuggers that might have Debug.Script referring to this script.
+    js::GlobalObjectSet *debuggees = &script->compartment->getDebuggees();
+    for (GlobalObjectSet::Range r = debuggees->all(); !r.empty(); r.popFront()) {
+        GlobalObject::DebugVector *debuggers = r.front()->getDebuggers();
+        for (Debug **p = debuggers->begin(); p != debuggers->end(); p++)
+            (*p)->destroyEvalScript(script);
+    }
+}
+
+void
+Debug::destroyEvalScript(JSScript *script)
+{
+    ScriptMap::Ptr p = evalScripts.lookup(script);
+    if (p) {
+        JS_ASSERT(GetScriptReferent(p->value) == script);
+        ClearScriptReferent(p->value);
+        evalScripts.remove(p);
+    }
+}
+
+static JSObject *
+DebugScript_checkThis(JSContext *cx, Value *vp, const char *fnname, bool checkLive)
+{
+    if (!vp[1].isObject()) {
+        ReportObjectRequired(cx);
+        return NULL;
+    }
+    JSObject *thisobj = &vp[1].toObject();
+    if (thisobj->clasp != &DebugScript_class) {
+        JS_ReportErrorNumber(cx, js_GetErrorMessage, NULL, JSMSG_INCOMPATIBLE_PROTO,
+                             "Debug.Script", fnname, thisobj->getClass()->name);
+        return NULL;
+    }
+
+    // Check for Debug.Script.prototype, which is of class DebugScript_class
+    // but whose holding object is undefined.
+    if (thisobj->getReservedSlot(JSSLOT_DEBUGSCRIPT_HOLDER).isUndefined()) {
+        JS_ReportErrorNumber(cx, js_GetErrorMessage, NULL, JSMSG_INCOMPATIBLE_PROTO,
+                             "Debug.Script", fnname, "prototype object");
+        return NULL;
+    }
+
+    if (checkLive && !GetScriptReferent(thisobj)) {
+        JS_ReportErrorNumber(cx, js_GetErrorMessage, NULL, JSMSG_DEBUG_NOT_LIVE,
+                             "Debug.Script", fnname, "script");
+        return NULL;
+    }
+    
+    return thisobj;
+}
+
+#define THIS_DEBUGSCRIPT_SCRIPT_NEEDLIVE(cx, vp, fnname, obj, script, checkLive)    \
+    JSObject *obj = DebugScript_checkThis(cx, vp, fnname, checkLive);               \
+    if (!obj)                                                                       \
+        return false;                                                               \
+    JSScript *script = GetScriptReferent(obj)
+
+#define THIS_DEBUGSCRIPT_SCRIPT(cx, vp, fnname, obj, script)                  \
+    THIS_DEBUGSCRIPT_SCRIPT_NEEDLIVE(cx, vp, fnname, obj, script, false)
+#define THIS_DEBUGSCRIPT_LIVE_SCRIPT(cx, vp, fnname, obj, script)             \
+    THIS_DEBUGSCRIPT_SCRIPT_NEEDLIVE(cx, vp, fnname, obj, script, true)
+
+
+static JSBool
+DebugScript_getLive(JSContext *cx, uintN argc, Value *vp)
+{
+    THIS_DEBUGSCRIPT_SCRIPT(cx, vp, "get live", obj, script);
+    vp->setBoolean(!!script);
+    return true;
+}
+
+static JSBool
+DebugScript_construct(JSContext *cx, uintN argc, Value *vp)
+{
+    JS_ReportErrorNumber(cx, js_GetErrorMessage, NULL, JSMSG_NO_CONSTRUCTOR, "Debug.Script");
+    return false;
+}
+
+static JSPropertySpec DebugScript_properties[] = {
+    JS_PSG("live", DebugScript_getLive, 0),
+    JS_PS_END
+};
+
+static JSFunctionSpec DebugScript_methods[] = {
+//    JS_FN("getOffsetLine", DebugScript_getOffsetLine, 0, 0),
+    JS_FS_END
+};
+
+
 // === Debug.Frame
 
 Class DebugFrame_class = {
     "Frame", JSCLASS_HAS_PRIVATE | JSCLASS_HAS_RESERVED_SLOTS(JSSLOT_DEBUGFRAME_COUNT),
     PropertyStub, PropertyStub, PropertyStub, StrictPropertyStub,
     EnumerateStub, ResolveStub, ConvertStub
 };
 
@@ -1051,18 +1296,18 @@ CheckThisFrame(JSContext *cx, Value *vp,
     // distinguished by having a NULL private value. Also, forbid popped frames.
     if (!thisobj->getPrivate()) {
         if (thisobj->getReservedSlot(JSSLOT_DEBUGFRAME_OWNER).isUndefined()) {
             JS_ReportErrorNumber(cx, js_GetErrorMessage, NULL, JSMSG_INCOMPATIBLE_PROTO,
                                  "Debug.Frame", fnname, "prototype object");
             return NULL;
         }
         if (checkLive) {
-            JS_ReportErrorNumber(cx, js_GetErrorMessage, NULL, JSMSG_DEBUG_FRAME_NOT_LIVE,
-                                 "Debug.Frame", fnname);
+            JS_ReportErrorNumber(cx, js_GetErrorMessage, NULL, JSMSG_DEBUG_NOT_LIVE,
+                                 "Debug.Frame", fnname, "stack frame");
             return NULL;
         }
     }
     return thisobj;
 }
 
 #if DEBUG
 static bool
@@ -1239,16 +1484,55 @@ DebugFrame_getArguments(JSContext *cx, u
         argsobj = NULL;
     }
     *vp = ObjectOrNullValue(argsobj);
     thisobj->setReservedSlot(JSSLOT_DEBUGFRAME_ARGUMENTS, *vp);
     return true;
 }
 
 static JSBool
+DebugFrame_getScript(JSContext *cx, uintN argc, Value *vp)
+{
+    THIS_FRAME(cx, vp, "get script", thisobj, fp);
+    Debug *debug = Debug::fromChildJSObject(thisobj);
+
+    JSObject *scriptObject = NULL;
+    if (fp->isFunctionFrame() && !fp->isEvalFrame()) {
+        JSFunction *callee = fp->callee().getFunctionPrivate();
+        if (callee->isInterpreted()) {
+            scriptObject = debug->wrapFunctionScript(cx, callee);
+            if (!scriptObject)
+                return false;
+        }
+    } else if (fp->isScriptFrame()) {
+        JSScript *script = fp->script();
+        // Both calling a JSAPI script object (via JS_ExecuteScript, say) and
+        // calling 'eval' create non-function script frames. However, scripts
+        // for the former are held by script objects, and must go in
+        // heldScripts, whereas scripts for the latter are explicitly destroyed
+        // when the call returns, and must go in evalScripts. Distinguish the
+        // two cases by checking whether the script has a Script object
+        // allocated to it.
+        if (script->u.object) {
+            JS_ASSERT(!fp->isEvalFrame());
+            scriptObject = debug->wrapJSAPIScript(cx, script->u.object);
+            if (!scriptObject)
+                return false;
+        } else {
+            JS_ASSERT(fp->isEvalFrame());
+            scriptObject = debug->wrapEvalScript(cx, script);
+            if (!scriptObject)
+                return false;
+        }
+    }
+    vp->setObjectOrNull(scriptObject);
+    return true;
+}
+
+static JSBool
 DebugFrame_getLive(JSContext *cx, uintN argc, Value *vp)
 {
     JSObject *thisobj = CheckThisFrame(cx, vp, "get live", false);
     if (!thisobj)
         return false;
     StackFrame *fp = (StackFrame *) thisobj->getPrivate();
     vp->setBoolean(!!fp);
     return true;
@@ -1375,40 +1659,41 @@ DebugFrame_evalWithBindings(JSContext *c
 static JSBool
 DebugFrame_construct(JSContext *cx, uintN argc, Value *vp)
 {
     JS_ReportErrorNumber(cx, js_GetErrorMessage, NULL, JSMSG_NO_CONSTRUCTOR, "Debug.Frame");
     return false;
 }
 
 static JSPropertySpec DebugFrame_properties[] = {
-    JS_PSG("type", DebugFrame_getType, 0),
-    JS_PSG("this", DebugFrame_getThis, 0),
-    JS_PSG("older", DebugFrame_getOlder, 0),
+    JS_PSG("arguments", DebugFrame_getArguments, 0),
+    JS_PSG("callee", DebugFrame_getCallee, 0),
+    JS_PSG("constructing", DebugFrame_getConstructing, 0),
+    JS_PSG("generator", DebugFrame_getGenerator, 0),
     JS_PSG("live", DebugFrame_getLive, 0),
-    JS_PSG("callee", DebugFrame_getCallee, 0),
-    JS_PSG("generator", DebugFrame_getGenerator, 0),
-    JS_PSG("constructing", DebugFrame_getConstructing, 0),
-    JS_PSG("arguments", DebugFrame_getArguments, 0),
+    JS_PSG("older", DebugFrame_getOlder, 0),
+    JS_PSG("script", DebugFrame_getScript, 0),
+    JS_PSG("this", DebugFrame_getThis, 0),
+    JS_PSG("type", DebugFrame_getType, 0),
     JS_PS_END
 };
 
 static JSFunctionSpec DebugFrame_methods[] = {
     JS_FN("eval", DebugFrame_eval, 1, 0),
     JS_FN("evalWithBindings", DebugFrame_evalWithBindings, 1, 0),
     JS_FS_END
 };
 
 
 // === Debug.Object
 
 Class DebugObject_class = {
     "Object", JSCLASS_HAS_PRIVATE | JSCLASS_HAS_RESERVED_SLOTS(JSSLOT_DEBUGOBJECT_COUNT),
     PropertyStub, PropertyStub, PropertyStub, StrictPropertyStub,
-    EnumerateStub, ResolveStub, ConvertStub, FinalizeStub,
+    EnumerateStub, ResolveStub, ConvertStub
 };
 
 static JSObject *
 DebugObject_checkThis(JSContext *cx, Value *vp, const char *fnname)
 {
     if (!vp[1].isObject()) {
         ReportObjectRequired(cx);
         return NULL;
@@ -1540,16 +1825,38 @@ DebugObject_getParameterNames(JSContext 
         for (size_t i = 0; i < fun->nargs; i++)
             result->addressOfDenseArrayElement(i)->setUndefined();
     }
 
     vp->setObject(*result);
     return true;
 }
 
+static JSBool
+DebugObject_getScript(JSContext *cx, uintN argc, Value *vp)
+{
+    THIS_DEBUGOBJECT_OWNER_REFERENT(cx, vp, "get script", dbg, obj);
+
+    vp->setUndefined();
+
+    if (!obj->isFunction())
+        return true;
+
+    JSFunction *fun = obj->getFunctionPrivate();
+    if (!fun->isInterpreted())
+        return true;
+
+    JSObject *scriptObject = dbg->wrapFunctionScript(cx, fun);
+    if (!scriptObject)
+        return false;
+
+    vp->setObject(*scriptObject);
+    return true;
+}
+
 enum ApplyOrCallMode { ApplyMode, CallMode };
 
 static JSBool
 ApplyOrCall(JSContext *cx, uintN argc, Value *vp, ApplyOrCallMode mode)
 {
     THIS_DEBUGOBJECT_OWNER_REFERENT(cx, vp, "apply", dbg, obj);
 
     // Any JS exceptions thrown must be in the debugger compartment, so do
@@ -1622,16 +1929,17 @@ DebugObject_call(JSContext *cx, uintN ar
 }
 
 static JSPropertySpec DebugObject_properties[] = {
     JS_PSG("proto", DebugObject_getProto, 0),
     JS_PSG("class", DebugObject_getClass, 0),
     JS_PSG("callable", DebugObject_getCallable, 0),
     JS_PSG("name", DebugObject_getName, 0),
     JS_PSG("parameterNames", DebugObject_getParameterNames, 0),
+    JS_PSG("script", DebugObject_getScript, 0),
     JS_PS_END
 };
 
 static JSFunctionSpec DebugObject_methods[] = {
     JS_FN("apply", DebugObject_apply, 0, 0),
     JS_FN("call", DebugObject_call, 0, 0),
     JS_FS_END
 };
@@ -1655,18 +1963,27 @@ JS_DefineDebugObject(JSContext *cx, JSOb
     JSObject *frameCtor;
     JSObject *frameProto = js_InitClass(cx, debugCtor, objProto, &DebugFrame_class,
                                         DebugFrame_construct, 0,
                                         DebugFrame_properties, DebugFrame_methods, NULL, NULL,
                                         &frameCtor);
     if (!frameProto)
         return false;
 
+    JSObject *scriptCtor;
+    JSObject *scriptProto = js_InitClass(cx, debugCtor, objProto, &DebugScript_class,
+                                         DebugScript_construct, 0,
+                                         DebugScript_properties, DebugScript_methods, NULL, NULL,
+                                         &scriptCtor);
+    if (!scriptProto || !scriptProto->ensureClassReservedSlots(cx))
+        return false;
+
     JSObject *objectProto = js_InitClass(cx, debugCtor, objProto, &DebugObject_class,
                                          DebugObject_construct, 0,
                                          DebugObject_properties, DebugObject_methods, NULL, NULL);
     if (!objectProto)
         return false;
 
     debugProto->setReservedSlot(JSSLOT_DEBUG_FRAME_PROTO, ObjectValue(*frameProto));
     debugProto->setReservedSlot(JSSLOT_DEBUG_OBJECT_PROTO, ObjectValue(*objectProto));
+    debugProto->setReservedSlot(JSSLOT_DEBUG_SCRIPT_PROTO, ObjectValue(*scriptProto));
     return true;
 }
--- a/js/src/jsdbg.h
+++ b/js/src/jsdbg.h
@@ -71,47 +71,69 @@ class Debug {
 
     // Weak references to stack frames that are currently on the stack and thus
     // necessarily alive. We drop them as soon as they leave the stack (see
     // slowPathLeaveStackFrame) and in removeDebuggee.
     typedef HashMap<StackFrame *, JSObject *, DefaultHasher<StackFrame *>, SystemAllocPolicy>
         FrameMap;
     FrameMap frames;
 
-    // Mark policy for ObjectMap.
-    class ObjectMapMarkPolicy: public DefaultMarkPolicy<JSObject *, JSObject *> {
+    // Mark policy for weak maps where the keys are CCWs, but the liveness of the entry
+    // depends on the CCW's *referent's* markedness, not the CCW itself. This policy is
+    // only usable when marking an entry's value eventually marks the key. These
+    // properties hold for ObjectWeakMap and ScriptWeakMap.
+    class CCWReferentKeyMarkPolicy: public DefaultMarkPolicy<JSObject *, JSObject *> {
         typedef DefaultMarkPolicy<JSObject *, JSObject *> Base;
       public:
-        explicit ObjectMapMarkPolicy(JSTracer *tracer) : Base(tracer) { }
-
+        explicit CCWReferentKeyMarkPolicy(JSTracer *tracer) : Base(tracer) { }
         // The unwrap() call here means that we use the *referent's* mark, not that of the
         // CCW itself, to decide whether the table entry is live. This seems weird: if the
         // CCW is not marked, and the referent is, won't we end up keeping the table entry
-        // but GC'ing its key? But it's okay: the Debug.Object always refers to the CCW,
-        // so marking the value marks the CCW.
+        // but GC'ing its key? But it's okay: the value always refers to the CCW, so
+        // marking the value marks the CCW.
         bool keyMarked(JSObject *k) { 
             JS_ASSERT(k->isCrossCompartmentWrapper());
             return k->unwrap()->isMarked(); 
         }
         void markKey(JSObject *k, const char *description) {
             js::gc::MarkObject(tracer, *k->unwrap(), description);
         }
     };
 
     // The map from debuggee objects to their Debug.Object instances. However, to avoid
     // holding cross-compartment references directly, the keys in this map are the
     // referents' CCWs, not the referents themselves. Thus, to find the Debug.Object for a
     // debuggee object, you must first find its CCW, and then look that up here.
     //
     // Using CCWs for keys when it's really their referents' liveness that determines the
-    // table entry's liveness is delicate; see comments on ObjectMapMarkPolicy.
-    typedef WeakMap<JSObject *, JSObject *, DefaultHasher<JSObject *>, ObjectMapMarkPolicy>
+    // table entry's liveness is delicate; see comments on CCWReferentKeyMarkPolicy.
+    typedef WeakMap<JSObject *, JSObject *, DefaultHasher<JSObject *>, CCWReferentKeyMarkPolicy>
         ObjectWeakMap;
     ObjectWeakMap objects;
 
+    // An ephemeral map from JSObject CCWs to Debug.Script instances.
+    typedef WeakMap<JSObject *, JSObject *, DefaultHasher<JSObject *>, CCWReferentKeyMarkPolicy>
+        ScriptWeakMap;
+
+    // Map of Debug.Script instances for garbage-collected JSScripts. For function
+    // scripts, the key is the compiler-created, internal JSFunction; for scripts returned
+    // by JSAPI functions, the key is the "Script"-class JSObject.
+    ScriptWeakMap heldScripts;
+
+    // An ordinary (non-ephemeral) map from JSScripts to Debug.Script instances, for eval
+    // scripts that are explicitly freed.
+    typedef HashMap<JSScript *, JSObject *, DefaultHasher<JSScript *>, SystemAllocPolicy>
+        ScriptMap;
+
+    // Map from eval JSScripts to their Debug.Script objects. "Eval scripts" are scripts
+    // created for 'eval' and similar calls that are explicitly destroyed when the call
+    // returns. Debug.Script objects are not strong references to such JSScripts; the
+    // Debug.Script becomes "dead" when the eval call returns.
+    ScriptMap evalScripts;
+
     bool addDebuggeeGlobal(JSContext *cx, GlobalObject *obj);
     void removeDebuggeeGlobal(JSContext *cx, GlobalObject *global,
                               GlobalObjectSet::Enum *compartmentEnum,
                               GlobalObjectSet::Enum *debugEnum);
 
     JSTrapStatus handleUncaughtException(AutoCompartment &ac, Value *vp, bool callHook);
     JSTrapStatus parseResumptionValue(AutoCompartment &ac, bool ok, const Value &rv, Value *vp,
                                       bool callHook = true);
@@ -135,29 +157,41 @@ class Debug {
     static JSBool getYoungestFrame(JSContext *cx, uintN argc, Value *vp);
     static JSBool construct(JSContext *cx, uintN argc, Value *vp);
     static JSPropertySpec properties[];
     static JSFunctionSpec methods[];
 
     inline bool hasAnyLiveHooks() const;
 
     static void slowPathLeaveStackFrame(JSContext *cx);
+    static void slowPathOnDestroyScript(JSScript *script);
 
     typedef bool (Debug::*DebugObservesMethod)() const;
     typedef JSTrapStatus (Debug::*DebugHandleMethod)(JSContext *, Value *) const;
     static JSTrapStatus dispatchHook(JSContext *cx, js::Value *vp,
                                      DebugObservesMethod observesEvent,
                                      DebugHandleMethod handleEvent);
 
     bool observesDebuggerStatement() const;
     JSTrapStatus handleDebuggerStatement(JSContext *cx, Value *vp);
 
     bool observesThrow() const;
     JSTrapStatus handleThrow(JSContext *cx, Value *vp);
 
+    // Allocate and initialize a Debug.Script instance whose referent is |script| and
+    // whose holder is |obj|. If |obj| is NULL, this creates a Debug.Script whose holder
+    // is null, for eval scripts.
+    JSObject *newDebugScript(JSContext *cx, JSScript *script, JSObject *obj);
+
+    // Helper function for wrapFunctionScript and wrapJSAPIscript.
+    JSObject *wrapHeldScript(JSContext *cx, JSScript *script, JSObject *obj);
+
+    // Remove script from our table of eval scripts.
+    void destroyEvalScript(JSScript *script);
+
   public:
     Debug(JSObject *dbg, JSObject *hooks);
     ~Debug();
 
     bool init(JSContext *cx);
     inline JSObject *toJSObject() const;
     static inline Debug *fromJSObject(JSObject *obj);
     static Debug *fromChildJSObject(JSObject *obj);
@@ -182,16 +216,17 @@ class Debug {
     static void sweepAll(JSContext *cx);
     static void sweepCompartment(JSContext *cx, JSCompartment *compartment);
     static void detachAllDebuggersFromGlobal(JSContext *cx, GlobalObject *global,
                                              GlobalObjectSet::Enum *compartmentEnum);
 
     static inline void leaveStackFrame(JSContext *cx);
     static inline JSTrapStatus onDebuggerStatement(JSContext *cx, js::Value *vp);
     static inline JSTrapStatus onThrow(JSContext *cx, js::Value *vp);
+    static inline void onDestroyScript(JSScript *script);
 
     /**************************************** Functions for use by jsdbg.cpp. */
 
     inline bool observesScope(JSObject *obj) const;
     inline bool observesFrame(StackFrame *fp) const;
 
     // Precondition: *vp is a value from a debuggee compartment and cx is in
     // the debugger's compartment.
@@ -226,16 +261,32 @@ class Debug {
     // whether creating the new completion value succeeded or not.
     //
     // On success, a completion value is in vp and ac.context does not have a
     // pending exception. (This ordinarily returns true even if the ok argument
     // is false.)
     //
     bool newCompletionValue(AutoCompartment &ac, bool ok, Value val, Value *vp);
 
+    // Return the Debug.Script object for |fun|'s script, or create a new one if needed.
+    // The context |cx| must be in the debugger compartment; |fun| must be a
+    // cross-compartment wrapper referring to the JSFunction in a debuggee compartment.
+    JSObject *wrapFunctionScript(JSContext *cx, JSFunction *fun);
+
+    // Return the Debug.Script object for the Script object |obj|'s JSScript, or create a
+    // new one if needed. The context |cx| must be in the debugger compartment; |obj| must
+    // be a cross-compartment wrapper referring to a script object in a debuggee
+    // compartment.
+    JSObject *wrapJSAPIScript(JSContext *cx, JSObject *scriptObj);
+
+    // Return the Debug.Script object for the eval script |script|, or create a new one if
+    // needed. The context |cx| must be in the debugger compartment; |script| must be a
+    // script in a debuggee compartment.
+    JSObject *wrapEvalScript(JSContext *cx, JSScript *script);
+
   private:
     // Prohibit copying.
     Debug(const Debug &);
     Debug & operator=(const Debug &);
 };
 
 bool
 Debug::hasAnyLiveHooks() const
@@ -291,15 +342,22 @@ Debug::onThrow(JSContext *cx, js::Value 
 {
     return cx->compartment->getDebuggees().empty()
            ? JSTRAP_CONTINUE
            : dispatchHook(cx, vp,
                           DebugObservesMethod(&Debug::observesThrow),
                           DebugHandleMethod(&Debug::handleThrow));
 }
 
+void
+Debug::onDestroyScript(JSScript *script)
+{
+    if (!script->compartment->getDebuggees().empty())
+        slowPathOnDestroyScript(script);
+}
+
 extern JSBool
 EvaluateInScope(JSContext *cx, JSObject *scobj, StackFrame *fp, const jschar *chars,
                 uintN length, const char *filename, uintN lineno, Value *rval);
 
 }
 
 #endif /* jsdbg_h__ */
--- a/js/src/jsscript.cpp
+++ b/js/src/jsscript.cpp
@@ -45,16 +45,17 @@
 #include "jstypes.h"
 #include "jsstdint.h"
 #include "jsutil.h"
 #include "jsprf.h"
 #include "jsapi.h"
 #include "jsatom.h"
 #include "jscntxt.h"
 #include "jsversion.h"
+#include "jsdbg.h"
 #include "jsdbgapi.h"
 #include "jsemit.h"
 #include "jsfun.h"
 #include "jsgc.h"
 #include "jsgcmark.h"
 #include "jsinterp.h"
 #include "jslock.h"
 #include "jsnum.h"
@@ -1438,16 +1439,17 @@ js_CallNewScriptHook(JSContext *cx, JSSc
 void
 js_CallDestroyScriptHook(JSContext *cx, JSScript *script)
 {
     JSDestroyScriptHook hook;
 
     hook = cx->debugHooks->destroyScriptHook;
     if (hook)
         hook(cx, script, cx->debugHooks->destroyScriptHookData);
+    Debug::onDestroyScript(script);
     JS_ClearScriptTraps(cx, script);
 }
 
 static void
 DestroyScript(JSContext *cx, JSScript *script)
 {
 #ifdef DEBUG
     if (script->isEmpty())