From: Alexander Borisov Date: Tue, 17 Dec 2019 07:35:11 +0000 (+0300) Subject: Introduced the Promise object. X-Git-Url: http://git.kaiwu.me/postgresql/log/contrib/postgres_fdw/%7B@url%7D?a=commitdiff_plain;h=13a970cac8c46cfc593f1299499ef90459215641;p=njs.git Introduced the Promise object. Implemented according to the specification without: Promise.all(), Promise.allSettled(), Promise.race(). This closes #39 issue on GitHub. --- diff --git a/auto/sources b/auto/sources index c289fe39..d7b39ece 100644 --- a/auto/sources +++ b/auto/sources @@ -53,6 +53,7 @@ NJS_LIB_SRCS=" \ src/njs_generator.c \ src/njs_disassembler.c \ src/njs_array_buffer.c \ + src/njs_promise.c \ " NJS_LIB_TEST_SRCS=" \ diff --git a/src/njs_builtin.c b/src/njs_builtin.c index 9c6b808e..4825a581 100644 --- a/src/njs_builtin.c +++ b/src/njs_builtin.c @@ -66,6 +66,7 @@ static const njs_object_type_init_t *const &njs_function_type_init, &njs_regexp_type_init, &njs_date_type_init, + &njs_promise_type_init, /* Hidden types. */ @@ -1171,6 +1172,15 @@ static const njs_object_prop_t njs_global_this_object_properties[] = .configurable = 1, }, + { + .type = NJS_PROPERTY_HANDLER, + .name = njs_string("Promise"), + .value = njs_prop_handler2(njs_top_level_constructor, + NJS_OBJ_TYPE_PROMISE, NJS_PROMISE_HASH), + .writable = 1, + .configurable = 1, + }, + { .type = NJS_PROPERTY_HANDLER, .name = njs_string("Error"), diff --git a/src/njs_date.c b/src/njs_date.c index cefe7555..41c84bd7 100644 --- a/src/njs_date.c +++ b/src/njs_date.c @@ -1840,3 +1840,4 @@ const njs_object_type_init_t njs_date_type_init = { .prototype_props = &njs_date_prototype_init, .prototype_value = { .object = { .type = NJS_OBJECT } }, }; + diff --git a/src/njs_event.h b/src/njs_event.h index 64baad86..9ca3ac08 100644 --- a/src/njs_event.h +++ b/src/njs_event.h @@ -16,6 +16,8 @@ #define njs_posted_events(vm) (!njs_queue_is_empty(&(vm)->posted_events)) +#define njs_promise_events(vm) (!njs_queue_is_empty(&(vm)->promise_events)) + typedef struct { njs_function_t *function; diff --git a/src/njs_function.c b/src/njs_function.c index 46850e25..9e6cfba7 100644 --- a/src/njs_function.c +++ b/src/njs_function.c @@ -539,14 +539,14 @@ njs_function_frame_alloc(njs_vm_t *vm, size_t size) njs_int_t -njs_function_call(njs_vm_t *vm, njs_function_t *function, +njs_function_call2(njs_vm_t *vm, njs_function_t *function, const njs_value_t *this, const njs_value_t *args, - njs_uint_t nargs, njs_value_t *retval) + njs_uint_t nargs, njs_value_t *retval, njs_bool_t ctor) { njs_int_t ret; njs_value_t dst njs_aligned(16); - ret = njs_function_frame(vm, function, this, args, nargs, 0); + ret = njs_function_frame(vm, function, this, args, nargs, ctor); if (njs_slow_path(ret != NJS_OK)) { return ret; } diff --git a/src/njs_function.h b/src/njs_function.h index a659a452..d736508b 100644 --- a/src/njs_function.h +++ b/src/njs_function.h @@ -115,9 +115,9 @@ njs_int_t njs_function_native_frame(njs_vm_t *vm, njs_function_t *function, njs_int_t njs_function_lambda_frame(njs_vm_t *vm, njs_function_t *function, const njs_value_t *this, const njs_value_t *args, njs_uint_t nargs, njs_bool_t ctor); -njs_int_t njs_function_call(njs_vm_t *vm, njs_function_t *function, - const njs_value_t *this, const njs_value_t *args, njs_uint_t nargs, - njs_value_t *retval); +njs_int_t njs_function_call2(njs_vm_t *vm, njs_function_t *function, + const njs_value_t *this, const njs_value_t *args, + njs_uint_t nargs, njs_value_t *retval, njs_bool_t ctor); njs_int_t njs_function_lambda_call(njs_vm_t *vm); njs_int_t njs_function_native_call(njs_vm_t *vm); void njs_function_frame_free(njs_vm_t *vm, njs_native_frame_t *frame); @@ -185,12 +185,21 @@ njs_function_frame_invoke(njs_vm_t *vm, njs_index_t retval) } +njs_inline njs_int_t +njs_function_call(njs_vm_t *vm, njs_function_t *function, + const njs_value_t *this, const njs_value_t *args, + njs_uint_t nargs, njs_value_t *retval) +{ + return njs_function_call2(vm, function, this, args, nargs, retval, 0); +} + + njs_inline njs_int_t njs_function_apply(njs_vm_t *vm, njs_function_t *function, const njs_value_t *args, njs_uint_t nargs, njs_value_t *retval) { - return njs_function_call(vm, function, &args[0], &args[1], nargs - 1, - retval); + return njs_function_call2(vm, function, &args[0], &args[1], nargs - 1, + retval, 0); } diff --git a/src/njs_json.c b/src/njs_json.c index e740070f..f8e10eeb 100644 --- a/src/njs_json.c +++ b/src/njs_json.c @@ -1904,6 +1904,7 @@ njs_dump_is_object(const njs_value_t *value) return (value->type == NJS_OBJECT && !njs_object(value)->error_data) || (value->type == NJS_ARRAY) || (value->type == NJS_ARRAY_BUFFER) + || (value->type == NJS_PROMISE) || (value->type == NJS_OBJECT_VALUE) || njs_dump_is_external_object(value); } diff --git a/src/njs_main.h b/src/njs_main.h index 5fc40c74..6bc39614 100644 --- a/src/njs_main.h +++ b/src/njs_main.h @@ -65,6 +65,7 @@ #include #include #include +#include #include #include diff --git a/src/njs_object.c b/src/njs_object.c index c42bc152..41fa88f8 100644 --- a/src/njs_object.c +++ b/src/njs_object.c @@ -2410,6 +2410,7 @@ njs_object_prototype_to_string(njs_vm_t *vm, njs_value_t *args, &njs_object_regexp_string, &njs_object_date_string, &njs_object_object_string, + &njs_object_object_string, &njs_object_array_buffer_string, }; diff --git a/src/njs_object_hash.h b/src/njs_object_hash.h index 9049ba3f..1d845dfa 100644 --- a/src/njs_object_hash.h +++ b/src/njs_object_hash.h @@ -88,6 +88,17 @@ 'D'), 'a'), 't'), 'e') +#define NJS_PROMISE_HASH \ + njs_djb_hash_add( \ + njs_djb_hash_add( \ + njs_djb_hash_add( \ + njs_djb_hash_add( \ + njs_djb_hash_add( \ + njs_djb_hash_add( \ + njs_djb_hash_add(NJS_DJB_HASH_INIT, \ + 'P'), 'r'), 'o'), 'm'), 'i'), 's'), 'e') + + #define NJS_ENUMERABLE_HASH \ njs_djb_hash_add( \ njs_djb_hash_add( \ diff --git a/src/njs_promise.c b/src/njs_promise.c new file mode 100644 index 00000000..6c888893 --- /dev/null +++ b/src/njs_promise.c @@ -0,0 +1,1221 @@ + +/* + * Copyright (C) Nginx, Inc. + */ + +#include + + +typedef enum { + NJS_PROMISE_PENDING = 0, + NJS_PROMISE_FULFILL, + NJS_PROMISE_REJECTED +} njs_promise_type_t; + +typedef struct { + njs_promise_type_t state; + njs_value_t result; + njs_queue_t fulfill_queue; + njs_queue_t reject_queue; + njs_bool_t is_handled; +} njs_promise_data_t; + +typedef struct { + njs_value_t promise; + njs_value_t resolve; + njs_value_t reject; +} njs_promise_capability_t; + +typedef struct { + njs_promise_capability_t *capability; + njs_promise_type_t type; + njs_queue_link_t link; + njs_value_t handler; +} njs_promise_reaction_t; + +typedef struct { + njs_value_t promise; + njs_value_t finally; + njs_value_t constructor; + njs_bool_t resolved; + njs_bool_t *resolved_ref; + njs_promise_capability_t *capability; +} njs_promise_context_t; + + +static njs_promise_t *njs_promise_constructor_call(njs_vm_t *vm, + njs_function_t *function); +static njs_int_t njs_promise_create_resolving_functions(njs_vm_t *vm, + njs_promise_t *promise, njs_value_t *dst); +static njs_int_t njs_promise_value_constructor(njs_vm_t *vm, njs_value_t *value, + njs_value_t *dst); +static njs_int_t njs_promise_capability_executor(njs_vm_t *vm, + njs_value_t *args, njs_uint_t nargs, njs_index_t retval); +static njs_int_t njs_promise_resolve_function(njs_vm_t *vm, njs_value_t *args, + njs_uint_t nargs, njs_index_t retval); +static njs_promise_t *njs_promise_resolve(njs_vm_t *vm, + njs_value_t *constructor, njs_value_t *x); +static njs_int_t njs_promise_reject_function(njs_vm_t *vm, njs_value_t *args, + njs_uint_t nargs, njs_index_t retval); +static njs_int_t njs_promise_perform_then(njs_vm_t *vm, njs_value_t *value, + njs_value_t *fulfilled, njs_value_t *rejected, + njs_promise_capability_t *capability); +static njs_int_t njs_promise_then_finally_function(njs_vm_t *vm, + njs_value_t *args, njs_uint_t nargs, njs_index_t unused); +static njs_int_t njs_promise_catch_finally_function(njs_vm_t *vm, + njs_value_t *args, njs_uint_t nargs, njs_index_t unused); +static njs_int_t njs_promise_reaction_job(njs_vm_t *vm, njs_value_t *args, + njs_uint_t nargs, njs_index_t unused); +static njs_int_t njs_promise_resolve_thenable_job(njs_vm_t *vm, + njs_value_t *args, njs_uint_t nargs, njs_index_t unused); + + +static njs_promise_t * +njs_promise_alloc(njs_vm_t *vm) +{ + njs_promise_t *promise; + njs_promise_data_t *data; + + promise = njs_mp_alloc(vm->mem_pool, sizeof(njs_promise_t) + + sizeof(njs_promise_data_t)); + if (njs_slow_path(promise == NULL)) { + goto memory_error; + } + + njs_lvlhsh_init(&promise->object.hash); + njs_lvlhsh_init(&promise->object.shared_hash); + promise->object.type = NJS_PROMISE; + promise->object.shared = 0; + promise->object.extensible = 1; + promise->object.__proto__ = &vm->prototypes[NJS_OBJ_TYPE_PROMISE].object; + + data = (njs_promise_data_t *) ((uint8_t *) promise + sizeof(njs_promise_t)); + + data->state = NJS_PROMISE_PENDING; + data->is_handled = 0; + + njs_queue_init(&data->fulfill_queue); + njs_queue_init(&data->reject_queue); + + njs_set_promise(&vm->retval, promise); + njs_value_data_set(&promise->value, data); + + return promise; + +memory_error: + + njs_memory_error(vm); + + return NULL; +} + + +njs_int_t +njs_promise_constructor(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, + njs_index_t unused) +{ + njs_promise_t *promise; + njs_function_t *function; + + if (njs_slow_path(!vm->top_frame->ctor)) { + njs_type_error(vm, "the Promise constructor must be called with new"); + return NJS_ERROR; + } + + if (njs_slow_path(!njs_is_function(njs_arg(args, nargs, 1)))) { + njs_type_error(vm, "unexpected arguments"); + return NJS_ERROR; + } + + function = njs_function(njs_argument(args, 1)); + + promise = njs_promise_constructor_call(vm, function); + if (njs_slow_path(promise == NULL)) { + return NJS_ERROR; + } + + njs_set_promise(&vm->retval, promise); + + return NJS_OK; +} + + +static njs_promise_t * +njs_promise_constructor_call(njs_vm_t *vm, njs_function_t *function) +{ + njs_int_t ret; + njs_value_t retval, arguments[2]; + njs_promise_t *promise; + + promise = njs_promise_alloc(vm); + if (njs_slow_path(promise == NULL)) { + return NULL; + } + + ret = njs_promise_create_resolving_functions(vm, promise, arguments); + if (njs_slow_path(ret != NJS_OK)) { + return NULL; + } + + ret = njs_function_call(vm, function, &njs_value_undefined, arguments, 2, + &retval); + if (njs_slow_path(ret != NJS_OK)) { + if (njs_slow_path(njs_is_memory_error(vm, &vm->retval))) { + return NULL; + } + + ret = njs_function_call(vm, njs_function(&arguments[1]), + &njs_value_undefined, &vm->retval, 1, &retval); + if (njs_slow_path(ret != NJS_OK)) { + return NULL; + } + } + + return promise; +} + + +static njs_function_t * +njs_promise_create_function(njs_vm_t *vm) +{ + njs_function_t *function; + njs_promise_context_t *context; + + function = njs_mp_zalloc(vm->mem_pool, sizeof(njs_function_t)); + if (njs_slow_path(function == NULL)) { + goto memory_error; + } + + context = njs_mp_zalloc(vm->mem_pool, sizeof(njs_promise_context_t)); + if (njs_slow_path(context == NULL)) { + njs_mp_free(vm->mem_pool, function); + goto memory_error; + } + + function->object.__proto__ = &vm->prototypes[NJS_OBJ_TYPE_FUNCTION].object; + function->object.shared_hash = vm->shared->arrow_instance_hash; + function->object.type = NJS_FUNCTION; + function->object.shared = 0; + function->object.extensible = 1; + function->args_offset = 1; + function->native = 1; + function->context = context; + + return function; + +memory_error: + + njs_memory_error(vm); + + return NULL; +} + + +static njs_int_t +njs_promise_create_resolving_functions(njs_vm_t *vm, njs_promise_t *promise, + njs_value_t *dst) +{ + unsigned i; + njs_function_t *function; + njs_promise_context_t *context, *resolve_context; + + i = 0; + + /* Some compilers give at error an uninitialized context if using for. */ + do { + function = njs_promise_create_function(vm); + if (njs_slow_path(function == NULL)) { + return NJS_ERROR; + } + + function->args_count = 1; + + context = function->context; + context->resolved_ref = &context->resolved; + + njs_set_promise(&context->promise, promise); + njs_set_function(&dst[i], function); + + } while (++i < 2); + + njs_function(&dst[0])->u.native = njs_promise_resolve_function; + njs_function(&dst[1])->u.native = njs_promise_reject_function; + + resolve_context = njs_function(&dst[0])->context; + resolve_context->resolved_ref = &context->resolved; + + return NJS_OK; +} + + +static njs_promise_capability_t * +njs_promise_new_capability(njs_vm_t *vm, njs_value_t *constructor) +{ + njs_int_t ret; + njs_value_t argument, this; + njs_object_t *object; + njs_function_t *function; + njs_promise_context_t *context; + njs_promise_capability_t *capability; + + object = NULL; + function = NULL; + + ret = njs_promise_value_constructor(vm, constructor, constructor); + if (njs_slow_path(ret != NJS_OK)) { + return NULL; + } + + capability = njs_mp_zalloc(vm->mem_pool, sizeof(njs_promise_capability_t)); + if (njs_slow_path(capability == NULL)) { + njs_memory_error(vm); + return NULL; + } + + function = njs_promise_create_function(vm); + if (njs_slow_path(function == NULL)) { + return NULL; + } + + njs_set_undefined(&capability->resolve); + njs_set_undefined(&capability->reject); + + function->u.native = njs_promise_capability_executor; + function->args_count = 2; + + context = function->context; + context->capability = capability; + + njs_set_function(&argument, function); + + object = njs_function_new_object(vm, constructor); + if (njs_slow_path(object == NULL)) { + return NULL; + } + + njs_set_object(&this, object); + + ret = njs_function_call2(vm, njs_function(constructor), &this, + &argument, 1, &capability->promise, 1); + if (njs_slow_path(ret != NJS_OK)) { + return NULL; + } + + if (njs_slow_path(!njs_is_function(&capability->resolve))) { + njs_type_error(vm, "capability resolve slot is not callable"); + return NULL; + } + + if (njs_slow_path(!njs_is_function(&capability->reject))) { + njs_type_error(vm, "capability reject slot is not callable"); + return NULL; + } + + return capability; +} + + +static njs_int_t +njs_promise_value_constructor(njs_vm_t *vm, njs_value_t *value, + njs_value_t *dst) +{ + njs_int_t ret; + + static const njs_value_t string_constructor = njs_string("constructor"); + + if (njs_is_function(value)) { + *dst = *value; + return NJS_OK; + } + + ret = njs_value_property(vm, value, njs_value_arg(&string_constructor), + dst); + if (njs_slow_path(ret != NJS_OK)) { + return ret; + } + + if (!njs_is_function(dst)) { + njs_type_error(vm, "the object does not contain a constructor"); + return NJS_ERROR; + } + + return NJS_OK; +} + + +static njs_int_t +njs_promise_capability_executor(njs_vm_t *vm, njs_value_t *args, + njs_uint_t nargs, njs_index_t unused) +{ + njs_promise_context_t *context; + njs_promise_capability_t *capability; + + context = vm->top_frame->function->context; + capability = context->capability; + + if (njs_slow_path(capability == NULL)) { + njs_type_error(vm, "failed to get function capability"); + return NJS_ERROR; + } + + if (!njs_is_undefined(&capability->resolve)) { + njs_type_error(vm, "capability resolve slot is not undefined"); + return NJS_ERROR; + } + + if (!njs_is_undefined(&capability->reject)) { + njs_type_error(vm, "capability reject slot is not undefined"); + return NJS_ERROR; + } + + capability->resolve = *njs_arg(args, nargs, 1); + capability->reject = *njs_arg(args, nargs, 2); + + njs_vm_retval_set(vm, &njs_value_undefined); + + return NJS_OK; +} + + +njs_inline njs_int_t +njs_promise_add_event(njs_vm_t *vm, njs_function_t *function, njs_value_t *args, + njs_uint_t nargs) +{ + njs_event_t *event; + + event = njs_mp_zalloc(vm->mem_pool, sizeof(njs_event_t)); + if (njs_slow_path(event == NULL)) { + njs_memory_error(vm); + return NJS_ERROR; + } + + event->function = function; + event->once = 1; + + if (nargs != 0) { + event->args = njs_mp_alloc(vm->mem_pool, sizeof(njs_value_t) * nargs); + if (njs_slow_path(event->args == NULL)) { + njs_memory_error(vm); + return NJS_ERROR; + } + + memcpy(event->args, args, sizeof(njs_value_t) * nargs); + + event->nargs = nargs; + } + + njs_queue_insert_tail(&vm->promise_events, &event->link); + + return NJS_OK; +} + + +njs_inline njs_value_t * +njs_promise_trigger_reactions(njs_vm_t *vm, njs_value_t *value, + njs_queue_t *queue) +{ + njs_int_t ret; + njs_value_t arguments[2]; + njs_function_t *function; + njs_queue_link_t *link; + njs_promise_reaction_t *reaction; + + for (link = njs_queue_first(queue); + link != njs_queue_tail(queue); + link = njs_queue_next(link)) + { + reaction = njs_queue_link_data(link, njs_promise_reaction_t, link); + + function = njs_promise_create_function(vm); + function->u.native = njs_promise_reaction_job; + + njs_set_data(&arguments[0], reaction); + arguments[1] = *value; + + ret = njs_promise_add_event(vm, function, arguments, 2); + if (njs_slow_path(ret != NJS_OK)) { + return njs_value_arg(&njs_value_null); + } + } + + return njs_value_arg(&njs_value_undefined); +} + + +njs_inline njs_value_t * +njs_promise_fulfill(njs_vm_t *vm, njs_promise_t *promise, njs_value_t *value) +{ + njs_queue_t queue; + njs_promise_data_t *data; + + data = njs_value_data(&promise->value); + + data->result = *value; + data->state = NJS_PROMISE_FULFILL; + + if (njs_queue_is_empty(&data->fulfill_queue)) { + return njs_value_arg(&njs_value_undefined); + + } else { + queue = data->fulfill_queue; + + queue.head.prev->next = &queue.head; + queue.head.next->prev = &queue.head; + } + + njs_queue_init(&data->fulfill_queue); + njs_queue_init(&data->reject_queue); + + return njs_promise_trigger_reactions(vm, value, &queue); +} + + +njs_inline njs_value_t * +njs_promise_reject(njs_vm_t *vm, njs_promise_t *promise, njs_value_t *reason) +{ + njs_queue_t queue; + njs_promise_data_t *data; + + data = njs_value_data(&promise->value); + + data->result = *reason; + data->state = NJS_PROMISE_REJECTED; + + if (njs_queue_is_empty(&data->reject_queue)) { + return njs_value_arg(&njs_value_undefined); + + } else { + queue = data->reject_queue; + + queue.head.prev->next = &queue.head; + queue.head.next->prev = &queue.head; + } + + njs_queue_init(&data->fulfill_queue); + njs_queue_init(&data->reject_queue); + + return njs_promise_trigger_reactions(vm, reason, &queue); +} + + +static njs_int_t +njs_promise_invoke_then(njs_vm_t *vm, njs_value_t *promise, njs_value_t *args, + njs_int_t nargs) +{ + njs_int_t ret; + njs_value_t function; + + static const njs_value_t string_then = njs_string("then"); + + ret = njs_value_property(vm, promise, njs_value_arg(&string_then), + &function); + if (njs_slow_path(ret != NJS_OK)) { + if (ret == NJS_DECLINED) { + goto failed; + } + + return NJS_ERROR; + } + + if (njs_fast_path(njs_is_function(&function))) { + return njs_function_call(vm, njs_function(&function), promise, args, + nargs, &vm->retval); + } + +failed: + + njs_type_error(vm, "is not a function"); + + return NJS_ERROR; +} + + +static njs_int_t +njs_promise_resolve_function(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, + njs_index_t unused) +{ + njs_int_t ret; + njs_frame_t *active_frame; + njs_value_t *resolution, error, then, arguments[3]; + njs_promise_t *promise; + njs_function_t *function; + njs_promise_context_t *context; + + static const njs_value_t string_then = njs_string("then"); + + active_frame = (njs_frame_t *) vm->top_frame; + context = active_frame->native.function->context; + promise = njs_promise(&context->promise); + + if (*context->resolved_ref) { + njs_vm_retval_set(vm, &njs_value_undefined); + return NJS_OK; + } + + *context->resolved_ref = 1; + + resolution = njs_arg(args, nargs, 1); + + if (njs_values_same(resolution, &context->promise)) { + njs_error_fmt_new(vm, &error, NJS_OBJ_TYPE_TYPE_ERROR, + "promise self resolution"); + if (njs_slow_path(!njs_is_error(&error))) { + return NJS_ERROR; + } + + njs_vm_retval_set(vm, njs_promise_reject(vm, promise, &error)); + + return NJS_OK; + } + + if (!njs_is_object(resolution)) { + goto fulfill; + } + + ret = njs_value_property(vm, resolution, njs_value_arg(&string_then), + &then); + if (njs_slow_path(ret == NJS_ERROR)) { + if (njs_slow_path(njs_is_memory_error(vm, &vm->retval))) { + return NJS_ERROR; + } + + njs_vm_retval_set(vm, njs_promise_reject(vm, promise, &vm->retval)); + if (njs_slow_path(njs_vm_retval(vm)->type == NJS_NULL)) { + return NJS_ERROR; + } + + return NJS_OK; + } + + if (!njs_is_function(&then)) { + goto fulfill; + } + + arguments[0] = context->promise; + arguments[1] = *resolution; + arguments[2] = then; + + function = njs_promise_create_function(vm); + if (njs_slow_path(function == NULL)) { + return NJS_ERROR; + } + + function->u.native = njs_promise_resolve_thenable_job; + + ret = njs_promise_add_event(vm, function, arguments, 3); + if (njs_slow_path(ret != NJS_OK)) { + return ret; + } + + njs_vm_retval_set(vm, &njs_value_undefined); + + return NJS_OK; + +fulfill: + + njs_vm_retval_set(vm, njs_promise_fulfill(vm, promise, resolution)); + if (njs_slow_path(njs_vm_retval(vm)->type == NJS_NULL)) { + return NJS_ERROR; + } + + return NJS_OK; +} + + +static njs_int_t +njs_promise_object_resolve(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, + njs_index_t unused) +{ + njs_promise_t *promise; + + if (njs_slow_path(!njs_is_object(njs_arg(args, nargs, 0)))) { + njs_type_error(vm, "this value is not an object"); + return NJS_ERROR; + } + + promise = njs_promise_resolve(vm, njs_argument(args, 0), + njs_arg(args, nargs, 1)); + if (njs_slow_path(promise == NULL)) { + return NJS_ERROR; + } + + njs_set_promise(&vm->retval, promise); + + return NJS_OK; +} + + +static njs_promise_t * +njs_promise_resolve(njs_vm_t *vm, njs_value_t *constructor, njs_value_t *x) +{ + njs_int_t ret; + njs_value_t value; + njs_object_t *object; + njs_promise_capability_t *capability; + + static const njs_value_t string_constructor = njs_string("constructor"); + + if (njs_is_object(x)) { + object = njs_object_proto_lookup(njs_object(x), NJS_PROMISE, + njs_object_t); + + if (object != NULL) { + ret = njs_value_property(vm, x, njs_value_arg(&string_constructor), + &value); + if (njs_slow_path(ret == NJS_ERROR)) { + return NULL; + } + + if (njs_values_same(&value, constructor)) { + return njs_promise(x); + } + } + } + + capability = njs_promise_new_capability(vm, constructor); + if (njs_slow_path(capability == NULL)) { + return NULL; + } + + ret = njs_function_call(vm, njs_function(&capability->resolve), + &njs_value_undefined, x, 1, &value); + if (njs_slow_path(ret != NJS_OK)) { + return NULL; + } + + return njs_promise(&capability->promise); +} + + +static njs_int_t +njs_promise_reject_function(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, + njs_index_t unused) +{ + njs_frame_t *active_frame; + njs_value_t *value; + njs_promise_context_t *context; + + active_frame = (njs_frame_t *) vm->top_frame; + context = active_frame->native.function->context; + + if (*context->resolved_ref) { + njs_vm_retval_set(vm, &njs_value_undefined); + return NJS_OK; + } + + *context->resolved_ref = 1; + + value = njs_promise_reject(vm, njs_promise(&context->promise), + njs_arg(args, nargs, 1)); + if (njs_slow_path(value->type == NJS_NULL)) { + return NJS_ERROR; + } + + njs_vm_retval_set(vm, value); + + return NJS_OK; +} + + +static njs_int_t +njs_promise_object_reject(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, + njs_index_t unused) +{ + njs_int_t ret; + njs_value_t value; + njs_promise_capability_t *capability; + + if (njs_slow_path(!njs_is_object(njs_arg(args, nargs, 0)))) { + njs_type_error(vm, "this value is not an object"); + return NJS_ERROR; + } + + capability = njs_promise_new_capability(vm, njs_argument(args, 0)); + if (njs_slow_path(capability == NULL)) { + return NJS_ERROR; + } + + ret = njs_function_call(vm, njs_function(&capability->reject), + &njs_value_undefined, njs_arg(args, nargs, 1), + 1, &value); + if (njs_slow_path(ret != NJS_OK)) { + return ret; + } + + njs_vm_retval_set(vm, &capability->promise); + + return NJS_OK; +} + + +static njs_int_t +njs_promise_prototype_then(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, + njs_index_t unused) +{ + njs_int_t ret; + njs_value_t *promise, *fulfilled, *rejected, constructor; + njs_object_t *object; + njs_function_t *function; + njs_promise_capability_t *capability; + + promise = njs_arg(args, nargs, 0); + + if (njs_slow_path(!njs_is_object(promise))) { + goto failed; + } + + object = njs_object_proto_lookup(njs_object(promise), NJS_PROMISE, + njs_object_t); + if (njs_slow_path(object == NULL)) { + goto failed; + } + + function = njs_promise_create_function(vm); + function->u.native = njs_promise_constructor; + + njs_set_function(&constructor, function); + + ret = njs_value_species_constructor(vm, promise, &constructor, + &constructor); + if (njs_slow_path(ret != NJS_OK)) { + return ret; + } + + capability = njs_promise_new_capability(vm, &constructor); + if (njs_slow_path(capability == NULL)) { + return NJS_ERROR; + } + + fulfilled = njs_arg(args, nargs, 1); + rejected = njs_arg(args, nargs, 2); + + return njs_promise_perform_then(vm, promise, fulfilled, rejected, + capability); + +failed: + + njs_type_error(vm, "required a promise object"); + + return NJS_ERROR; +} + + +static njs_int_t +njs_promise_perform_then(njs_vm_t *vm, njs_value_t *value, + njs_value_t *fulfilled, njs_value_t *rejected, + njs_promise_capability_t *capability) +{ + njs_int_t ret; + njs_value_t arguments[2]; + njs_promise_t *promise; + njs_function_t *function; + njs_promise_data_t *data; + njs_promise_reaction_t *fulfilled_reaction, *rejected_reaction; + + if (!njs_is_function(fulfilled)) { + fulfilled = njs_value_arg(&njs_value_undefined); + } + + if (!njs_is_function(rejected)) { + rejected = njs_value_arg(&njs_value_undefined); + } + + promise = njs_promise(value); + data = njs_value_data(&promise->value); + + fulfilled_reaction = njs_mp_alloc(vm->mem_pool, + sizeof(njs_promise_reaction_t)); + if (njs_slow_path(fulfilled_reaction == NULL)) { + njs_memory_error(vm); + return NJS_ERROR; + } + + fulfilled_reaction->capability = capability; + fulfilled_reaction->handler = *fulfilled; + fulfilled_reaction->type = NJS_PROMISE_FULFILL; + + rejected_reaction = njs_mp_alloc(vm->mem_pool, + sizeof(njs_promise_reaction_t)); + if (njs_slow_path(fulfilled_reaction == NULL)) { + njs_memory_error(vm); + return NJS_ERROR; + } + + rejected_reaction->capability = capability; + rejected_reaction->handler = *rejected; + rejected_reaction->type = NJS_PROMISE_REJECTED; + + if (data->state == NJS_PROMISE_PENDING) { + njs_queue_insert_tail(&data->fulfill_queue, &fulfilled_reaction->link); + njs_queue_insert_tail(&data->reject_queue, &rejected_reaction->link); + + } else { + function = njs_promise_create_function(vm); + function->u.native = njs_promise_reaction_job; + + if (data->state == NJS_PROMISE_REJECTED) { + njs_set_data(&arguments[0], rejected_reaction); + + /* TODO: HostPromiseRejectionTracker */ + + } else { + njs_set_data(&arguments[0], fulfilled_reaction); + } + + arguments[1] = data->result; + + ret = njs_promise_add_event(vm, function, arguments, 2); + if (njs_slow_path(ret != NJS_OK)) { + return ret; + } + } + + data->is_handled = 1; + + if (capability == NULL) { + njs_vm_retval_set(vm, &njs_value_undefined); + + } else { + njs_vm_retval_set(vm, &capability->promise); + } + + return NJS_OK; +} + + +static njs_int_t +njs_promise_prototype_catch(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, + njs_index_t unused) +{ + njs_value_t arguments[2]; + + arguments[0] = njs_value_undefined; + arguments[1] = *njs_arg(args, nargs, 1); + + return njs_promise_invoke_then(vm, njs_arg(args, nargs, 0), arguments, 2); +} + + +static njs_int_t +njs_promise_prototype_finally(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, + njs_index_t unused) +{ + njs_int_t ret; + njs_value_t *promise, *finally, constructor, arguments[2]; + njs_function_t *function; + njs_promise_context_t *context; + + promise = njs_arg(args, nargs, 0); + + if (njs_slow_path(!njs_is_object(promise))) { + njs_type_error(vm, "required a object"); + return NJS_ERROR; + } + + finally = njs_arg(args, nargs, 1); + + function = njs_promise_create_function(vm); + function->u.native = njs_promise_constructor; + + njs_set_function(&constructor, function); + + ret = njs_value_species_constructor(vm, promise, &constructor, + &constructor); + if (njs_slow_path(ret != NJS_OK)) { + return ret; + } + + if (!njs_is_function(finally)) { + arguments[0] = *finally; + arguments[1] = *finally; + + return njs_promise_invoke_then(vm, promise, arguments, 2); + } + + function = njs_promise_create_function(vm); + if (njs_slow_path(function == NULL)) { + return NJS_ERROR; + } + + function->u.native = njs_promise_then_finally_function; + function->args_count = 1; + + context = function->context; + context->constructor = constructor; + context->finally = *finally; + + njs_set_function(&arguments[0], function); + + function = njs_promise_create_function(vm); + if (njs_slow_path(function == NULL)) { + njs_mp_free(vm->mem_pool, njs_function(&arguments[0])); + return NJS_ERROR; + } + + function->u.native = njs_promise_catch_finally_function; + function->args_count = 1; + + context = function->context; + context->constructor = constructor; + context->finally = *finally; + + njs_set_function(&arguments[1], function); + + return njs_promise_invoke_then(vm, promise, arguments, 2); +} + + +static njs_int_t +njs_promise_then_finally_function(njs_vm_t *vm, njs_value_t *args, + njs_uint_t nargs, njs_index_t unused) +{ + njs_int_t ret; + njs_value_t value, retval; + njs_frame_t *frame; + njs_promise_t *promise; + njs_promise_context_t *context; + + frame = (njs_frame_t *) vm->top_frame; + context = frame->native.function->context; + + ret = njs_function_call(vm, njs_function(&context->finally), + &njs_value_undefined, args, 0, &retval); + if (njs_slow_path(ret != NJS_OK)) { + return ret; + } + + promise = njs_promise_resolve(vm, &context->constructor, &retval); + if (njs_slow_path(promise == NULL)) { + return NJS_ERROR; + } + + njs_set_promise(&value, promise); + + return njs_promise_invoke_then(vm, &value, njs_arg(args, nargs, 1), 1); +} + + +static njs_int_t +njs_promise_catch_finally_function(njs_vm_t *vm, njs_value_t *args, + njs_uint_t nargs, njs_index_t unused) +{ + return njs_promise_then_finally_function(vm, args, nargs, unused); +} + + +static njs_int_t +njs_promise_reaction_job(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, + njs_index_t unused) +{ + njs_int_t ret; + njs_bool_t is_error; + njs_value_t *value, *argument, retval; + njs_promise_reaction_t *reaction; + njs_promise_capability_t *capability; + + value = njs_arg(args, nargs, 1); + argument = njs_arg(args, nargs, 2); + + reaction = njs_data(value); + capability = reaction->capability; + + is_error = 0; + + if (njs_is_undefined(&reaction->handler)) { + if (reaction->type == NJS_PROMISE_REJECTED) { + is_error = 1; + } + + retval = *argument; + + } else { + ret = njs_function_call(vm, njs_function(&reaction->handler), + &njs_value_undefined, argument, 1, &retval); + if (njs_slow_path(ret != NJS_OK)) { + if (njs_slow_path(njs_is_memory_error(vm, &vm->retval))) { + return NJS_ERROR; + } + + retval = vm->retval; + is_error = 1; + } + } + + if (capability == NULL) { + njs_vm_retval_set(vm, &retval); + return NJS_OK; + } + + if (is_error) { + ret = njs_function_call(vm, njs_function(&capability->reject), + &njs_value_undefined, &retval, 1, &vm->retval); + + } else { + ret = njs_function_call(vm, njs_function(&capability->resolve), + &njs_value_undefined, &retval, 1, &vm->retval); + } + + if (njs_slow_path(ret != NJS_OK)) { + return NJS_ERROR; + } + + return NJS_OK; +} + + +static njs_int_t +njs_promise_resolve_thenable_job(njs_vm_t *vm, njs_value_t *args, + njs_uint_t nargs, njs_index_t unused) +{ + njs_int_t ret; + njs_value_t *promise, retval, arguments[2]; + + promise = njs_arg(args, nargs, 1); + + ret = njs_promise_create_resolving_functions(vm, njs_promise(promise), + arguments); + if (njs_slow_path(ret != NJS_OK)) { + return ret; + } + + ret = njs_function_call(vm, njs_function(njs_arg(args, nargs, 3)), + njs_arg(args, nargs, 2), arguments, 2, &retval); + if (njs_slow_path(ret != NJS_OK)) { + + if (njs_slow_path(njs_is_memory_error(vm, &vm->retval))) { + return NJS_ERROR; + } + + ret = njs_function_call(vm, njs_function(&arguments[1]), + &njs_value_undefined, &vm->retval, 1, + &vm->retval); + if (njs_slow_path(ret != NJS_OK)) { + return NJS_ERROR; + } + } + + return NJS_OK; +} + + +static njs_int_t +njs_promise_species(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, + njs_index_t unused) +{ + njs_vm_retval_set(vm, njs_arg(args, nargs, 0)); + + return NJS_OK; +} + + +static const njs_object_prop_t njs_promise_constructor_properties[] = +{ + { + .type = NJS_PROPERTY, + .name = njs_string("name"), + .value = njs_string("Promise"), + .configurable = 1, + }, + + { + .type = NJS_PROPERTY, + .name = njs_string("length"), + .value = njs_value(NJS_NUMBER, 1, 1.0), + .configurable = 1, + }, + + { + .type = NJS_PROPERTY_HANDLER, + .name = njs_string("prototype"), + .value = njs_prop_handler(njs_object_prototype_create), + }, + + { + .type = NJS_PROPERTY, + .name = njs_string("resolve"), + .value = njs_native_function(njs_promise_object_resolve, 1), + .writable = 1, + .configurable = 1, + }, + + { + .type = NJS_PROPERTY, + .name = njs_string("reject"), + .value = njs_native_function(njs_promise_object_reject, 1), + .writable = 1, + .configurable = 1, + }, + + { + .type = NJS_PROPERTY, + .name = njs_wellknown_symbol(NJS_SYMBOL_SPECIES), + .value = njs_value(NJS_INVALID, 1, NAN), + .getter = njs_native_function(njs_promise_species, 0), + .setter = njs_value(NJS_UNDEFINED, 0, NAN), + .writable = NJS_ATTRIBUTE_UNSET, + .configurable = 1, + .enumerable = 0, + }, +}; + + +const njs_object_init_t njs_promise_constructor_init = { + njs_promise_constructor_properties, + njs_nitems(njs_promise_constructor_properties), +}; + + +static const njs_object_prop_t njs_promise_prototype_properties[] = +{ + { + .type = NJS_PROPERTY_HANDLER, + .name = njs_string("constructor"), + .value = njs_prop_handler(njs_object_prototype_create_constructor), + .writable = 1, + .configurable = 1, + }, + + { + .type = NJS_PROPERTY, + .name = njs_wellknown_symbol(NJS_SYMBOL_TO_STRING_TAG), + .value = njs_string("Promise"), + .configurable = 1, + }, + + { + .type = NJS_PROPERTY, + .name = njs_string("then"), + .value = njs_native_function(njs_promise_prototype_then, 2), + .writable = 1, + .configurable = 1, + }, + + { + .type = NJS_PROPERTY, + .name = njs_string("catch"), + .value = njs_native_function(njs_promise_prototype_catch, 1), + .writable = 1, + .configurable = 1, + }, + + { + .type = NJS_PROPERTY, + .name = njs_string("finally"), + .value = njs_native_function(njs_promise_prototype_finally, 1), + .writable = 1, + .configurable = 1, + }, +}; + + +const njs_object_init_t njs_promise_prototype_init = { + njs_promise_prototype_properties, + njs_nitems(njs_promise_prototype_properties), +}; + +const njs_object_type_init_t njs_promise_type_init = { + .constructor = njs_native_ctor(njs_promise_constructor, 1, 0), + .prototype_props = &njs_promise_prototype_init, + .constructor_props = &njs_promise_constructor_init, + .prototype_value = { .object = { .type = NJS_OBJECT } }, +}; diff --git a/src/njs_promise.h b/src/njs_promise.h new file mode 100644 index 00000000..bb645f34 --- /dev/null +++ b/src/njs_promise.h @@ -0,0 +1,18 @@ + +/* + * Copyright (C) Nginx, Inc. + */ + +#ifndef _NJS_PROMISE_H_INCLUDED_ +#define _NJS_PROMISE_H_INCLUDED_ + + +njs_int_t +njs_promise_constructor(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, + njs_index_t unused); + + +extern const njs_object_type_init_t njs_promise_type_init; + + +#endif /* _NJS_PROMISE_H_INCLUDED_ */ diff --git a/src/njs_shell.c b/src/njs_shell.c index d4378990..f509ce3d 100644 --- a/src/njs_shell.c +++ b/src/njs_shell.c @@ -109,6 +109,7 @@ static njs_int_t njs_ext_console_time_end(njs_vm_t *vm, njs_value_t *args, static njs_host_event_t njs_console_set_timer(njs_external_ptr_t external, uint64_t delay, njs_vm_event_t vm_event); + static void njs_console_clear_timer(njs_external_ptr_t external, njs_host_event_t event); diff --git a/src/njs_value.c b/src/njs_value.c index a861889d..e015c213 100644 --- a/src/njs_value.c +++ b/src/njs_value.c @@ -362,6 +362,9 @@ njs_type_string(njs_value_type_t type) case NJS_DATE: return "date"; + case NJS_PROMISE: + return "promise"; + default: return NULL; } @@ -557,6 +560,7 @@ njs_property_query(njs_vm_t *vm, njs_property_query_t *pq, njs_value_t *value, case NJS_OBJECT_STRING: case NJS_REGEXP: case NJS_DATE: + case NJS_PROMISE: case NJS_OBJECT_VALUE: obj = njs_object(value); break; diff --git a/src/njs_value.h b/src/njs_value.h index 0af5dae9..28ed7640 100644 --- a/src/njs_value.h +++ b/src/njs_value.h @@ -67,8 +67,9 @@ typedef enum { NJS_FUNCTION = 0x16, NJS_REGEXP = 0x17, NJS_DATE = 0x18, - NJS_OBJECT_VALUE = 0x19, - NJS_ARRAY_BUFFER = 0x1A, + NJS_PROMISE = 0x19, + NJS_OBJECT_VALUE = 0x1A, + NJS_ARRAY_BUFFER = 0x1B, NJS_VALUE_TYPE_MAX } njs_value_type_t; @@ -83,6 +84,7 @@ typedef struct njs_array_s njs_array_t; typedef struct njs_array_buffer_s njs_array_buffer_t; typedef struct njs_regexp_s njs_regexp_t; typedef struct njs_date_s njs_date_t; +typedef struct njs_object_value_s njs_promise_t; typedef struct njs_property_next_s njs_property_next_t; typedef struct njs_object_init_s njs_object_init_t; @@ -146,6 +148,7 @@ union njs_value_s { njs_function_lambda_t *lambda; njs_regexp_t *regexp; njs_date_t *date; + njs_promise_t *promise; njs_prop_handler_t prop_handler; njs_value_t *value; njs_property_next_t *next; @@ -283,6 +286,8 @@ struct njs_function_s { njs_function_t *bound_target; } u; + void *context; + njs_value_t *bound; }; @@ -312,6 +317,7 @@ typedef union { njs_function_t function; njs_regexp_t regexp; njs_date_t date; + njs_promise_t promise; } njs_object_prototype_t; @@ -644,6 +650,10 @@ typedef struct { ((value)->type == NJS_DATE) +#define njs_is_promise(value) \ + ((value)->type == NJS_PROMISE) + + #define njs_is_error(value) \ ((value)->type == NJS_OBJECT && njs_object(value)->error_data) @@ -708,6 +718,10 @@ typedef struct { ((value)->data.u.date) +#define njs_promise(value) \ + ((value)->data.u.promise) + + #define njs_regexp(value) \ ((value)->data.u.regexp) @@ -874,6 +888,15 @@ njs_set_date(njs_value_t *value, njs_date_t *date) } +njs_inline void +njs_set_promise(njs_value_t *value, njs_promise_t *promise) +{ + value->data.u.promise = promise; + value->type = NJS_PROMISE; + value->data.truth = 1; +} + + njs_inline void njs_set_regexp(njs_value_t *value, njs_regexp_t *regexp) { diff --git a/src/njs_vm.c b/src/njs_vm.c index cd2953d6..13b3ae76 100644 --- a/src/njs_vm.c +++ b/src/njs_vm.c @@ -304,6 +304,7 @@ njs_vm_init(njs_vm_t *vm) njs_lvlhsh_init(&vm->events_hash); njs_queue_init(&vm->posted_events); + njs_queue_init(&vm->promise_events); return NJS_OK; } @@ -435,7 +436,7 @@ njs_vm_waiting(njs_vm_t *vm) njs_int_t njs_vm_posted(njs_vm_t *vm) { - return njs_posted_events(vm); + return njs_posted_events(vm) || njs_promise_events(vm); } @@ -495,31 +496,53 @@ njs_vm_handle_events(njs_vm_t *vm) njs_queue_t *events; njs_queue_link_t *link; - events = &vm->posted_events; + do { + events = &vm->promise_events; - for ( ;; ) { - link = njs_queue_first(events); - - if (link == njs_queue_tail(events)) { - break; - } + for ( ;; ) { + link = njs_queue_first(events); - ev = njs_queue_link_data(link, njs_event_t, link); + if (link == njs_queue_tail(events)) { + break; + } - if (ev->once) { - njs_del_event(vm, ev, NJS_EVENT_RELEASE | NJS_EVENT_DELETE); + ev = njs_queue_link_data(link, njs_event_t, link); - } else { - ev->posted = 0; njs_queue_remove(&ev->link); + + ret = njs_vm_call(vm, ev->function, ev->args, ev->nargs); + if (njs_slow_path(ret == NJS_ERROR)) { + return ret; + } } - ret = njs_vm_call(vm, ev->function, ev->args, ev->nargs); + events = &vm->posted_events; - if (ret == NJS_ERROR) { - return ret; + for ( ;; ) { + link = njs_queue_first(events); + + if (link == njs_queue_tail(events)) { + break; + } + + ev = njs_queue_link_data(link, njs_event_t, link); + + if (ev->once) { + njs_del_event(vm, ev, NJS_EVENT_RELEASE | NJS_EVENT_DELETE); + + } else { + ev->posted = 0; + njs_queue_remove(&ev->link); + } + + ret = njs_vm_call(vm, ev->function, ev->args, ev->nargs); + + if (ret == NJS_ERROR) { + return ret; + } } - } + + } while (!njs_queue_is_empty(events)); return njs_posted_events(vm) ? NJS_AGAIN : NJS_OK; } diff --git a/src/njs_vm.h b/src/njs_vm.h index 612924d2..5167ff80 100644 --- a/src/njs_vm.h +++ b/src/njs_vm.h @@ -90,6 +90,7 @@ typedef enum { NJS_OBJ_TYPE_FUNCTION, NJS_OBJ_TYPE_REGEXP, NJS_OBJ_TYPE_DATE, + NJS_OBJ_TYPE_PROMISE, NJS_OBJ_TYPE_CRYPTO_HASH, #define NJS_OBJ_TYPE_HIDDEN_MIN (NJS_OBJ_TYPE_CRYPTO_HASH) NJS_OBJ_TYPE_CRYPTO_HMAC, @@ -187,6 +188,7 @@ struct njs_vm_s { uint32_t event_id; njs_lvlhsh_t events_hash; njs_queue_t posted_events; + njs_queue_t promise_events; njs_vm_opt_t options; diff --git a/src/njs_vmcode.c b/src/njs_vmcode.c index 5f6e36e4..b5de9966 100644 --- a/src/njs_vmcode.c +++ b/src/njs_vmcode.c @@ -66,8 +66,6 @@ static njs_jump_off_t njs_primitive_values_compare(njs_vm_t *vm, static njs_jump_off_t njs_function_frame_create(njs_vm_t *vm, njs_value_t *value, const njs_value_t *this, uintptr_t nargs, njs_bool_t ctor); -static njs_object_t *njs_function_new_object(njs_vm_t *vm, - njs_value_t *constructor); /* * The nJSVM is optimized for an ABIs where the first several arguments @@ -1421,6 +1419,7 @@ njs_vmcode_typeof(njs_vm_t *vm, njs_value_t *value, njs_value_t *invld) &njs_string_object, &njs_string_object, &njs_string_object, + &njs_string_object, }; vm->retval = *types[value->type]; @@ -1624,7 +1623,7 @@ njs_function_frame_create(njs_vm_t *vm, njs_value_t *value, } -static njs_object_t * +njs_object_t * njs_function_new_object(njs_vm_t *vm, njs_value_t *constructor) { njs_value_t proto, bound; diff --git a/src/njs_vmcode.h b/src/njs_vmcode.h index b6e4062b..1c90112a 100644 --- a/src/njs_vmcode.h +++ b/src/njs_vmcode.h @@ -394,5 +394,7 @@ typedef struct { njs_int_t njs_vmcode_interpreter(njs_vm_t *vm, u_char *pc); +njs_object_t *njs_function_new_object(njs_vm_t *vm, njs_value_t *constructor); + #endif /* _NJS_VMCODE_H_INCLUDED_ */ diff --git a/test/js/promise_s1.js b/test/js/promise_s1.js new file mode 100644 index 00000000..308dbe2d --- /dev/null +++ b/test/js/promise_s1.js @@ -0,0 +1,15 @@ +var promise = new Promise(function(resolve, reject) { + console.log('One'); + + reject(123); +}).catch((v) => {console.log(v)}); + +console.log('Two'); + +promise.then(() => {console.log('Four'); return {num: 'Five'}}) +.then((obj) => {console.log(obj.num); return {num: 'Six'}}) +.then((obj) => {console.log(obj.num); return {num: 'Seven'}}) +.then((obj) => {console.log(obj.num); return {num: 'Eight'}}) +.then((obj) => {console.log(obj.num)}); + +console.log('Three'); \ No newline at end of file diff --git a/test/js/promise_s10.js b/test/js/promise_s10.js new file mode 100644 index 00000000..6fd1d6b2 --- /dev/null +++ b/test/js/promise_s10.js @@ -0,0 +1,11 @@ +var promise = new Promise(function(resolve, reject) { + console.log('One'); + reject('Oh no'); +}); + +console.log('Two'); + +promise.then(() => {console.log('Three')}) +.catch((v) => {console.log(v)}); + +console.log('Three'); \ No newline at end of file diff --git a/test/js/promise_s11.js b/test/js/promise_s11.js new file mode 100644 index 00000000..ecffccca --- /dev/null +++ b/test/js/promise_s11.js @@ -0,0 +1,13 @@ +var promise = new Promise((resolve, reject) => resolve('all')); + +promise.then( function f1(result) { + console.log('S: ' + result); + return 'f1'; +}); + +promise.then( function f2(result) { + console.log('R: ' + result); + return 'f2'; +}); + +console.log('end') \ No newline at end of file diff --git a/test/js/promise_s12.js b/test/js/promise_s12.js new file mode 100644 index 00000000..e157b472 --- /dev/null +++ b/test/js/promise_s12.js @@ -0,0 +1,10 @@ +var thenable = new Promise(function() {}); + +var p = new Promise(function(resolve) { + resolve(); + throw thenable; +}); + +p.then(function() { + console.log('Done'); +}); \ No newline at end of file diff --git a/test/js/promise_s13.js b/test/js/promise_s13.js new file mode 100644 index 00000000..4ace323e --- /dev/null +++ b/test/js/promise_s13.js @@ -0,0 +1,21 @@ +var thenable = Promise.resolve(); +var p = new Promise(function(a,b) { + throw thenable; +}); + +p.then(function() { + console.log('The promise should not be fulfilled.'); +}) +.then( + function() { + console.log('The promise should not be fulfilled.'); + }, + function(x) { + if (x !== thenable) { + console.log('The promise should be rejected with the resolution value.'); + return; + } + + console.log('Done'); + } +); \ No newline at end of file diff --git a/test/js/promise_s14.js b/test/js/promise_s14.js new file mode 100644 index 00000000..63494eea --- /dev/null +++ b/test/js/promise_s14.js @@ -0,0 +1,9 @@ +var isLoading = true; +var promise = new Promise((a, b) => {a()} ); + +promise.then(function(response) { + throw new TypeError('oops'); +}) +.then(function(json) { }) +.catch(function(error) { console.log(error); }) +.finally(function() { console.log('Done') }); \ No newline at end of file diff --git a/test/js/promise_s15.js b/test/js/promise_s15.js new file mode 100644 index 00000000..5b134bdb --- /dev/null +++ b/test/js/promise_s15.js @@ -0,0 +1,10 @@ +var obj = {}; +var p = Promise.resolve(obj); + +p.then(undefined, undefined) +.then(function(arg) { + if (arg !== obj) { + console.log('Expected resolution object to be passed through, got ' + arg); + } +}) +.then(() => {console.log('Done')}, () => {console.log('Error')}); \ No newline at end of file diff --git a/test/js/promise_s16.js b/test/js/promise_s16.js new file mode 100644 index 00000000..769d38a6 --- /dev/null +++ b/test/js/promise_s16.js @@ -0,0 +1,10 @@ +var obj = {}; +var p = Promise.reject(obj); + +p.then(undefined, undefined).then(function() { + console.log('Should not be called -- promise was rejected.'); +}, function(arg) { + if (arg !== obj) { + console.log('Expected resolution object to be passed through, got ' + arg); + } +}).then(() => {console.log('Done')}, () => {console.log('Error')}); diff --git a/test/js/promise_s17.js b/test/js/promise_s17.js new file mode 100644 index 00000000..769d38a6 --- /dev/null +++ b/test/js/promise_s17.js @@ -0,0 +1,10 @@ +var obj = {}; +var p = Promise.reject(obj); + +p.then(undefined, undefined).then(function() { + console.log('Should not be called -- promise was rejected.'); +}, function(arg) { + if (arg !== obj) { + console.log('Expected resolution object to be passed through, got ' + arg); + } +}).then(() => {console.log('Done')}, () => {console.log('Error')}); diff --git a/test/js/promise_s18.js b/test/js/promise_s18.js new file mode 100644 index 00000000..10d10641 --- /dev/null +++ b/test/js/promise_s18.js @@ -0,0 +1,23 @@ +var thenable = { + then: function(resolve) { + resolve(); + console.log('State 5'); + } +}; + +var thenableWithError = { + then: function(resolve) { + console.log('State 3'); + resolve(thenable); + console.log('State 4'); + throw new Error('ignored exception'); + } +}; + +function executor(resolve, reject) { + console.log('State 1'); + resolve(thenableWithError); + console.log('State 2'); +} + +new Promise(executor).then(() => {console.log('Done')}, () => {console.log('Error')}); \ No newline at end of file diff --git a/test/js/promise_s19.js b/test/js/promise_s19.js new file mode 100644 index 00000000..451f710e --- /dev/null +++ b/test/js/promise_s19.js @@ -0,0 +1,33 @@ +var returnValue = null; +var value = {}; +var resolve; + +var poisonedThen = Object.defineProperty({}, 'then', { + get: function() { + console.log('Throw!'); + throw value; + } +}); + +var promise = new Promise(function(_resolve) { + resolve = _resolve; +}); + +promise.then( +function() { + console.log('Resolve!'); + console.log('The promise should not be fulfilled.'); +}, +function(val) { + console.log('Reject!'); + if (val !== value) { + console.log('The promise should be fulfilled with the provided value.'); + return; + } + + console.log('Done'); +}); + +returnValue = resolve(poisonedThen); + +console.log(returnValue); \ No newline at end of file diff --git a/test/js/promise_s2.js b/test/js/promise_s2.js new file mode 100644 index 00000000..92d92dd0 --- /dev/null +++ b/test/js/promise_s2.js @@ -0,0 +1,14 @@ +var promise = new Promise(function(resolve, reject) { + console.log('One'); + reject(123); +}); + +console.log('Two'); + +promise.then(() => {console.log('Four'); return {num: 'Five'}}) +.then((obj) => {console.log(obj.num); return {num: 'Six'}}) +.then((obj) => {console.log(obj.num); return {num: 'Seven'}}) +.then((obj) => {console.log(obj.num); return {num: 'Eight'}}) +.then((obj) => {console.log(obj.num)}); + +console.log('Three'); \ No newline at end of file diff --git a/test/js/promise_s20.js b/test/js/promise_s20.js new file mode 100644 index 00000000..0934bba0 --- /dev/null +++ b/test/js/promise_s20.js @@ -0,0 +1,23 @@ +var returnValue = null; +var value = {}; +var poisonedThen = Object.defineProperty({}, 'then', { + get: function() { + throw value; + } +}); +var promise = new Promise(function(resolve) { + returnValue = resolve(poisonedThen); +}); + +console.log(returnValue == undefined); + +promise.then(function() { + console.log('The promise should not be fulfilled.'); +}, function(val) { + if (val !== value) { + console.log('The promise should be fulfilled with the provided value.'); + return; + } + + console.log('Done'); +}); \ No newline at end of file diff --git a/test/js/promise_s21.js b/test/js/promise_s21.js new file mode 100644 index 00000000..aad00cca --- /dev/null +++ b/test/js/promise_s21.js @@ -0,0 +1,30 @@ +var value = {}; +var resolve; +var poisonedThen = Object.defineProperty({}, 'then', { + get: function() { + throw value; + } +}); + +var p1 = new Promise(function(_resolve) { + resolve = _resolve; +}); + +var p2; + +p2 = p1.then(function() { + return poisonedThen; +}); + +p2.then(function(x) { + console.log('The promise should not be fulfilled.'); +}, function(x) { + if (x !== value) { + console.log('The promise should be rejected with the thrown exception.'); + return; + } + + console.log('Done'); +}); + +resolve(); \ No newline at end of file diff --git a/test/js/promise_s22.js b/test/js/promise_s22.js new file mode 100644 index 00000000..9dee7c17 --- /dev/null +++ b/test/js/promise_s22.js @@ -0,0 +1,32 @@ +var returnValue = null; +var value = {}; +var resolve; + +var thenable = new Promise(function(resolve) { + resolve(); +}); + +var promise = new Promise(function(_resolve) { + resolve = _resolve; +}); + +thenable.then = function(resolve) { + resolve(value); +}; + +promise.then( +function(val) { + if (val !== value) { + console.log('The promise should be fulfilled with the provided value.'); + return; + } + + console.log('Done'); +}, +function() { + console.log('The promise should not be rejected.'); +}); + +returnValue = resolve(thenable); + +console.log(returnValue == undefined); \ No newline at end of file diff --git a/test/js/promise_s23.js b/test/js/promise_s23.js new file mode 100644 index 00000000..8525cdea --- /dev/null +++ b/test/js/promise_s23.js @@ -0,0 +1,28 @@ +var returnValue = null; +var resolve; + +var promise = new Promise(function(_resolve) { + resolve = _resolve; +}); + +promise.then( +function() { + console.log('The promise should not be fulfilled.'); +}, +function(reason) { + if (!reason) { + console.log('The promise should be rejected with a value.'); + return; + } + + if (reason.constructor !== TypeError) { + console.log('The promise should be rejected with a TypeError instance.'); + return; + } + + console.log('Done'); +}); + +returnValue = resolve(promise); + +console.log(returnValue == undefined) \ No newline at end of file diff --git a/test/js/promise_s24.js b/test/js/promise_s24.js new file mode 100644 index 00000000..bb3dddff --- /dev/null +++ b/test/js/promise_s24.js @@ -0,0 +1,13 @@ +var checkPoint = ''; + +Promise.reject.call(function(executor) { + checkPoint += 'a'; + executor(); + + checkPoint += 'b'; + executor(function() {}, function() {}); + + checkPoint += 'c'; +}, {}); + +console.log(checkPoint == 'abc'); \ No newline at end of file diff --git a/test/js/promise_s25.js b/test/js/promise_s25.js new file mode 100644 index 00000000..3b70e46d --- /dev/null +++ b/test/js/promise_s25.js @@ -0,0 +1,29 @@ +var resolve, reject; +var promise = new Promise(function(_resolve, _reject) { + resolve = _resolve; + reject = _reject; +}); + +var P = function(executor) { + executor(resolve, reject); + return promise; +}; + +Promise.resolve.call(P, promise) +.then( +function() { + console.log('The promise should not be fulfilled.'); +}, +function(value) { + if (!value) { + console.log('The promise should be rejected with a value.'); + return; + } + + if (value.constructor !== TypeError) { + console.log('The promise should be rejected with a TypeError instance.'); + return; + } + + console.log('Done'); +}); \ No newline at end of file diff --git a/test/js/promise_s26.js b/test/js/promise_s26.js new file mode 100644 index 00000000..f5244d33 --- /dev/null +++ b/test/js/promise_s26.js @@ -0,0 +1,144 @@ +var inherits = (child, parent) => { + child.prototype = Object.create(parent.prototype, { + constructor: { + value: child, + enumerable: false, + writable: true, + configurable: true + } + }); + Object.setPrototypeOf(child, parent); +}; + + +var BoxedPromise = (() => { + function BoxedPromise(executor) { + console.log('BoxedPromise.constructor'); + + if (!(this instanceof BoxedPromise)) { + return Promise(executor); + } + + if (typeof executor !== 'function') { + return new Promise(executor); + } + + var context, args; + var promise = new Promise(wrappedExecutor); + this.boxed = promise; + + try { + executor.apply(context, args); + + } catch (e) { + args[1](e); + } + + function wrappedExecutor(resolve, reject) { + context = this; + args = [wrappedResolve, wrappedReject]; + function wrappedResolve(val) { + return resolve(val); + } + function wrappedReject(val) { + return reject(val); + } + } + } + + inherits(BoxedPromise, Promise); + + BoxedPromise.prototype.then = function(res, rej) { + console.log('BoxedPromise.prototype.then'); + var rs = Object.create(Object.getPrototypeOf(this)); + rs.boxed = this.boxed.then(res, rej); + return rs; + }; + + return BoxedPromise; +})(); + + +var PatchedPromise = (() => { + function PatchedPromise(executor) { + console.log('PatchedPromise.constructor'); + + if (!(this instanceof PatchedPromise)) { + return Promise(executor); + } + + if (typeof executor !== 'function') { + return new Promise(executor); + } + + var context, args; + var promise = new Promise(wrappedExecutor); + Object.setPrototypeOf(promise, PatchedPromise.prototype); + + try { + executor.apply(context, args); + + } catch (e) { + args[1](e); + } + + return promise; + + function wrappedExecutor(resolve, reject) { + context = this; + args = [wrappedResolve, wrappedReject]; + function wrappedResolve(val) { + return resolve(val); + } + function wrappedReject(val) { + return reject(val); + } + } + } + + inherits(PatchedPromise, Promise); + + return PatchedPromise; +})(); + + +var testSubclass = (Class, name) => { + return new Promise((resolve) => { + var resolved = Class.resolve(name) + .then((x) => console.log('resolved', name)); + + console.log(name, 'resolve', resolved instanceof Class ? 'OK' : 'failed'); + + + var rejected = Class.reject(name) + .catch((x) => console.log('rejected', name)); + + console.log(name, 'reject', rejected instanceof Class ? 'OK' : 'failed'); + + + var instance = new Class((resolve) => { + setImmediate(() => resolve(name)); + }); + + var chain = instance + .then((x) => { console.log('then', x); return x; }) + .then((x) => { console.log('then', x); return x; }); + + console.log(name, 'chain', chain instanceof Class ? 'OK' : 'failed'); + + var fin = chain + .finally(() => console.log('finally', name)); + + console.log(name, 'finally', fin instanceof Class ? 'OK' : 'failed'); + + console.log(name, 'sync done\n'); + + fin + .then(() => console.log(name, 'async done\n')) + .then(resolve); + }); +}; + +Promise.resolve() + .then(() => testSubclass(BoxedPromise, 'BoxedPromise')) + .then(() => testSubclass(PatchedPromise, 'PatchedPromise')); diff --git a/test/js/promise_s3.js b/test/js/promise_s3.js new file mode 100644 index 00000000..8e71fe3e --- /dev/null +++ b/test/js/promise_s3.js @@ -0,0 +1,11 @@ + +var promise = new Promise(function(resolve, reject) { + console.log('One'); + reject(new Error('Blah')); +}); + +console.log('Two'); + +promise.then((response) => console.log(`Fulfilled: ${response}`), (error) => console.log(`Rejected: ${error}`)); + +console.log('Three'); \ No newline at end of file diff --git a/test/js/promise_s4.js b/test/js/promise_s4.js new file mode 100644 index 00000000..ce52e6bd --- /dev/null +++ b/test/js/promise_s4.js @@ -0,0 +1,6 @@ +Promise.reject(new Error('Oh my')).then(function(success) { +}, +function(error) { + console.log(error); + throw error; +}); \ No newline at end of file diff --git a/test/js/promise_s5.js b/test/js/promise_s5.js new file mode 100644 index 00000000..91995f94 --- /dev/null +++ b/test/js/promise_s5.js @@ -0,0 +1,7 @@ + +Promise.resolve('Success').then(function(value) { + console.log(value); +}, +function(value) { + console.log('ignored'); +}); \ No newline at end of file diff --git a/test/js/promise_s6.js b/test/js/promise_s6.js new file mode 100644 index 00000000..9451c6ec --- /dev/null +++ b/test/js/promise_s6.js @@ -0,0 +1,4 @@ +var p = Promise.resolve([1,2,3]); +p.then(function(v) { + console.log(v[0]); +}); \ No newline at end of file diff --git a/test/js/promise_s7.js b/test/js/promise_s7.js new file mode 100644 index 00000000..5ecc378e --- /dev/null +++ b/test/js/promise_s7.js @@ -0,0 +1,12 @@ +var p1 = Promise.resolve({ + then: function(onFulfill, onReject) { onFulfill('fulfilled!'); } +}); + +console.log(p1 instanceof Promise); + +p1.then(function(v) { + console.log(v); +}, +function(e) { + console.log('ignored'); +}); \ No newline at end of file diff --git a/test/js/promise_s8.js b/test/js/promise_s8.js new file mode 100644 index 00000000..da87ff7c --- /dev/null +++ b/test/js/promise_s8.js @@ -0,0 +1,13 @@ +var thenable = { + then: function(resolve) { + console.log('Ok') + resolve(); + } +}; + +function executor(resolve, reject) { + resolve(thenable); + throw new Error('ignored'); +} + +new Promise(executor).then(() => {}); \ No newline at end of file diff --git a/test/js/promise_s9.js b/test/js/promise_s9.js new file mode 100644 index 00000000..4c014bbe --- /dev/null +++ b/test/js/promise_s9.js @@ -0,0 +1,10 @@ +var executorFunction; + +function NotPromise(executor) { + executorFunction = executor; + executor(function() {console.log('S')}, function() {console.log('R')}); +} + +Promise.resolve.call(NotPromise); + +console.log(Object.isExtensible(executorFunction)); \ No newline at end of file diff --git a/test/js/promise_set_timeout.js b/test/js/promise_set_timeout.js new file mode 100644 index 00000000..c0d365bd --- /dev/null +++ b/test/js/promise_set_timeout.js @@ -0,0 +1,17 @@ +var res = []; +function abc() { + var promise = new Promise(function(resolve, reject) { + res.push('One'); + resolve(); + }); + res.push('Two'); + promise.then(() => {res.push('Four'); return {num: 'Five'}}) + .then((obj) => {res.push(obj.num); return {num: 'Six'}}) + .then((obj) => {res.push(obj.num); return {num: 'Seven'}}) + .then((obj) => {res.push(obj.num); return {num: 'Eight'}}) + .then((obj) => {res.push(obj.num)}); + res.push('Three'); +} +abc(); +console.log(res.join(',')); +setTimeout(() => console.log(res.join(',')), 0); diff --git a/test/njs_expect_test.exp b/test/njs_expect_test.exp index c5b9e59e..b3eb186f 100644 --- a/test/njs_expect_test.exp +++ b/test/njs_expect_test.exp @@ -860,3 +860,173 @@ njs_test { njs_run {"-v"} "\\d+\.\\d+\.\\d+" + +# Promise + +njs_run {"./test/js/promise_set_timeout.js"} \ +"One,Two,Three +One,Two,Three,Four,Five,Six,Seven,Eight" + +njs_run {"./test/js/promise_s1.js"} \ +"One +Two +Three +123 +Four +Five +Six +Seven +Eight" + +njs_run {"./test/js/promise_s2.js"} \ +"One +Two +Three" + +njs_run {"./test/js/promise_s3.js"} \ +"One +Two +Three +Rejected: Error: Blah" + +njs_run {"./test/js/promise_s4.js"} \ +"Error: Oh my" + +njs_run {"./test/js/promise_s5.js"} \ +"Success" + +njs_run {"./test/js/promise_s6.js"} \ +"1" + +njs_run {"./test/js/promise_s7.js"} \ +"true +fulfilled!" + +njs_run {"./test/js/promise_s8.js"} \ +"Ok" + +njs_run {"./test/js/promise_s9.js"} \ +"S +true" + +njs_run {"./test/js/promise_s10.js"} \ +"One +Two +Three +Oh no" + +njs_run {"./test/js/promise_s11.js"} \ +"end +S: all +R: all" + +njs_run {"./test/js/promise_s12.js"} \ +"Done" + +njs_run {"./test/js/promise_s13.js"} \ +"Done" + +njs_run {"./test/js/promise_s14.js"} \ +"TypeError: oops + at anonymous \\\(promise_s14.js:4\\\) + at native \\\(native\\\) + at main \\\(native\\\) + +Done" + +njs_run {"./test/js/promise_s15.js"} \ +"Done" + +njs_run {"./test/js/promise_s16.js"} \ +"Done" + +njs_run {"./test/js/promise_s17.js"} \ +"Done" + +njs_run {"./test/js/promise_s18.js"} \ +"State 1 +State 2 +State 3 +State 4 +State 5 +Done" + +njs_run {"./test/js/promise_s19.js"} \ +"Throw! +undefined +Reject! +Done" + +njs_run {"./test/js/promise_s20.js"} \ +"true +Done" + +njs_run {"./test/js/promise_s21.js"} \ +"Done" + +njs_run {"./test/js/promise_s22.js"} \ +"true +Done" + +njs_run {"./test/js/promise_s23.js"} \ +"true +Done" + +njs_run {"./test/js/promise_s24.js"} \ +"true" + +njs_run {"./test/js/promise_s25.js"} \ +"Done" + +njs_run {"./test/js/promise_s26.js"} \ +"BoxedPromise.constructor +BoxedPromise.prototype.then +BoxedPromise resolve OK +BoxedPromise.constructor +BoxedPromise.prototype.then +BoxedPromise reject OK +BoxedPromise.constructor +BoxedPromise.prototype.then +BoxedPromise.prototype.then +BoxedPromise chain OK +BoxedPromise.prototype.then +BoxedPromise finally OK +BoxedPromise sync done + +BoxedPromise.prototype.then +BoxedPromise.prototype.then +resolved BoxedPromise +rejected BoxedPromise +then BoxedPromise +then BoxedPromise +finally BoxedPromise +BoxedPromise.constructor +BoxedPromise.prototype.then +BoxedPromise.prototype.then +BoxedPromise async done + +PatchedPromise.constructor +PatchedPromise.constructor +PatchedPromise resolve OK +PatchedPromise.constructor +PatchedPromise.constructor +PatchedPromise reject OK +PatchedPromise.constructor +PatchedPromise.constructor +PatchedPromise.constructor +PatchedPromise chain OK +PatchedPromise.constructor +PatchedPromise finally OK +PatchedPromise sync done + +PatchedPromise.constructor +PatchedPromise.constructor +resolved PatchedPromise +rejected PatchedPromise +then PatchedPromise +then PatchedPromise +finally PatchedPromise +PatchedPromise.constructor +PatchedPromise.constructor +PatchedPromise.constructor +PatchedPromise async done"