Trigger debug event on not yet caught exception in promises.
authoryangguo@chromium.org <yangguo@chromium.org@ce2b1a6d-e550-0410-aec6-3dcde31c8c00>
Fri, 25 Apr 2014 07:03:05 +0000 (07:03 +0000)
committeryangguo@chromium.org <yangguo@chromium.org@ce2b1a6d-e550-0410-aec6-3dcde31c8c00>
Fri, 25 Apr 2014 07:03:05 +0000 (07:03 +0000)
R=aandrey@chromium.org, rossberg@chromium.org, yurys@chromium.org
BUG=v8:3093
LOG=Y

Review URL: https://codereview.chromium.org/249503002

git-svn-id: http://v8.googlecode.com/svn/branches/bleeding_edge@20956 ce2b1a6d-e550-0410-aec6-3dcde31c8c00

15 files changed:
include/v8-debug.h
src/debug-debugger.js
src/debug.cc
src/debug.h
src/execution.cc
src/isolate.cc
src/promise.js
src/runtime.cc
src/runtime.h
test/mjsunit/es6/debug-promises-caught-all.js [new file with mode: 0644]
test/mjsunit/es6/debug-promises-caught-uncaught.js [new file with mode: 0644]
test/mjsunit/es6/debug-promises-throw-in-reject.js [new file with mode: 0644]
test/mjsunit/es6/debug-promises-uncaught-all.js [new file with mode: 0644]
test/mjsunit/es6/debug-promises-uncaught-uncaught.js [new file with mode: 0644]
test/mjsunit/es6/debug-promises-undefined-reject.js [new file with mode: 0644]

index 49916fe..6b2c278 100644 (file)
@@ -43,7 +43,8 @@ enum DebugEvent {
   BeforeCompile = 4,
   AfterCompile  = 5,
   ScriptCollected = 6,
-  BreakForCommand = 7
+  UncaughtExceptionInPromise = 7,
+  BreakForCommand = 8
 };
 
 
index d759fe5..aebef47 100644 (file)
@@ -42,7 +42,8 @@ Debug.DebugEvent = { Break: 1,
                      NewFunction: 3,
                      BeforeCompile: 4,
                      AfterCompile: 5,
-                     ScriptCollected: 6 };
+                     ScriptCollected: 6,
+                     PendingExceptionInPromise: 7 };
 
 // Types of exceptions that can be broken upon.
 Debug.ExceptionBreak = { Caught : 0,
@@ -1093,15 +1094,16 @@ BreakEvent.prototype.toJSONProtocol = function() {
 };
 
 
-function MakeExceptionEvent(exec_state, exception, uncaught) {
-  return new ExceptionEvent(exec_state, exception, uncaught);
+function MakeExceptionEvent(exec_state, exception, uncaught, promise) {
+  return new ExceptionEvent(exec_state, exception, uncaught, promise);
 }
 
 
-function ExceptionEvent(exec_state, exception, uncaught) {
+function ExceptionEvent(exec_state, exception, uncaught, promise) {
   this.exec_state_ = exec_state;
   this.exception_ = exception;
   this.uncaught_ = uncaught;
+  this.promise_ = promise;
 }
 
 
@@ -1125,6 +1127,11 @@ ExceptionEvent.prototype.uncaught = function() {
 };
 
 
+ExceptionEvent.prototype.promise = function() {
+  return this.promise_;
+};
+
+
 ExceptionEvent.prototype.func = function() {
   return this.exec_state_.frame(0).func();
 };
index b41375b..c6a224e 100644 (file)
@@ -2628,13 +2628,15 @@ MaybeHandle<Object> Debugger::MakeBreakEvent(Handle<Object> break_points_hit) {
 
 
 MaybeHandle<Object> Debugger::MakeExceptionEvent(Handle<Object> exception,
-                                                 bool uncaught) {
+                                                 bool uncaught,
+                                                 Handle<Object> promise) {
   Handle<Object> exec_state;
   if (!MakeExecutionState().ToHandle(&exec_state)) return MaybeHandle<Object>();
   // Create the new exception event object.
   Handle<Object> argv[] = { exec_state,
                             exception,
-                            isolate_->factory()->ToBoolean(uncaught) };
+                            isolate_->factory()->ToBoolean(uncaught),
+                            promise };
   return MakeJSObject(CStrVector("MakeExceptionEvent"), ARRAY_SIZE(argv), argv);
 }
 
@@ -2664,7 +2666,9 @@ MaybeHandle<Object> Debugger::MakeScriptCollectedEvent(int id) {
 }
 
 
-void Debugger::OnException(Handle<Object> exception, bool uncaught) {
+void Debugger::OnException(Handle<Object> exception,
+                           bool uncaught,
+                           Handle<Object> promise) {
   HandleScope scope(isolate_);
   Debug* debug = isolate_->debug();
 
@@ -2688,13 +2692,21 @@ void Debugger::OnException(Handle<Object> exception, bool uncaught) {
 
   // Clear all current stepping setup.
   debug->ClearStepping();
+
+  // Determine event;
+  DebugEvent event = promise->IsUndefined()
+      ? v8::Exception : v8::UncaughtExceptionInPromise;
+
   // Create the event data object.
   Handle<Object> event_data;
   // Bail out and don't call debugger if exception.
-  if (!MakeExceptionEvent(exception, uncaught).ToHandle(&event_data)) return;
+  if (!MakeExceptionEvent(
+          exception, uncaught, promise).ToHandle(&event_data)) {
+    return;
+  }
 
   // Process debug event.
-  ProcessDebugEvent(v8::Exception, Handle<JSObject>::cast(event_data), false);
+  ProcessDebugEvent(event, Handle<JSObject>::cast(event_data), false);
   // Return to continue execution from where the exception was thrown.
 }
 
index b0a6892..b132243 100644 (file)
@@ -793,13 +793,17 @@ class Debugger {
   MUST_USE_RESULT MaybeHandle<Object> MakeBreakEvent(
       Handle<Object> break_points_hit);
   MUST_USE_RESULT MaybeHandle<Object> MakeExceptionEvent(
-      Handle<Object> exception, bool uncaught);
+      Handle<Object> exception,
+      bool uncaught,
+      Handle<Object> promise);
   MUST_USE_RESULT MaybeHandle<Object> MakeCompileEvent(
       Handle<Script> script, bool before);
   MUST_USE_RESULT MaybeHandle<Object> MakeScriptCollectedEvent(int id);
 
   void OnDebugBreak(Handle<Object> break_points_hit, bool auto_continue);
-  void OnException(Handle<Object> exception, bool uncaught);
+  void OnException(Handle<Object> exception,
+                   bool uncaught,
+                   Handle<Object> promise = Handle<Object>::null());
   void OnBeforeCompile(Handle<Script> script);
 
   enum AfterCompileFlags {
index f88e619..3ca42fb 100644 (file)
@@ -316,7 +316,7 @@ void Execution::RunMicrotasks(Isolate* isolate) {
       isolate->run_microtasks(),
       isolate->factory()->undefined_value(),
       0,
-      NULL).Assert();
+      NULL).Check();
 }
 
 
@@ -327,7 +327,7 @@ void Execution::EnqueueMicrotask(Isolate* isolate, Handle<Object> microtask) {
       isolate->enqueue_external_microtask(),
       isolate->factory()->undefined_value(),
       1,
-      args).Assert();
+      args).Check();
 }
 
 
index 9a5bc5a..4cdcef3 100644 (file)
@@ -1055,7 +1055,8 @@ void Isolate::DoThrow(Object* exception, MessageLocation* location) {
 #ifdef ENABLE_DEBUGGER_SUPPORT
   // Notify debugger of exception.
   if (catchable_by_javascript) {
-    debugger_->OnException(exception_handle, report_exception);
+    debugger_->OnException(
+        exception_handle, report_exception, factory()->undefined_value());
   }
 #endif
 
index 27890a7..f77fc5c 100644 (file)
@@ -191,9 +191,22 @@ function PromiseHandle(value, handler, deferred) {
       %_CallFunction(result, deferred.resolve, deferred.reject, PromiseChain);
     else
       deferred.resolve(result);
-  } catch(e) {
-    // TODO(rossberg): perhaps log uncaught exceptions below.
-    try { deferred.reject(e) } catch(e) {}
+  } catch (exception) {
+    var uncaught = false;
+    var reject_queue = GET_PRIVATE(deferred.promise, promiseOnReject);
+    if (reject_queue && reject_queue.length == 0) {
+      // The deferred promise may get a reject handler attached later.
+      // For now, we consider the exception to be (for the moment) uncaught.
+      uncaught = true;
+    }
+    try {
+      deferred.reject(exception);
+    } catch (e) {
+      // The reject handler can only throw for a custom deferred promise.
+      // We consider the original exception to be uncaught.
+      uncaught = true;
+    }
+    if (uncaught) %DebugPendingExceptionInPromise(exception, deferred.promise);
   }
 }
 
index 7a2958f..ca0b99b 100644 (file)
@@ -5625,7 +5625,6 @@ RUNTIME_FUNCTION(Runtime_StoreArrayLiteralElement) {
 // Check whether debugger and is about to step into the callback that is passed
 // to a built-in function such as Array.forEach.
 RUNTIME_FUNCTION(Runtime_DebugCallbackSupportsStepping) {
-  SealHandleScope shs(isolate);
 #ifdef ENABLE_DEBUGGER_SUPPORT
   ASSERT(args.length() == 1);
   if (!isolate->IsDebuggerActive() || !isolate->debug()->StepInActive()) {
@@ -5644,7 +5643,6 @@ RUNTIME_FUNCTION(Runtime_DebugCallbackSupportsStepping) {
 // Set one shot breakpoints for the callback function that is passed to a
 // built-in function such as Array.forEach to enable stepping into the callback.
 RUNTIME_FUNCTION(Runtime_DebugPrepareStepInIfStepping) {
-  SealHandleScope shs(isolate);
 #ifdef ENABLE_DEBUGGER_SUPPORT
   ASSERT(args.length() == 1);
   Debug* debug = isolate->debug();
@@ -5661,6 +5659,19 @@ RUNTIME_FUNCTION(Runtime_DebugPrepareStepInIfStepping) {
 }
 
 
+// Notify the debugger if an expcetion in a promise is not caught (yet).
+RUNTIME_FUNCTION(Runtime_DebugPendingExceptionInPromise) {
+#ifdef ENABLE_DEBUGGER_SUPPORT
+  ASSERT(args.length() == 2);
+  HandleScope scope(isolate);
+  CONVERT_ARG_HANDLE_CHECKED(Object, exception, 0);
+  CONVERT_ARG_HANDLE_CHECKED(JSObject, promise, 1);
+  isolate->debugger()->OnException(exception, true, promise);
+#endif  // ENABLE_DEBUGGER_SUPPORT
+  return isolate->heap()->undefined_value();
+}
+
+
 // Set a local property, even if it is READ_ONLY.  If the property does not
 // exist, it will be added with attributes NONE.
 RUNTIME_FUNCTION(Runtime_IgnoreAttributesAndSetProperty) {
index 237f9de..9bced7d 100644 (file)
@@ -99,6 +99,7 @@ namespace internal {
   F(StoreArrayLiteralElement, 5, 1) \
   F(DebugCallbackSupportsStepping, 1, 1) \
   F(DebugPrepareStepInIfStepping, 1, 1) \
+  F(DebugPendingExceptionInPromise, 2, 1) \
   F(FlattenString, 1, 1) \
   F(LoadMutableDouble, 2, 1) \
   F(TryMigrateInstance, 1, 1) \
diff --git a/test/mjsunit/es6/debug-promises-caught-all.js b/test/mjsunit/es6/debug-promises-caught-all.js
new file mode 100644 (file)
index 0000000..53369e3
--- /dev/null
@@ -0,0 +1,56 @@
+// Copyright 2014 the V8 project authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Flags: --harmony-promises --expose-debug-as debug
+
+// Test debug events when we listen to all exceptions and
+// there is a catch handler for the exception thrown in a Promise.
+// Expectation:
+//  - only the normal Exception debug event is triggered.
+
+Debug = debug.Debug;
+
+var log = [];
+var step = 0;
+
+var p = new Promise(function(resolve, reject) {
+  log.push("resolve");
+  resolve();
+});
+
+var q = p.chain(
+  function() {
+    log.push("throw");
+    throw new Error("caught");
+  });
+
+q.catch(
+  function(e) {
+    assertEquals("caught", e.message);
+  });
+
+function listener(event, exec_state, event_data, data) {
+  try {
+    // Ignore exceptions during startup in stress runs.
+    if (step >= 1) return;
+    assertEquals(["resolve", "end main", "throw"], log);
+    assertTrue(event != Debug.DebugEvent.PendingExceptionInPromise);
+    if (event == Debug.DebugEvent.Exception) {
+      assertEquals("caught", event_data.exception().message);
+      assertEquals(undefined, event_data.promise());
+      step++;
+    }
+  } catch (e) {
+    // Signal a failure with exit code 1.  This is necessary since the
+    // debugger swallows exceptions and we expect the chained function
+    // and this listener to be executed after the main script is finished.
+    print("Unexpected exception: " + e + "\n" + e.stack);
+    quit(1);
+  }
+}
+
+Debug.setBreakOnException();
+Debug.setListener(listener);
+
+log.push("end main");
diff --git a/test/mjsunit/es6/debug-promises-caught-uncaught.js b/test/mjsunit/es6/debug-promises-caught-uncaught.js
new file mode 100644 (file)
index 0000000..b7f6d48
--- /dev/null
@@ -0,0 +1,42 @@
+// Copyright 2014 the V8 project authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Flags: --harmony-promises --expose-debug-as debug
+
+// Test debug events when we only listen to uncaught exceptions and
+// there is a catch handler for the exception thrown in a Promise.
+// Expectation:
+//  - no debug event is triggered.
+
+Debug = debug.Debug;
+
+var p = new Promise(function(resolve, reject) {
+  resolve();
+});
+
+var q = p.chain(
+  function() {
+    throw new Error("caught");
+  });
+
+q.catch(
+  function(e) {
+    assertEquals("caught", e.message);
+  });
+
+function listener(event, exec_state, event_data, data) {
+  try {
+    assertTrue(event != Debug.DebugEvent.Exception);
+    assertTrue(event != Debug.DebugEvent.PendingExceptionInPromise);
+  } catch (e) {
+    // Signal a failure with exit code 1.  This is necessary since the
+    // debugger swallows exceptions and we expect the chained function
+    // and this listener to be executed after the main script is finished.
+    print("Unexpected exception: " + e + "\n" + e.stack);
+    quit(1);
+  }
+}
+
+Debug.setBreakOnUncaughtException();
+Debug.setListener(listener);
diff --git a/test/mjsunit/es6/debug-promises-throw-in-reject.js b/test/mjsunit/es6/debug-promises-throw-in-reject.js
new file mode 100644 (file)
index 0000000..6ae064d
--- /dev/null
@@ -0,0 +1,61 @@
+// Copyright 2014 the V8 project authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Flags: --harmony-promises --expose-debug-as debug
+
+// Test debug events when an exception is thrown inside a Promise, which is
+// caught by a custom promise, which throws a new exception in its reject
+// handler.  We expect a PendingExceptionInPromise event to be triggered.
+
+Debug = debug.Debug;
+
+var log = [];
+var step = 0;
+
+var p = new Promise(function(resolve, reject) {
+  log.push("resolve");
+  resolve();
+});
+
+function MyPromise(resolver) {
+  var reject = function() {
+    log.push("throw reject");
+    throw new Error("reject");
+  };
+  var resolve = function() { };
+  log.push("construct");
+  resolver(resolve, reject);
+};
+
+MyPromise.prototype = p;
+p.constructor = MyPromise;
+
+var q = p.chain(
+  function() {
+    log.push("throw caught");
+    throw new Error("caught");
+  });
+
+function listener(event, exec_state, event_data, data) {
+  try {
+    if (event == Debug.DebugEvent.PendingExceptionInPromise) {
+      assertEquals(["resolve", "construct", "end main",
+                    "throw caught", "throw reject"], log);
+      assertEquals("caught", event_data.exception().message);
+    } else if (event == Debug.DebugEvent.Exception) {
+      assertUnreachable();
+    }
+  } catch (e) {
+    // Signal a failure with exit code 1.  This is necessary since the
+    // debugger swallows exceptions and we expect the chained function
+    // and this listener to be executed after the main script is finished.
+    print("Unexpected exception: " + e + "\n" + e.stack);
+    quit(1);
+  }
+}
+
+Debug.setBreakOnUncaughtException();
+Debug.setListener(listener);
+
+log.push("end main");
diff --git a/test/mjsunit/es6/debug-promises-uncaught-all.js b/test/mjsunit/es6/debug-promises-uncaught-all.js
new file mode 100644 (file)
index 0000000..7ee8cb2
--- /dev/null
@@ -0,0 +1,62 @@
+// Copyright 2014 the V8 project authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Flags: --harmony-promises --expose-debug-as debug
+
+// Test debug events when we listen to all exceptions and
+// there is a catch handler for the exception thrown in a Promise.
+// Expectation:
+//  - the normal Exception debug event is triggered.
+//  - the PendingExceptionInPromise debug event is triggered afterwards,
+//    with the same exception object.
+
+Debug = debug.Debug;
+
+var log = [];
+var step = 0;
+var exception = undefined;
+
+var p = new Promise(function(resolve, reject) {
+  log.push("resolve");
+  resolve();
+});
+
+var q = p.chain(
+  function() {
+    log.push("throw");
+    throw new Error("uncaught");
+  });
+
+function listener(event, exec_state, event_data, data) {
+  try {
+    // Ignore exceptions during startup in stress runs.
+    if (step > 1) return;
+    assertEquals(["resolve", "end main", "throw"], log);
+    if (event == Debug.DebugEvent.Exception) {
+      assertEquals(0, step);
+      exception = event_data.exception();
+      assertEquals(undefined, event_data.promise());
+    } else if (event == Debug.DebugEvent.PendingExceptionInPromise) {
+      assertEquals(1, step);
+      assertEquals(exception, event_data.exception());
+      assertEquals("uncaught", exception.message);
+      assertTrue(event_data.promise() instanceof Promise);
+      assertTrue(event_data.uncaught());
+    } else {
+      return;
+    }
+    step++;
+  } catch (e) {
+    // Signal a failure with exit code 1.  This is necessary since the
+    // debugger swallows exceptions and we expect the chained function
+    // and this listener to be executed after the main script is finished.
+    print("Unexpected exception: " + e + "\n" + e.stack);
+    quit(1);
+  }
+}
+
+Debug.setBreakOnException();
+Debug.setListener(listener);
+
+log.push("end main");
diff --git a/test/mjsunit/es6/debug-promises-uncaught-uncaught.js b/test/mjsunit/es6/debug-promises-uncaught-uncaught.js
new file mode 100644 (file)
index 0000000..ae58c62
--- /dev/null
@@ -0,0 +1,54 @@
+// Copyright 2014 the V8 project authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Flags: --harmony-promises --expose-debug-as debug
+
+// Test debug events when we only listen to uncaught exceptions and
+// there is a catch handler for the exception thrown in a Promise.
+// Expectation:
+//  - only the PendingExceptionInPromise debug event is triggered.
+
+Debug = debug.Debug;
+
+var log = [];
+var step = 0;
+
+var p = new Promise(function(resolve, reject) {
+  log.push("resolve");
+  resolve();
+});
+
+var q = p.chain(
+  function() {
+    log.push("throw");
+    throw new Error("uncaught");
+  });
+
+function listener(event, exec_state, event_data, data) {
+  try {
+    // Ignore exceptions during startup in stress runs.
+    if (step >= 1) return;
+    assertEquals(["resolve", "end main", "throw"], log);
+    if (event == Debug.DebugEvent.Exception) {
+      assertUnreachable();
+    } else if (event == Debug.DebugEvent.PendingExceptionInPromise) {
+      assertEquals(0, step);
+      assertEquals("uncaught", event_data.exception().message);
+      assertTrue(event_data.promise() instanceof Promise);
+      assertTrue(event_data.uncaught());
+      step++;
+    }
+  } catch (e) {
+    // Signal a failure with exit code 1.  This is necessary since the
+    // debugger swallows exceptions and we expect the chained function
+    // and this listener to be executed after the main script is finished.
+    print("Unexpected exception: " + e + "\n" + e.stack);
+    quit(1);
+  }
+}
+
+Debug.setBreakOnUncaughtException();
+Debug.setListener(listener);
+
+log.push("end main");
diff --git a/test/mjsunit/es6/debug-promises-undefined-reject.js b/test/mjsunit/es6/debug-promises-undefined-reject.js
new file mode 100644 (file)
index 0000000..d95052f
--- /dev/null
@@ -0,0 +1,57 @@
+// Copyright 2014 the V8 project authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Flags: --harmony-promises --expose-debug-as debug
+
+// Test debug events when an exception is thrown inside a Promise, which is
+// caught by a custom promise, which has no reject handler.
+// We expect a PendingExceptionInPromise event to be triggered.
+
+Debug = debug.Debug;
+
+var log = [];
+var step = 0;
+
+var p = new Promise(function(resolve, reject) {
+  log.push("resolve");
+  resolve();
+});
+
+function MyPromise(resolver) {
+  var reject = undefined;
+  var resolve = function() { };
+  log.push("construct");
+  resolver(resolve, reject);
+};
+
+MyPromise.prototype = p;
+p.constructor = MyPromise;
+
+var q = p.chain(
+  function() {
+    log.push("throw caught");
+    throw new Error("caught");
+  });
+
+function listener(event, exec_state, event_data, data) {
+  try {
+    if (event == Debug.DebugEvent.PendingExceptionInPromise) {
+      assertEquals(["resolve", "construct", "end main", "throw caught"], log);
+      assertEquals("caught", event_data.exception().message);
+    } else if (event == Debug.DebugEvent.Exception) {
+      assertUnreachable();
+    }
+  } catch (e) {
+    // Signal a failure with exit code 1.  This is necessary since the
+    // debugger swallows exceptions and we expect the chained function
+    // and this listener to be executed after the main script is finished.
+    print("Unexpected exception: " + e + "\n" + e.stack);
+    quit(1);
+  }
+}
+
+Debug.setBreakOnUncaughtException();
+Debug.setListener(listener);
+
+log.push("end main");