Commit 8c87ae9b authored by gsathya's avatar gsathya Committed by Commit bot

[promises] Move PromiseResolveThenableJob to c++

- Add a new container object to store the data required for
PromiseResolveThenableJob.

- Create a new runtime function to enqueue the microtask event with
the required data.

This patches causes a 4% regression in the bluebird benchmark.

BUG=v8:5343

Review-Url: https://codereview.chromium.org/2314903004
Cr-Commit-Position: refs/heads/master@{#39571}
parent 0ed1254d
...@@ -8202,8 +8202,8 @@ class Internals { ...@@ -8202,8 +8202,8 @@ class Internals {
static const int kNodeIsPartiallyDependentShift = 4; static const int kNodeIsPartiallyDependentShift = 4;
static const int kNodeIsActiveShift = 4; static const int kNodeIsActiveShift = 4;
static const int kJSObjectType = 0xb8; static const int kJSObjectType = 0xb9;
static const int kJSApiObjectType = 0xb7; static const int kJSApiObjectType = 0xb8;
static const int kFirstNonstringType = 0x80; static const int kFirstNonstringType = 0x80;
static const int kOddballType = 0x83; static const int kOddballType = 0x83;
static const int kForeignType = 0x87; static const int kForeignType = 0x87;
......
...@@ -259,6 +259,7 @@ AstType::bitset AstBitsetType::Lub(i::Map* map) { ...@@ -259,6 +259,7 @@ AstType::bitset AstBitsetType::Lub(i::Map* map) {
case ACCESS_CHECK_INFO_TYPE: case ACCESS_CHECK_INFO_TYPE:
case INTERCEPTOR_INFO_TYPE: case INTERCEPTOR_INFO_TYPE:
case CALL_HANDLER_INFO_TYPE: case CALL_HANDLER_INFO_TYPE:
case PROMISE_CONTAINER_TYPE:
case FUNCTION_TEMPLATE_INFO_TYPE: case FUNCTION_TEMPLATE_INFO_TYPE:
case OBJECT_TEMPLATE_INFO_TYPE: case OBJECT_TEMPLATE_INFO_TYPE:
case SIGNATURE_INFO_TYPE: case SIGNATURE_INFO_TYPE:
......
...@@ -264,6 +264,7 @@ Type::bitset BitsetType::Lub(i::Map* map) { ...@@ -264,6 +264,7 @@ Type::bitset BitsetType::Lub(i::Map* map) {
case TYPE_FEEDBACK_INFO_TYPE: case TYPE_FEEDBACK_INFO_TYPE:
case ALIASED_ARGUMENTS_ENTRY_TYPE: case ALIASED_ARGUMENTS_ENTRY_TYPE:
case BOX_TYPE: case BOX_TYPE:
case PROMISE_CONTAINER_TYPE:
case DEBUG_INFO_TYPE: case DEBUG_INFO_TYPE:
case BREAK_POINT_INFO_TYPE: case BREAK_POINT_INFO_TYPE:
case CELL_TYPE: case CELL_TYPE:
......
...@@ -92,7 +92,6 @@ Handle<Box> Factory::NewBox(Handle<Object> value) { ...@@ -92,7 +92,6 @@ Handle<Box> Factory::NewBox(Handle<Object> value) {
return result; return result;
} }
Handle<PrototypeInfo> Factory::NewPrototypeInfo() { Handle<PrototypeInfo> Factory::NewPrototypeInfo() {
Handle<PrototypeInfo> result = Handle<PrototypeInfo> result =
Handle<PrototypeInfo>::cast(NewStruct(PROTOTYPE_INFO_TYPE)); Handle<PrototypeInfo>::cast(NewStruct(PROTOTYPE_INFO_TYPE));
...@@ -899,6 +898,20 @@ Handle<Struct> Factory::NewStruct(InstanceType type) { ...@@ -899,6 +898,20 @@ Handle<Struct> Factory::NewStruct(InstanceType type) {
Struct); Struct);
} }
Handle<PromiseContainer> Factory::NewPromiseContainer(
Handle<JSReceiver> thenable, Handle<JSFunction> then,
Handle<JSFunction> resolve, Handle<JSFunction> reject,
Handle<Object> before_debug_event, Handle<Object> after_debug_event) {
Handle<PromiseContainer> result =
Handle<PromiseContainer>::cast(NewStruct(PROMISE_CONTAINER_TYPE));
result->set_thenable(*thenable);
result->set_then(*then);
result->set_resolve(*resolve);
result->set_reject(*reject);
result->set_before_debug_event(*before_debug_event);
result->set_after_debug_event(*after_debug_event);
return result;
}
Handle<AliasedArgumentsEntry> Factory::NewAliasedArgumentsEntry( Handle<AliasedArgumentsEntry> Factory::NewAliasedArgumentsEntry(
int aliased_context_slot) { int aliased_context_slot) {
......
...@@ -60,6 +60,12 @@ class Factory final { ...@@ -60,6 +60,12 @@ class Factory final {
// Create a new boxed value. // Create a new boxed value.
Handle<Box> NewBox(Handle<Object> value); Handle<Box> NewBox(Handle<Object> value);
// Create a new PromiseContainer struct.
Handle<PromiseContainer> NewPromiseContainer(
Handle<JSReceiver> thenable, Handle<JSFunction> then,
Handle<JSFunction> resolve, Handle<JSFunction> reject,
Handle<Object> before_debug_event, Handle<Object> after_debug_event);
// Create a new PrototypeInfo struct. // Create a new PrototypeInfo struct.
Handle<PrototypeInfo> NewPrototypeInfo(); Handle<PrototypeInfo> NewPrototypeInfo();
......
...@@ -2968,9 +2968,44 @@ void Isolate::ReportPromiseReject(Handle<JSObject> promise, ...@@ -2968,9 +2968,44 @@ void Isolate::ReportPromiseReject(Handle<JSObject> promise,
v8::Utils::StackTraceToLocal(stack_trace))); v8::Utils::StackTraceToLocal(stack_trace)));
} }
void Isolate::PromiseResolveThenableJob(Handle<PromiseContainer> container,
MaybeHandle<Object>* result,
MaybeHandle<Object>* maybe_exception) {
if (debug()->is_active()) {
Handle<Object> before_debug_event(container->before_debug_event(), this);
if (before_debug_event->IsJSObject()) {
debug()->OnAsyncTaskEvent(Handle<JSObject>::cast(before_debug_event));
}
}
Handle<JSReceiver> thenable(container->thenable(), this);
Handle<JSFunction> resolve(container->resolve(), this);
Handle<JSFunction> reject(container->reject(), this);
Handle<JSFunction> then(container->then(), this);
Handle<Object> argv[] = {resolve, reject};
*result = Execution::TryCall(this, then, thenable, arraysize(argv), argv,
maybe_exception);
Handle<Object> reason;
if (maybe_exception->ToHandle(&reason)) {
DCHECK(result->is_null());
Handle<Object> reason_arg[] = {reason};
*result =
Execution::TryCall(this, reject, factory()->undefined_value(),
arraysize(reason_arg), reason_arg, maybe_exception);
}
if (debug()->is_active()) {
Handle<Object> after_debug_event(container->after_debug_event(), this);
if (after_debug_event->IsJSObject()) {
debug()->OnAsyncTaskEvent(Handle<JSObject>::cast(after_debug_event));
}
}
}
void Isolate::EnqueueMicrotask(Handle<Object> microtask) { void Isolate::EnqueueMicrotask(Handle<Object> microtask) {
DCHECK(microtask->IsJSFunction() || microtask->IsCallHandlerInfo()); DCHECK(microtask->IsJSFunction() || microtask->IsCallHandlerInfo() ||
microtask->IsPromiseContainer());
Handle<FixedArray> queue(heap()->microtask_queue(), this); Handle<FixedArray> queue(heap()->microtask_queue(), this);
int num_tasks = pending_microtask_count(); int num_tasks = pending_microtask_count();
DCHECK(num_tasks <= queue->length()); DCHECK(num_tasks <= queue->length());
...@@ -3012,18 +3047,40 @@ void Isolate::RunMicrotasksInternal() { ...@@ -3012,18 +3047,40 @@ void Isolate::RunMicrotasksInternal() {
Isolate* isolate = this; Isolate* isolate = this;
FOR_WITH_HANDLE_SCOPE(isolate, int, i = 0, i, i < num_tasks, i++, { FOR_WITH_HANDLE_SCOPE(isolate, int, i = 0, i, i < num_tasks, i++, {
Handle<Object> microtask(queue->get(i), this); Handle<Object> microtask(queue->get(i), this);
if (microtask->IsJSFunction()) {
Handle<JSFunction> microtask_function = if (microtask->IsCallHandlerInfo()) {
Handle<JSFunction>::cast(microtask); Handle<CallHandlerInfo> callback_info =
Handle<CallHandlerInfo>::cast(microtask);
v8::MicrotaskCallback callback =
v8::ToCData<v8::MicrotaskCallback>(callback_info->callback());
void* data = v8::ToCData<void*>(callback_info->data());
callback(data);
} else {
SaveContext save(this); SaveContext save(this);
set_context(microtask_function->context()->native_context()); Context* context =
microtask->IsJSFunction()
? Handle<JSFunction>::cast(microtask)->context()
: Handle<PromiseContainer>::cast(microtask)->then()->context();
set_context(context->native_context());
handle_scope_implementer_->EnterMicrotaskContext( handle_scope_implementer_->EnterMicrotaskContext(
handle(microtask_function->context(), this)); Handle<Context>(context, this));
MaybeHandle<Object> result;
MaybeHandle<Object> maybe_exception; MaybeHandle<Object> maybe_exception;
MaybeHandle<Object> result = Execution::TryCall(
this, microtask_function, factory()->undefined_value(), 0, NULL, if (microtask->IsJSFunction()) {
&maybe_exception); Handle<JSFunction> microtask_function =
Handle<JSFunction>::cast(microtask);
result = Execution::TryCall(this, microtask_function,
factory()->undefined_value(), 0, NULL,
&maybe_exception);
} else {
PromiseResolveThenableJob(Handle<PromiseContainer>::cast(microtask),
&result, &maybe_exception);
}
handle_scope_implementer_->LeaveMicrotaskContext(); handle_scope_implementer_->LeaveMicrotaskContext();
// If execution is terminating, just bail out. // If execution is terminating, just bail out.
if (result.is_null() && maybe_exception.is_null()) { if (result.is_null() && maybe_exception.is_null()) {
// Clear out any remaining callbacks in the queue. // Clear out any remaining callbacks in the queue.
...@@ -3031,13 +3088,6 @@ void Isolate::RunMicrotasksInternal() { ...@@ -3031,13 +3088,6 @@ void Isolate::RunMicrotasksInternal() {
set_pending_microtask_count(0); set_pending_microtask_count(0);
return; return;
} }
} else {
Handle<CallHandlerInfo> callback_info =
Handle<CallHandlerInfo>::cast(microtask);
v8::MicrotaskCallback callback =
v8::ToCData<v8::MicrotaskCallback>(callback_info->callback());
void* data = v8::ToCData<void*>(callback_info->data());
callback(data);
} }
}); });
} }
......
...@@ -1114,6 +1114,9 @@ class Isolate { ...@@ -1114,6 +1114,9 @@ class Isolate {
void ReportPromiseReject(Handle<JSObject> promise, Handle<Object> value, void ReportPromiseReject(Handle<JSObject> promise, Handle<Object> value,
v8::PromiseRejectEvent event); v8::PromiseRejectEvent event);
void PromiseResolveThenableJob(Handle<PromiseContainer> container,
MaybeHandle<Object>* result,
MaybeHandle<Object>* maybe_exception);
void EnqueueMicrotask(Handle<Object> microtask); void EnqueueMicrotask(Handle<Object> microtask);
void RunMicrotasks(); void RunMicrotasks();
bool IsRunningMicrotasks() const { return is_running_microtasks_; } bool IsRunningMicrotasks() const { return is_running_microtasks_; }
......
...@@ -284,34 +284,34 @@ function ResolvePromise(promise, resolution) { ...@@ -284,34 +284,34 @@ function ResolvePromise(promise, resolution) {
} }
if (IS_CALLABLE(then)) { if (IS_CALLABLE(then)) {
// PromiseResolveThenableJob var callbacks = CreateResolvingFunctions(promise, false);
var id; var id, before_debug_event, after_debug_event;
var name = "PromiseResolveThenableJob";
var instrumenting = DEBUG_IS_ACTIVE; var instrumenting = DEBUG_IS_ACTIVE;
if (instrumenting && IsPromise(resolution)) {
// Mark the dependency of the new promise on the resolution
SET_PRIVATE(resolution, promiseHandledBySymbol, promise);
}
%EnqueueMicrotask(function() {
if (instrumenting) {
%DebugAsyncTaskEvent({ type: "willHandle", id: id, name: name });
}
// These resolving functions simply forward the exception, so
// don't create a new debugEvent.
var callbacks = CreateResolvingFunctions(promise, false);
try {
%_Call(then, resolution, callbacks.resolve, callbacks.reject);
} catch (e) {
%_Call(callbacks.reject, UNDEFINED, e);
}
if (instrumenting) {
%DebugAsyncTaskEvent({ type: "didHandle", id: id, name: name });
}
});
if (instrumenting) { if (instrumenting) {
if (IsPromise(resolution)) {
// Mark the dependency of the new promise on the resolution
SET_PRIVATE(resolution, promiseHandledBySymbol, promise);
}
id = ++lastMicrotaskId; id = ++lastMicrotaskId;
%DebugAsyncTaskEvent({ type: "enqueue", id: id, name: name }); before_debug_event = {
type: "willHandle",
id: id,
name: "PromiseResolveThenableJob"
};
after_debug_event = {
type: "didHandle",
id: id,
name: "PromiseResolveThenableJob"
};
%DebugAsyncTaskEvent({
type: "enqueue",
id: id,
name: "PromiseResolveThenableJob"
});
} }
%EnqueuePromiseResolveThenableJob(
resolution, then, callbacks.resolve, callbacks.reject,
before_debug_event, after_debug_event);
return; return;
} }
} }
...@@ -655,7 +655,7 @@ utils.InstallFunctions(GlobalPromise.prototype, DONT_ENUM, [ ...@@ -655,7 +655,7 @@ utils.InstallFunctions(GlobalPromise.prototype, DONT_ENUM, [
"promise_has_user_defined_reject_handler", PromiseHasUserDefinedRejectHandler, "promise_has_user_defined_reject_handler", PromiseHasUserDefinedRejectHandler,
"promise_reject", DoRejectPromise, "promise_reject", DoRejectPromise,
"promise_resolve", ResolvePromise, "promise_resolve", ResolvePromise,
"promise_then", PromiseThen, "promise_then", PromiseThen
]); ]);
// This allows extras to create promises quickly without building extra // This allows extras to create promises quickly without building extra
......
...@@ -900,6 +900,16 @@ void Box::BoxVerify() { ...@@ -900,6 +900,16 @@ void Box::BoxVerify() {
value()->ObjectVerify(); value()->ObjectVerify();
} }
void PromiseContainer::PromiseContainerVerify() {
CHECK(IsPromiseContainer());
thenable()->ObjectVerify();
then()->ObjectVerify();
resolve()->ObjectVerify();
reject()->ObjectVerify();
before_debug_event()->ObjectVerify();
after_debug_event()->ObjectVerify();
}
void Module::ModuleVerify() { void Module::ModuleVerify() {
CHECK(IsModule()); CHECK(IsModule());
CHECK(code()->IsSharedFunctionInfo() || code()->IsJSFunction()); CHECK(code()->IsSharedFunctionInfo() || code()->IsJSFunction());
......
...@@ -5650,6 +5650,13 @@ ACCESSORS(AccessorInfo, data, Object, kDataOffset) ...@@ -5650,6 +5650,13 @@ ACCESSORS(AccessorInfo, data, Object, kDataOffset)
ACCESSORS(Box, value, Object, kValueOffset) ACCESSORS(Box, value, Object, kValueOffset)
ACCESSORS(PromiseContainer, thenable, JSReceiver, kThenableOffset)
ACCESSORS(PromiseContainer, then, JSFunction, kThenOffset)
ACCESSORS(PromiseContainer, resolve, JSFunction, kResolveOffset)
ACCESSORS(PromiseContainer, reject, JSFunction, kRejectOffset)
ACCESSORS(PromiseContainer, before_debug_event, Object, kBeforeDebugEventOffset)
ACCESSORS(PromiseContainer, after_debug_event, Object, kAfterDebugEventOffset)
Map* PrototypeInfo::ObjectCreateMap() { Map* PrototypeInfo::ObjectCreateMap() {
return Map::cast(WeakCell::cast(object_create_map())->value()); return Map::cast(WeakCell::cast(object_create_map())->value());
} }
......
...@@ -1155,6 +1155,17 @@ void Box::BoxPrint(std::ostream& os) { // NOLINT ...@@ -1155,6 +1155,17 @@ void Box::BoxPrint(std::ostream& os) { // NOLINT
os << "\n"; os << "\n";
} }
void PromiseContainer::PromiseContainerPrint(std::ostream& os) { // NOLINT
HeapObject::PrintHeader(os, "PromiseContainer");
os << "\n - thenable: " << Brief(thenable());
os << "\n - then: " << Brief(then());
os << "\n - resolve: " << Brief(resolve());
os << "\n - reject: " << Brief(reject());
os << "\n - before debug event: " << Brief(before_debug_event());
os << "\n - after debug event: " << Brief(after_debug_event());
os << "\n";
}
void Module::ModulePrint(std::ostream& os) { // NOLINT void Module::ModulePrint(std::ostream& os) { // NOLINT
HeapObject::PrintHeader(os, "Module"); HeapObject::PrintHeader(os, "Module");
os << "\n - code: " << Brief(code()); os << "\n - code: " << Brief(code());
......
...@@ -396,6 +396,7 @@ const int kStubMinorKeyBits = kSmiValueSize - kStubMajorKeyBits - 1; ...@@ -396,6 +396,7 @@ const int kStubMinorKeyBits = kSmiValueSize - kStubMajorKeyBits - 1;
V(TYPE_FEEDBACK_INFO_TYPE) \ V(TYPE_FEEDBACK_INFO_TYPE) \
V(ALIASED_ARGUMENTS_ENTRY_TYPE) \ V(ALIASED_ARGUMENTS_ENTRY_TYPE) \
V(BOX_TYPE) \ V(BOX_TYPE) \
V(PROMISE_CONTAINER_TYPE) \
V(PROTOTYPE_INFO_TYPE) \ V(PROTOTYPE_INFO_TYPE) \
V(CONTEXT_EXTENSION_TYPE) \ V(CONTEXT_EXTENSION_TYPE) \
V(MODULE_TYPE) \ V(MODULE_TYPE) \
...@@ -500,6 +501,7 @@ const int kStubMinorKeyBits = kSmiValueSize - kStubMajorKeyBits - 1; ...@@ -500,6 +501,7 @@ const int kStubMinorKeyBits = kSmiValueSize - kStubMajorKeyBits - 1;
// manually. // manually.
#define STRUCT_LIST(V) \ #define STRUCT_LIST(V) \
V(BOX, Box, box) \ V(BOX, Box, box) \
V(PROMISE_CONTAINER, PromiseContainer, promise_container) \
V(ACCESSOR_INFO, AccessorInfo, accessor_info) \ V(ACCESSOR_INFO, AccessorInfo, accessor_info) \
V(ACCESSOR_PAIR, AccessorPair, accessor_pair) \ V(ACCESSOR_PAIR, AccessorPair, accessor_pair) \
V(ACCESS_CHECK_INFO, AccessCheckInfo, access_check_info) \ V(ACCESS_CHECK_INFO, AccessCheckInfo, access_check_info) \
...@@ -681,6 +683,7 @@ enum InstanceType { ...@@ -681,6 +683,7 @@ enum InstanceType {
TYPE_FEEDBACK_INFO_TYPE, TYPE_FEEDBACK_INFO_TYPE,
ALIASED_ARGUMENTS_ENTRY_TYPE, ALIASED_ARGUMENTS_ENTRY_TYPE,
BOX_TYPE, BOX_TYPE,
PROMISE_CONTAINER_TYPE,
DEBUG_INFO_TYPE, DEBUG_INFO_TYPE,
BREAK_POINT_INFO_TYPE, BREAK_POINT_INFO_TYPE,
FIXED_ARRAY_TYPE, FIXED_ARRAY_TYPE,
...@@ -6644,6 +6647,34 @@ class Struct: public HeapObject { ...@@ -6644,6 +6647,34 @@ class Struct: public HeapObject {
DECLARE_CAST(Struct) DECLARE_CAST(Struct)
}; };
// A container struct to hold state required for
// PromiseResolveThenableJob. {before, after}_debug_event could
// potentially be undefined if the debugger is turned off.
class PromiseContainer : public Struct {
public:
DECL_ACCESSORS(thenable, JSReceiver)
DECL_ACCESSORS(then, JSFunction)
DECL_ACCESSORS(resolve, JSFunction)
DECL_ACCESSORS(reject, JSFunction)
DECL_ACCESSORS(before_debug_event, Object)
DECL_ACCESSORS(after_debug_event, Object)
static const int kThenableOffset = Struct::kHeaderSize;
static const int kThenOffset = kThenableOffset + kPointerSize;
static const int kResolveOffset = kThenOffset + kPointerSize;
static const int kRejectOffset = kResolveOffset + kPointerSize;
static const int kBeforeDebugEventOffset = kRejectOffset + kPointerSize;
static const int kAfterDebugEventOffset =
kBeforeDebugEventOffset + kPointerSize;
static const int kSize = kAfterDebugEventOffset + kPointerSize;
DECLARE_CAST(PromiseContainer)
DECLARE_PRINTER(PromiseContainer)
DECLARE_VERIFIER(PromiseContainer)
private:
DISALLOW_IMPLICIT_CONSTRUCTORS(PromiseContainer);
};
// A simple one-element struct, useful where smis need to be boxed. // A simple one-element struct, useful where smis need to be boxed.
class Box : public Struct { class Box : public Struct {
......
...@@ -554,6 +554,21 @@ RUNTIME_FUNCTION(Runtime_GetAndResetRuntimeCallStats) { ...@@ -554,6 +554,21 @@ RUNTIME_FUNCTION(Runtime_GetAndResetRuntimeCallStats) {
} }
} }
RUNTIME_FUNCTION(Runtime_EnqueuePromiseResolveThenableJob) {
HandleScope scope(isolate);
DCHECK(args.length() == 6);
CONVERT_ARG_HANDLE_CHECKED(JSReceiver, resolution, 0);
CONVERT_ARG_HANDLE_CHECKED(JSFunction, then, 1);
CONVERT_ARG_HANDLE_CHECKED(JSFunction, resolve, 2);
CONVERT_ARG_HANDLE_CHECKED(JSFunction, reject, 3);
CONVERT_ARG_HANDLE_CHECKED(Object, before_debug_event, 4);
CONVERT_ARG_HANDLE_CHECKED(Object, after_debug_event, 5);
Handle<PromiseContainer> container = isolate->factory()->NewPromiseContainer(
resolution, then, resolve, reject, before_debug_event, after_debug_event);
isolate->EnqueueMicrotask(container);
return isolate->heap()->undefined_value();
}
RUNTIME_FUNCTION(Runtime_EnqueueMicrotask) { RUNTIME_FUNCTION(Runtime_EnqueueMicrotask) {
HandleScope scope(isolate); HandleScope scope(isolate);
DCHECK(args.length() == 1); DCHECK(args.length() == 1);
......
...@@ -290,6 +290,7 @@ namespace internal { ...@@ -290,6 +290,7 @@ namespace internal {
F(CheckIsBootstrapping, 0, 1) \ F(CheckIsBootstrapping, 0, 1) \
F(CreateListFromArrayLike, 1, 1) \ F(CreateListFromArrayLike, 1, 1) \
F(EnqueueMicrotask, 1, 1) \ F(EnqueueMicrotask, 1, 1) \
F(EnqueuePromiseResolveThenableJob, 6, 1) \
F(GetAndResetRuntimeCallStats, -1 /* <= 2 */, 1) \ F(GetAndResetRuntimeCallStats, -1 /* <= 2 */, 1) \
F(ExportExperimentalFromRuntime, 1, 1) \ F(ExportExperimentalFromRuntime, 1, 1) \
F(ExportFromRuntime, 1, 1) \ F(ExportFromRuntime, 1, 1) \
......
// Copyright 2015 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: --allow-natives-syntax
function assertAsync(b, s) {
if (!b) {
%AbortJS(" FAILED!")
}
}
var handler = {
get: function(target, name) {
if (name === 'then') {
return (val) => Promise.prototype.then.call(target, val);
}
}
};
var target = new Promise(r => r(42));
var p = new Proxy(target, handler);
Promise.resolve(p).then((val) => assertAsync(val === 42));
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment