[ESnext] Implement Promise.prototype.finally

Adds five new TF builtins for the spec defined functions/closures. This follows
mechanism similar to promise resolving functions approach where we store the
closure variables in a custom context.

Adds a new --harmony-promise-finally flag.

BUG=v8:5967

Review-Url: https://codereview.chromium.org/2695753002
Cr-Commit-Position: refs/heads/master@{#43294}
diff --git a/src/bootstrapper.cc b/src/bootstrapper.cc
index e894279..4963e85 100644
--- a/src/bootstrapper.cc
+++ b/src/bootstrapper.cc
@@ -3619,6 +3619,67 @@
                   factory()->async_iterator_symbol());
 }
 
+void Genesis::InitializeGlobal_harmony_promise_finally() {
+  if (!FLAG_harmony_promise_finally) return;
+
+  Handle<JSFunction> constructor(native_context()->promise_function());
+  Handle<JSObject> prototype(JSObject::cast(constructor->instance_prototype()));
+  SimpleInstallFunction(prototype, "finally", Builtins::kPromiseFinally, 1,
+                        true, DONT_ENUM);
+
+  // The promise prototype map has changed because we added a property
+  // to prototype, so we update the saved map.
+  Handle<Map> prototype_map(prototype->map());
+  Map::SetShouldBeFastPrototypeMap(prototype_map, true, isolate());
+  native_context()->set_promise_prototype_map(*prototype_map);
+
+  {
+    Handle<Code> code =
+        handle(isolate()->builtins()->builtin(Builtins::kPromiseThenFinally),
+               isolate());
+    Handle<SharedFunctionInfo> info = factory()->NewSharedFunctionInfo(
+        factory()->empty_string(), code, false);
+    info->set_internal_formal_parameter_count(1);
+    info->set_length(1);
+    info->set_native(true);
+    native_context()->set_promise_then_finally_shared_fun(*info);
+  }
+
+  {
+    Handle<Code> code =
+        handle(isolate()->builtins()->builtin(Builtins::kPromiseCatchFinally),
+               isolate());
+    Handle<SharedFunctionInfo> info = factory()->NewSharedFunctionInfo(
+        factory()->empty_string(), code, false);
+    info->set_internal_formal_parameter_count(1);
+    info->set_length(1);
+    info->set_native(true);
+    native_context()->set_promise_catch_finally_shared_fun(*info);
+  }
+
+  {
+    Handle<Code> code = handle(
+        isolate()->builtins()->builtin(Builtins::kPromiseValueThunkFinally),
+        isolate());
+    Handle<SharedFunctionInfo> info = factory()->NewSharedFunctionInfo(
+        factory()->empty_string(), code, false);
+    info->set_internal_formal_parameter_count(0);
+    info->set_length(0);
+    native_context()->set_promise_value_thunk_finally_shared_fun(*info);
+  }
+
+  {
+    Handle<Code> code =
+        handle(isolate()->builtins()->builtin(Builtins::kPromiseThrowerFinally),
+               isolate());
+    Handle<SharedFunctionInfo> info = factory()->NewSharedFunctionInfo(
+        factory()->empty_string(), code, false);
+    info->set_internal_formal_parameter_count(0);
+    info->set_length(0);
+    native_context()->set_promise_thrower_finally_shared_fun(*info);
+  }
+}
+
 #ifdef V8_I18N_SUPPORT
 void Genesis::InitializeGlobal_datetime_format_to_parts() {
   if (!FLAG_datetime_format_to_parts) return;
@@ -4153,6 +4214,7 @@
   static const char* harmony_object_rest_spread_natives[] = {nullptr};
   static const char* harmony_async_iteration_natives[] = {nullptr};
   static const char* harmony_dynamic_import_natives[] = {nullptr};
+  static const char* harmony_promise_finally_natives[] = {nullptr};
 
   for (int i = ExperimentalNatives::GetDebuggerCount();
        i < ExperimentalNatives::GetBuiltinsCount(); i++) {
diff --git a/src/builtins/builtins-promise.cc b/src/builtins/builtins-promise.cc
index 0f89d30..aac79f1 100644
--- a/src/builtins/builtins-promise.cc
+++ b/src/builtins/builtins-promise.cc
@@ -438,7 +438,6 @@
 
   Bind(&if_onresolvenotcallable);
   {
-    Isolate* isolate = this->isolate();
     Node* const default_resolve_handler_symbol = HeapConstant(
         isolate->factory()->promise_default_resolve_handler_symbol());
     var_on_resolve.Bind(default_resolve_handler_symbol);
@@ -1563,5 +1562,221 @@
   Return(UndefinedConstant());
 }
 
+Node* PromiseBuiltinsAssembler::CreatePromiseFinallyContext(
+    Node* on_finally, Node* native_context) {
+  Node* const context =
+      CreatePromiseContext(native_context, kOnFinallyContextLength);
+  StoreContextElementNoWriteBarrier(context, kOnFinallySlot, on_finally);
+  return context;
+}
+
+std::pair<Node*, Node*> PromiseBuiltinsAssembler::CreatePromiseFinallyFunctions(
+    Node* on_finally, Node* native_context) {
+  Node* const promise_context =
+      CreatePromiseFinallyContext(on_finally, native_context);
+  Node* const map = LoadContextElement(
+      native_context, Context::STRICT_FUNCTION_WITHOUT_PROTOTYPE_MAP_INDEX);
+  Node* const then_finally_info = LoadContextElement(
+      native_context, Context::PROMISE_THEN_FINALLY_SHARED_FUN);
+  Node* const then_finally = AllocateFunctionWithMapAndContext(
+      map, then_finally_info, promise_context);
+  Node* const catch_finally_info = LoadContextElement(
+      native_context, Context::PROMISE_CATCH_FINALLY_SHARED_FUN);
+  Node* const catch_finally = AllocateFunctionWithMapAndContext(
+      map, catch_finally_info, promise_context);
+  return std::make_pair(then_finally, catch_finally);
+}
+
+TF_BUILTIN(PromiseValueThunkFinally, PromiseBuiltinsAssembler) {
+  Node* const context = Parameter(3);
+
+  Node* const value = LoadContextElement(context, kOnFinallySlot);
+  Return(value);
+}
+
+Node* PromiseBuiltinsAssembler::CreateValueThunkFunctionContext(
+    Node* value, Node* native_context) {
+  Node* const context =
+      CreatePromiseContext(native_context, kOnFinallyContextLength);
+  StoreContextElementNoWriteBarrier(context, kOnFinallySlot, value);
+  return context;
+}
+
+Node* PromiseBuiltinsAssembler::CreateValueThunkFunction(Node* value,
+                                                         Node* native_context) {
+  Node* const value_thunk_context =
+      CreateValueThunkFunctionContext(value, native_context);
+  Node* const map = LoadContextElement(
+      native_context, Context::STRICT_FUNCTION_WITHOUT_PROTOTYPE_MAP_INDEX);
+  Node* const value_thunk_info = LoadContextElement(
+      native_context, Context::PROMISE_VALUE_THUNK_FINALLY_SHARED_FUN);
+  Node* const value_thunk = AllocateFunctionWithMapAndContext(
+      map, value_thunk_info, value_thunk_context);
+  return value_thunk;
+}
+
+TF_BUILTIN(PromiseThenFinally, PromiseBuiltinsAssembler) {
+  CSA_ASSERT_JS_ARGC_EQ(this, 1);
+
+  Node* const value = Parameter(1);
+  Node* const context = Parameter(4);
+
+  Node* const on_finally = LoadContextElement(context, kOnFinallySlot);
+
+  // 2.a Let result be ?  Call(onFinally, undefined).
+  Callable call_callable = CodeFactory::Call(isolate());
+  Node* result =
+      CallJS(call_callable, context, on_finally, UndefinedConstant());
+
+  // 2.b Let promise be !  PromiseResolve( %Promise%, result).
+  Node* const promise = AllocateAndInitJSPromise(context);
+  InternalResolvePromise(context, promise, result);
+
+  // 2.c Let valueThunk be equivalent to a function that returns value.
+  Node* native_context = LoadNativeContext(context);
+  Node* const value_thunk = CreateValueThunkFunction(value, native_context);
+
+  // 2.d Let promiseCapability be !  NewPromiseCapability( %Promise%).
+  Node* const promise_capability = AllocateAndInitJSPromise(context, promise);
+
+  // 2.e Return PerformPromiseThen(promise, valueThunk, undefined,
+  // promiseCapability).
+  InternalPerformPromiseThen(context, promise, value_thunk, UndefinedConstant(),
+                             promise_capability, UndefinedConstant(),
+                             UndefinedConstant());
+  Return(promise_capability);
+}
+
+TF_BUILTIN(PromiseThrowerFinally, PromiseBuiltinsAssembler) {
+  Node* const context = Parameter(3);
+
+  Node* const reason = LoadContextElement(context, kOnFinallySlot);
+  CallRuntime(Runtime::kThrow, context, reason);
+  Return(UndefinedConstant());
+}
+
+Node* PromiseBuiltinsAssembler::CreateThrowerFunctionContext(
+    Node* reason, Node* native_context) {
+  Node* const context =
+      CreatePromiseContext(native_context, kOnFinallyContextLength);
+  StoreContextElementNoWriteBarrier(context, kOnFinallySlot, reason);
+  return context;
+}
+
+Node* PromiseBuiltinsAssembler::CreateThrowerFunction(Node* reason,
+                                                      Node* native_context) {
+  Node* const thrower_context =
+      CreateThrowerFunctionContext(reason, native_context);
+  Node* const map = LoadContextElement(
+      native_context, Context::STRICT_FUNCTION_WITHOUT_PROTOTYPE_MAP_INDEX);
+  Node* const thrower_info = LoadContextElement(
+      native_context, Context::PROMISE_THROWER_FINALLY_SHARED_FUN);
+  Node* const thrower =
+      AllocateFunctionWithMapAndContext(map, thrower_info, thrower_context);
+  return thrower;
+}
+
+TF_BUILTIN(PromiseCatchFinally, PromiseBuiltinsAssembler) {
+  CSA_ASSERT_JS_ARGC_EQ(this, 1);
+
+  Node* const reason = Parameter(1);
+  Node* const context = Parameter(4);
+
+  Node* const on_finally = LoadContextElement(context, kOnFinallySlot);
+
+  // 2.a Let result be ?  Call(onFinally, undefined).
+  Callable call_callable = CodeFactory::Call(isolate());
+  Node* result =
+      CallJS(call_callable, context, on_finally, UndefinedConstant());
+
+  // 2.b Let promise be !  PromiseResolve( %Promise%, result).
+  Node* const promise = AllocateAndInitJSPromise(context);
+  InternalResolvePromise(context, promise, result);
+
+  // 2.c Let thrower be equivalent to a function that throws reason.
+  Node* native_context = LoadNativeContext(context);
+  Node* const thrower = CreateThrowerFunction(reason, native_context);
+
+  // 2.d Let promiseCapability be !  NewPromiseCapability( %Promise%).
+  Node* const promise_capability = AllocateAndInitJSPromise(context, promise);
+
+  // 2.e Return PerformPromiseThen(promise, thrower, undefined,
+  // promiseCapability).
+  InternalPerformPromiseThen(context, promise, thrower, UndefinedConstant(),
+                             promise_capability, UndefinedConstant(),
+                             UndefinedConstant());
+  Return(promise_capability);
+}
+
+TF_BUILTIN(PromiseFinally, PromiseBuiltinsAssembler) {
+  CSA_ASSERT_JS_ARGC_EQ(this, 1);
+
+  // 1.  Let promise be the this value.
+  Node* const promise = Parameter(0);
+  Node* const on_finally = Parameter(1);
+  Node* const context = Parameter(4);
+
+  // 2. If IsPromise(promise) is false, throw a TypeError exception.
+  ThrowIfNotInstanceType(context, promise, JS_PROMISE_TYPE,
+                         "Promise.prototype.finally");
+
+  Variable var_then_finally(this, MachineRepresentation::kTagged),
+      var_catch_finally(this, MachineRepresentation::kTagged);
+
+  Label if_notcallable(this, Label::kDeferred), perform_finally(this);
+
+  // 3. Let thenFinally be !  CreateThenFinally(onFinally).
+  // 4. Let catchFinally be !  CreateCatchFinally(onFinally).
+  GotoIf(TaggedIsSmi(on_finally), &if_notcallable);
+  Node* const on_finally_map = LoadMap(on_finally);
+  GotoIfNot(IsCallableMap(on_finally_map), &if_notcallable);
+
+  Node* const native_context = LoadNativeContext(context);
+  Node* then_finally = nullptr;
+  Node* catch_finally = nullptr;
+  std::tie(then_finally, catch_finally) =
+      CreatePromiseFinallyFunctions(on_finally, native_context);
+  var_then_finally.Bind(then_finally);
+  var_catch_finally.Bind(catch_finally);
+  Goto(&perform_finally);
+
+  Bind(&if_notcallable);
+  {
+    var_then_finally.Bind(on_finally);
+    var_catch_finally.Bind(on_finally);
+    Goto(&perform_finally);
+  }
+
+  // 5. Return PerformPromiseThen(promise, valueThunk, undefined,
+  // promiseCapability).
+  Bind(&perform_finally);
+  Label if_nativepromise(this), if_custompromise(this, Label::kDeferred);
+  BranchIfFastPath(context, promise, &if_nativepromise, &if_custompromise);
+
+  Bind(&if_nativepromise);
+  {
+    Node* deferred_promise = AllocateAndInitJSPromise(context, promise);
+    InternalPerformPromiseThen(context, promise, var_then_finally.value(),
+                               var_catch_finally.value(), deferred_promise,
+                               UndefinedConstant(), UndefinedConstant());
+    Return(deferred_promise);
+  }
+
+  Bind(&if_custompromise);
+  {
+    Isolate* isolate = this->isolate();
+    Node* const then_str = HeapConstant(isolate->factory()->then_string());
+    Callable getproperty_callable = CodeFactory::GetProperty(isolate);
+    Node* const then =
+        CallStub(getproperty_callable, context, promise, then_str);
+    Callable call_callable = CodeFactory::Call(isolate);
+    // 5. Return ?  Invoke(promise, "then", « thenFinally, catchFinally »).
+    Node* const result =
+        CallJS(call_callable, context, then, promise, var_then_finally.value(),
+               var_catch_finally.value());
+    Return(result);
+  }
+}
+
 }  // namespace internal
 }  // namespace v8
diff --git a/src/builtins/builtins-promise.h b/src/builtins/builtins-promise.h
index 9f7f456..df01182 100644
--- a/src/builtins/builtins-promise.h
+++ b/src/builtins/builtins-promise.h
@@ -36,6 +36,18 @@
     kCapabilitiesContextLength,
   };
 
+  // This is used by the PromiseThenFinally and PromiseCatchFinally
+  // builtins to store the onFinally in the onFinallySlot.
+  //
+  // This is also used by the PromiseValueThunkFinally to store the
+  // value in the onFinallySlot and PromiseThrowerFinally to store the
+  // reason in the onFinallySlot.
+  enum PromiseFinallyContextSlot {
+    kOnFinallySlot = Context::MIN_CONTEXT_SLOTS,
+
+    kOnFinallyContextLength,
+  };
+
   explicit PromiseBuiltinsAssembler(CodeAssemblerState* state)
       : CodeStubAssembler(state) {}
   // These allocate and initialize a promise with pending state and
@@ -115,6 +127,15 @@
                              bool debug_event);
   void InternalPromiseReject(Node* context, Node* promise, Node* value,
                              Node* debug_event);
+  std::pair<Node*, Node*> CreatePromiseFinallyFunctions(Node* on_finally,
+                                                        Node* native_context);
+  Node* CreatePromiseFinallyContext(Node* on_finally, Node* native_context);
+
+  Node* CreateValueThunkFunction(Node* value, Node* native_context);
+  Node* CreateValueThunkFunctionContext(Node* value, Node* native_context);
+
+  Node* CreateThrowerFunctionContext(Node* reason, Node* native_context);
+  Node* CreateThrowerFunction(Node* reason, Node* native_context);
 
  private:
   Node* AllocateJSPromise(Node* context);
diff --git a/src/builtins/builtins.h b/src/builtins/builtins.h
index f23bfd1..49a9f41 100644
--- a/src/builtins/builtins.h
+++ b/src/builtins/builtins.h
@@ -666,6 +666,11 @@
   TFJ(PromiseResolve, 1)                                                       \
   TFJ(PromiseReject, 1)                                                        \
   TFJ(InternalPromiseReject, 3)                                                \
+  TFJ(PromiseFinally, 1)                                                       \
+  TFJ(PromiseThenFinally, 1)                                                   \
+  TFJ(PromiseCatchFinally, 1)                                                  \
+  TFJ(PromiseValueThunkFinally, 0)                                             \
+  TFJ(PromiseThrowerFinally, 0)                                                \
                                                                                \
   /* Proxy */                                                                  \
   CPP(ProxyConstructor)                                                        \
diff --git a/src/contexts.h b/src/contexts.h
index 530e29a..5c88aa5 100644
--- a/src/contexts.h
+++ b/src/contexts.h
@@ -290,6 +290,14 @@
   V(PROMISE_RESOLVE_SHARED_FUN, SharedFunctionInfo,                            \
     promise_resolve_shared_fun)                                                \
   V(PROMISE_REJECT_SHARED_FUN, SharedFunctionInfo, promise_reject_shared_fun)  \
+  V(PROMISE_THEN_FINALLY_SHARED_FUN, SharedFunctionInfo,                       \
+    promise_then_finally_shared_fun)                                           \
+  V(PROMISE_CATCH_FINALLY_SHARED_FUN, SharedFunctionInfo,                      \
+    promise_catch_finally_shared_fun)                                          \
+  V(PROMISE_VALUE_THUNK_FINALLY_SHARED_FUN, SharedFunctionInfo,                \
+    promise_value_thunk_finally_shared_fun)                                    \
+  V(PROMISE_THROWER_FINALLY_SHARED_FUN, SharedFunctionInfo,                    \
+    promise_thrower_finally_shared_fun)                                        \
   V(PROMISE_PROTOTYPE_MAP_INDEX, Map, promise_prototype_map)                   \
   V(REGEXP_EXEC_FUNCTION_INDEX, JSFunction, regexp_exec_function)              \
   V(REGEXP_FUNCTION_INDEX, JSFunction, regexp_function)                        \
diff --git a/src/flag-definitions.h b/src/flag-definitions.h
index a5a1122..ee5fc3d 100644
--- a/src/flag-definitions.h
+++ b/src/flag-definitions.h
@@ -205,7 +205,8 @@
   V(harmony_function_tostring, "harmony Function.prototype.toString")   \
   V(harmony_class_fields, "harmony public fields in class literals")    \
   V(harmony_async_iteration, "harmony async iteration")                 \
-  V(harmony_dynamic_import, "harmony dynamic import")
+  V(harmony_dynamic_import, "harmony dynamic import")                   \
+  V(harmony_promise_finally, "harmony Promise.prototype.finally")
 
 // Features that are complete (but still behind --harmony/es-staging flag).
 #define HARMONY_STAGED(V)                                   \
diff --git a/src/objects-printer.cc b/src/objects-printer.cc
index dfeb3cb..53c35e9 100644
--- a/src/objects-printer.cc
+++ b/src/objects-printer.cc
@@ -500,6 +500,7 @@
   os << "\n - fulfill_reactions = " << Brief(fulfill_reactions());
   os << "\n - reject_reactions = " << Brief(reject_reactions());
   os << "\n - has_handler = " << has_handler();
+  os << "\n ";
 }
 
 void JSRegExp::JSRegExpPrint(std::ostream& os) {  // NOLINT
diff --git a/test/mjsunit/harmony/promise-prototype-finally.js b/test/mjsunit/harmony/promise-prototype-finally.js
new file mode 100644
index 0000000..eefce4b
--- /dev/null
+++ b/test/mjsunit/harmony/promise-prototype-finally.js
@@ -0,0 +1,661 @@
+// Copyright 2016 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-promise-finally --allow-natives-syntax
+
+var asyncAssertsExpected = 0;
+
+function assertUnreachable() {
+  %AbortJS("Unreachable: failure");
+}
+
+function assertAsyncRan() {
+  ++asyncAssertsExpected;
+}
+
+function assertAsync(b, s) {
+  if (b) {
+    print(s, "succeeded");
+  } else {
+    %AbortJS(s + " FAILED!");
+  }
+  --asyncAssertsExpected;
+}
+
+function assertEqualsAsync(b, s) {
+  if (b === s) {
+    print(b, "===", s, "succeeded");
+  } else {
+    %AbortJS(b + "===" + s + " FAILED!");
+  }
+  --asyncAssertsExpected;
+}
+
+function assertAsyncDone(iteration) {
+  var iteration = iteration || 0;
+  %EnqueueMicrotask(function() {
+    if (asyncAssertsExpected === 0)
+      assertAsync(true, "all");
+    else if (
+      iteration > 10 // Shouldn't take more.
+    )
+      assertAsync(false, "all... " + asyncAssertsExpected);
+    else
+      assertAsyncDone(iteration + 1);
+  });
+}
+
+(function() {
+  assertThrows(
+    function() {
+      Promise.prototype.finally.call(5);
+    },
+    TypeError
+  );
+})();
+
+// resolve/finally/then
+(function() {
+  Promise.resolve(3).finally().then(
+    x => {
+      assertEqualsAsync(3, x);
+    },
+    assertUnreachable
+  );
+  assertAsyncRan();
+})();
+
+// reject/finally/then
+(function() {
+  Promise.reject(3).finally().then(assertUnreachable, x => {
+    assertEqualsAsync(3, x);
+  });
+  assertAsyncRan();
+})();
+
+// resolve/finally-return-notcallable/then
+(function() {
+  Promise.resolve(3).finally(2).then(
+    x => {
+      assertEqualsAsync(3, x);
+    },
+    assertUnreachable
+  );
+  assertAsyncRan();
+})();
+
+// reject/finally-return-notcallable/then
+(function() {
+  Promise.reject(3).finally(2).then(
+    assertUnreachable, e => {
+      assertEqualsAsync(3, e);
+    });
+  assertAsyncRan();
+})();
+
+// reject/finally/catch
+(function() {
+  Promise.reject(3).finally().catch(reason => {
+    assertEqualsAsync(3, reason);
+  });
+  assertAsyncRan();
+})();
+
+// reject/finally/then/catch
+(function() {
+  Promise.reject(3).finally().then(assertUnreachable).catch(reason => {
+    assertEqualsAsync(3, reason);
+  });
+  assertAsyncRan();
+})();
+
+// resolve/then/finally/then
+(function() {
+  Promise.resolve(3)
+    .then(x => {
+      assertEqualsAsync(3, x);
+      return x;
+    })
+    .finally()
+    .then(
+      x => {
+        assertEqualsAsync(3, x);
+      },
+      assertUnreachable
+    );
+  assertAsyncRan();
+  assertAsyncRan();
+})();
+
+// reject/catch/finally/then
+(function() {
+  Promise.reject(3)
+    .catch(x => {
+      assertEqualsAsync(3, x);
+      return x;
+    })
+    .finally()
+    .then(
+      x => {
+        assertEqualsAsync(3, x);
+      },
+      assertUnreachable
+    );
+  assertAsyncRan();
+  assertAsyncRan();
+})();
+
+// resolve/finally-throw/then
+(function() {
+  Promise.resolve(3)
+    .finally(function onFinally() {
+      assertEqualsAsync(0, arguments.length);
+      throw 1;
+    })
+    .then(assertUnreachable, function onRejected(reason) {
+      assertEqualsAsync(1, reason);
+    });
+  assertAsyncRan();
+  assertAsyncRan();
+})();
+
+// reject/finally-throw/then
+(function() {
+  Promise.reject(3)
+    .finally(function onFinally() {
+      assertEqualsAsync(0, arguments.length);
+      throw 1;
+    })
+    .then(assertUnreachable, function onRejected(reason) {
+      assertEqualsAsync(1, reason);
+    });
+  assertAsyncRan();
+  assertAsyncRan();
+})();
+
+// resolve/finally-return/then
+(function() {
+  Promise.resolve(3)
+    .finally(function onFinally() {
+      assertEqualsAsync(0, arguments.length);
+      return 4;
+    })
+    .then(
+      x => {
+        assertEqualsAsync(x, 3);
+      },
+      assertUnreachable
+    );
+  assertAsyncRan();
+  assertAsyncRan();
+})();
+
+// reject/finally-return/then
+(function() {
+  Promise.reject(3)
+    .finally(function onFinally() {
+      assertEqualsAsync(0, arguments.length);
+      return 4;
+    })
+    .then(assertUnreachable, x => {
+      assertEqualsAsync(x, 3);
+    });
+  assertAsyncRan();
+  assertAsyncRan();
+})();
+
+// reject/catch-throw/finally-throw/then
+(function() {
+  Promise.reject(3)
+    .catch(e => {
+      assertEqualsAsync(3, e);
+      throw e;
+    })
+    .finally(function onFinally() {
+      assertEqualsAsync(0, arguments.length);
+      throw 4;
+    })
+    .then(assertUnreachable, function onRejected(e) {
+      assertEqualsAsync(4, e);
+    });
+  assertAsyncRan();
+  assertAsyncRan();
+  assertAsyncRan();
+})();
+
+// resolve/then-throw/finally-throw/then
+(function() {
+  Promise.resolve(3)
+    .then(e => {
+      assertEqualsAsync(3, e);
+      throw e;
+    })
+    .finally(function onFinally() {
+      assertEqualsAsync(0, arguments.length);
+      throw 4;
+    })
+    .then(assertUnreachable, function onRejected(e) {
+      assertEqualsAsync(4, e);
+    });
+  assertAsyncRan();
+  assertAsyncRan();
+  assertAsyncRan();
+})();
+
+// resolve/finally-return-rejected-promise/then
+(function() {
+  Promise.resolve(3)
+    .finally(function onFinally() {
+      assertEqualsAsync(0, arguments.length);
+      return Promise.reject(4);
+    })
+    .then(assertUnreachable, e => {
+      assertEqualsAsync(4, e);
+    });
+  assertAsyncRan();
+  assertAsyncRan();
+})();
+
+// reject/finally-return-rejected-promise/then
+(function() {
+  Promise.reject(3)
+    .finally(function onFinally() {
+      assertEqualsAsync(0, arguments.length);
+      return Promise.reject(4);
+    })
+    .then(assertUnreachable, e => {
+      assertEqualsAsync(4, e);
+    });
+  assertAsyncRan();
+  assertAsyncRan();
+})();
+
+// resolve/finally-return-resolved-promise/then
+(function() {
+  Promise.resolve(3)
+    .finally(function onFinally() {
+      assertEqualsAsync(0, arguments.length);
+      return Promise.resolve(4);
+    })
+    .then(
+      x => {
+        assertEqualsAsync(3, x);
+      },
+      assertUnreachable
+    );
+  assertAsyncRan();
+  assertAsyncRan();
+})();
+
+// reject/finally-return-resolved-promise/then
+(function() {
+  Promise.reject(3)
+    .finally(function onFinally() {
+      assertEqualsAsync(0, arguments.length);
+      return Promise.resolve(4);
+    })
+    .then(assertUnreachable, e => {
+      assertEqualsAsync(3, e);
+    });
+  assertAsyncRan();
+  assertAsyncRan();
+})();
+
+// reject/finally-return-resolved-promise/then
+(function() {
+  Promise.reject(3)
+    .finally(function onFinally() {
+      assertEqualsAsync(0, arguments.length);
+      return Promise.resolve(4);
+    })
+    .then(assertUnreachable, e => {
+      assertEqualsAsync(3, e);
+    });
+  assertAsyncRan();
+  assertAsyncRan();
+})();
+
+// resolve/finally-thenable-resolve/then
+(function() {
+  var thenable = {
+    then: function(onResolve, onReject) {
+      onResolve(5);
+    }
+  };
+
+  Promise.resolve(5)
+    .finally(function onFinally() {
+      assertEqualsAsync(0, arguments.length);
+      return thenable;
+    })
+    .then(
+      x => {
+        assertEqualsAsync(5, x);
+      },
+      assertUnreachable
+    );
+
+  assertAsyncRan();
+  assertAsyncRan();
+})();
+
+// reject/finally-thenable-resolve/then
+(function() {
+  var thenable = {
+    then: function(onResolve, onReject) {
+      onResolve(1);
+    }
+  };
+
+  Promise.reject(5)
+    .finally(function onFinally() {
+      assertEqualsAsync(0, arguments.length);
+      return thenable;
+    })
+    .then(assertUnreachable, e => {
+      assertEqualsAsync(5, e);
+    });
+
+  assertAsyncRan();
+  assertAsyncRan();
+})();
+
+// reject/finally-thenable-reject/then
+(function() {
+  var thenable = {
+    then: function(onResolve, onReject) {
+      onReject(1);
+    }
+  };
+
+  Promise.reject(5)
+    .finally(function onFinally() {
+      assertEqualsAsync(0, arguments.length);
+      return thenable;
+    })
+    .then(assertUnreachable, e => {
+      assertEqualsAsync(1, e);
+    });
+
+  assertAsyncRan();
+  assertAsyncRan();
+})();
+
+// resolve/finally-thenable-reject/then
+(function() {
+  var thenable = {
+    then: function(onResolve, onReject) {
+      onReject(1);
+    }
+  };
+
+  Promise.resolve(5)
+    .finally(function onFinally() {
+      assertEqualsAsync(0, arguments.length);
+      return thenable;
+    })
+    .then(assertUnreachable, e => {
+      assertEqualsAsync(1, e);
+    });
+
+  assertAsyncRan();
+  assertAsyncRan();
+})();
+
+// resolve/finally/finally/then
+(function() {
+  Promise.resolve(5)
+    .finally(function onFinally() {
+      assertEqualsAsync(0, arguments.length);
+    })
+    .finally(function onFinally() {
+      assertEqualsAsync(0, arguments.length);
+    })
+    .then(
+      x => {
+        assertEqualsAsync(5, x);
+      },
+      assertUnreachable
+    );
+
+  assertAsyncRan();
+  assertAsyncRan();
+  assertAsyncRan();
+})();
+
+// resolve/finally-throw/finally/then
+(function() {
+  Promise.resolve(5)
+    .finally(function onFinally() {
+      assertEqualsAsync(0, arguments.length);
+      throw 1;
+    })
+    .finally(function onFinally() {
+      assertEqualsAsync(0, arguments.length);
+    })
+    .then(assertUnreachable, e => {
+      assertEqualsAsync(1, e);
+    });
+
+  assertAsyncRan();
+  assertAsyncRan();
+  assertAsyncRan();
+})();
+
+// resolve/finally-return-rejected-promise/finally/then
+(function() {
+  Promise.resolve(5)
+    .finally(function onFinally() {
+      assertEqualsAsync(0, arguments.length);
+      return Promise.reject(1);
+    })
+    .finally(function onFinally() {
+      assertEqualsAsync(0, arguments.length);
+    })
+    .then(assertUnreachable, e => {
+      assertEqualsAsync(1, e);
+    });
+
+  assertAsyncRan();
+  assertAsyncRan();
+  assertAsyncRan();
+})();
+
+// reject/finally/finally/then
+(function() {
+  Promise.reject(5)
+    .finally(function onFinally() {
+      assertEqualsAsync(0, arguments.length);
+    })
+    .finally(function onFinally() {
+      assertEqualsAsync(0, arguments.length);
+    })
+    .then(assertUnreachable, e => {
+      assertEqualsAsync(5, e);
+    });
+
+  assertAsyncRan();
+  assertAsyncRan();
+  assertAsyncRan();
+})();
+
+// reject/finally-throw/finally/then
+(function() {
+  Promise.reject(5)
+    .finally(function onFinally() {
+      assertEqualsAsync(0, arguments.length);
+      throw 1;
+    })
+    .finally(function onFinally() {
+      assertEqualsAsync(0, arguments.length);
+    })
+    .then(assertUnreachable, e => {
+      assertEqualsAsync(1, e);
+    });
+
+  assertAsyncRan();
+  assertAsyncRan();
+  assertAsyncRan();
+})();
+
+// reject/finally-return-rejected-promise/finally/then
+(function() {
+  Promise.reject(5)
+    .finally(function onFinally() {
+      assertEqualsAsync(0, arguments.length);
+      return Promise.reject(1);
+    })
+    .finally(function onFinally() {
+      assertEqualsAsync(0, arguments.length);
+    })
+    .then(assertUnreachable, e => {
+      assertEqualsAsync(1, e);
+    });
+
+  assertAsyncRan();
+  assertAsyncRan();
+  assertAsyncRan();
+})();
+
+// resolve/finally-deferred-resolve/then
+(function() {
+  var resolve, reject;
+  var deferred = new Promise((x, y) => {
+    resolve = x;
+    reject = y;
+  });
+  Promise.resolve(1)
+    .finally(function onFinally() {
+      assertEqualsAsync(0, arguments.length);
+      return deferred;
+    })
+    .then(
+      x => {
+        assertEqualsAsync(1, x);
+      },
+      assertUnreachable
+    );
+
+  assertAsyncRan();
+  assertAsyncRan();
+
+  resolve(5);
+})();
+
+// resolve/finally-deferred-reject/then
+(function() {
+  var resolve, reject;
+  var deferred = new Promise((x, y) => {
+    resolve = x;
+    reject = y;
+  });
+  Promise.resolve(1)
+    .finally(function onFinally() {
+      assertEqualsAsync(0, arguments.length);
+      return deferred;
+    })
+    .then(assertUnreachable, e => {
+      assertEqualsAsync(5, e);
+    });
+
+  assertAsyncRan();
+  assertAsyncRan();
+
+  reject(5);
+})();
+
+// all/finally/then
+(function() {
+  var resolve, reject;
+  var deferred = new Promise((x, y) => {
+    resolve = x;
+    reject = y;
+  });
+
+  Promise.all([deferred])
+    .finally(function onFinally() {
+      assertEqualsAsync(0, arguments.length);
+    })
+    .then(
+      ([x]) => {
+        assertEqualsAsync(1, x);
+      },
+      assertUnreachable
+    );
+
+  assertAsyncRan();
+  assertAsyncRan();
+
+  resolve(1);
+})();
+
+// race/finally/then
+(function() {
+  var resolve, reject;
+  var d1 = new Promise((x, y) => {
+    resolve = x;
+    reject = y;
+  });
+  var d2 = new Promise((x, y) => {
+    resolve = x;
+    reject = y;
+  });
+
+  Promise.race([d1, d2])
+    .finally(function onFinally() {
+      assertEqualsAsync(0, arguments.length);
+    })
+    .then(
+      x => {
+        assertEqualsAsync(1, x);
+      },
+      assertUnreachable
+    );
+
+  assertAsyncRan();
+  assertAsyncRan();
+
+  resolve(1);
+})();
+
+// resolve/finally-customthen/then
+(function() {
+  class MyPromise extends Promise {
+      then(onFulfilled, onRejected) {
+          assertEqualsAsync(5, onFulfilled);
+          assertEqualsAsync(5, onRejected);
+          return super.then(onFulfilled, onRejected);
+      }
+  }
+
+  MyPromise.resolve(3).finally(5);
+
+  assertAsyncRan();
+  assertAsyncRan();
+})();
+
+// reject/finally-customthen/then
+(function() {
+  class MyPromise extends Promise {
+      then(onFulfilled, onRejected) {
+          assertEqualsAsync(5, onFulfilled);
+          assertEqualsAsync(5, onRejected);
+          return super.then(onFulfilled, onRejected);
+      }
+  }
+
+  MyPromise.reject(3).finally(5);
+
+  assertAsyncRan();
+  assertAsyncRan();
+})();
+
+var descriptor = Object.getOwnPropertyDescriptor(Promise.prototype, 'finally');
+assertTrue(descriptor.writable);
+assertTrue(descriptor.configurable);
+assertFalse(descriptor.enumerable);
+assertEquals("finally", Promise.prototype.finally.name);
+assertEquals(1, Promise.prototype.finally.length);
+
+assertAsyncDone();