
/*
 * Copyright (C) Igor Sysoev
 * Copyright (C) NGINX, Inc.
 */


#include <njs_main.h>


njs_function_t *
njs_function_alloc(njs_vm_t *vm, njs_function_lambda_t *lambda,
    njs_bool_t async)
{
    size_t          size;
    njs_object_t    *proto;
    njs_function_t  *function;

    size = sizeof(njs_function_t) + lambda->nclosures * sizeof(njs_value_t *);

    function = njs_mp_zalloc(vm->mem_pool, size);
    if (njs_slow_path(function == NULL)) {
        goto fail;
    }

    /*
     * njs_mp_zalloc() does also:
     *   njs_lvlhsh_init(&function->object.hash);
     *   function->object.__proto__ = NULL;
     */

    function->ctor = lambda->ctor;
    function->args_offset = 1;
    function->u.lambda = lambda;

    if (function->ctor) {
        function->object.shared_hash = vm->shared->function_instance_hash;

    } else if (async) {
        function->object.shared_hash = vm->shared->async_function_instance_hash;

    } else {
        function->object.shared_hash = vm->shared->arrow_instance_hash;
    }

    if (async) {
        proto = &vm->prototypes[NJS_OBJ_TYPE_ASYNC_FUNCTION].object;

    } else {
        proto = &vm->prototypes[NJS_OBJ_TYPE_FUNCTION].object;
    }

    function->object.__proto__ = proto;
    function->object.type = NJS_FUNCTION;

    function->object.extensible = 1;

    return function;

fail:

    njs_memory_error(vm);

    return NULL;
}


njs_function_t *
njs_vm_function_alloc(njs_vm_t *vm, njs_function_native_t native)
{
    njs_function_t  *function;

    function = njs_mp_zalloc(vm->mem_pool, sizeof(njs_function_t));
    if (njs_slow_path(function == NULL)) {
        return NULL;
    }

    function->native = 1;
    function->args_offset = 1;
    function->u.native = native;

    return function;
}


njs_function_t *
njs_function_value_copy(njs_vm_t *vm, njs_value_t *value)
{
    njs_function_t     *function, *copy;
    njs_object_type_t  type;

    function = njs_function(value);

    if (!function->object.shared) {
        return function;
    }

    copy = njs_function_copy(vm, function);
    if (njs_slow_path(copy == NULL)) {
        njs_memory_error(vm);
        return NULL;
    }

    type = njs_function_object_type(vm, function);

    if (copy->ctor) {
        copy->object.shared_hash = vm->shared->function_instance_hash;

    } else if (type == NJS_OBJ_TYPE_ASYNC_FUNCTION) {
        copy->object.shared_hash = vm->shared->async_function_instance_hash;

    } else {
        copy->object.shared_hash = vm->shared->arrow_instance_hash;
    }

    value->data.u.function = copy;

    return copy;
}


njs_int_t
njs_function_name_set(njs_vm_t *vm, njs_function_t *function,
    njs_value_t *name, const char *prefix)
{
    u_char              *p;
    size_t              len, symbol;
    njs_int_t           ret;
    njs_value_t         value;
    njs_string_prop_t   string;
    njs_object_prop_t   *prop;
    njs_lvlhsh_query_t  lhq;

    prop = njs_object_prop_alloc(vm, &njs_string_name, name, 0);
    if (njs_slow_path(prop == NULL)) {
        return NJS_ERROR;
    }

    symbol = 0;

    if (njs_is_symbol(&prop->value)) {
        symbol = 2;
        prop->value = *njs_symbol_description(&prop->value);
    }

    if (prefix != NULL || symbol != 0) {
        value = prop->value;
        (void) njs_string_prop(&string, &value);

        len = (prefix != NULL) ? njs_strlen(prefix) + 1: 0;
        p = njs_string_alloc(vm, &prop->value, string.size + len + symbol,
                             string.length + len + symbol);
        if (njs_slow_path(p == NULL)) {
            return NJS_ERROR;
        }

        if (len != 0) {
            p = njs_cpymem(p, prefix, len - 1);
            *p++ = ' ';
        }

        if (symbol != 0) {
            *p++ = '[';
        }

        p = njs_cpymem(p, string.start, string.size);

        if (symbol != 0) {
            *p++ = ']';
        }
    }

    prop->configurable = 1;

    lhq.key_hash = NJS_NAME_HASH;
    lhq.key = njs_str_value("name");
    lhq.replace = 0;
    lhq.value = prop;
    lhq.proto = &njs_object_hash_proto;
    lhq.pool = vm->mem_pool;

    ret = njs_lvlhsh_insert(&function->object.hash, &lhq);
    if (njs_slow_path(ret != NJS_OK)) {
        njs_internal_error(vm, "lvlhsh insert failed");
        return NJS_ERROR;
    }

    return NJS_OK;
}


njs_function_t *
njs_function_copy(njs_vm_t *vm, njs_function_t *function)
{
    size_t             size, n;
    njs_value_t        **from, **to;
    njs_function_t     *copy;
    njs_object_type_t  type;

    n = (function->native) ? 0 : function->u.lambda->nclosures;

    size = sizeof(njs_function_t) + n * sizeof(njs_value_t *);

    copy = njs_mp_alloc(vm->mem_pool, size);
    if (njs_slow_path(copy == NULL)) {
        return NULL;
    }

    *copy = *function;

    type = njs_function_object_type(vm, function);

    copy->object.__proto__ = &vm->prototypes[type].object;
    copy->object.shared = 0;

    if (n == 0) {
        return copy;
    }

    from = njs_function_closures(function);
    to = njs_function_closures(copy);

    do {
        n--;

        to[n] = from[n];

    } while (n != 0);

    return copy;
}


njs_int_t
njs_function_arguments_object_init(njs_vm_t *vm, njs_native_frame_t *frame)
{
    njs_int_t           ret;
    njs_uint_t          nargs, n;
    njs_value_t         value;
    njs_object_t        *arguments;
    njs_object_prop_t   *prop;
    njs_lvlhsh_query_t  lhq;

    static const njs_value_t  njs_string_length = njs_string("length");

    arguments = njs_object_alloc(vm);
    if (njs_slow_path(arguments == NULL)) {
        return NJS_ERROR;
    }

    arguments->shared_hash = vm->shared->arguments_object_instance_hash;

    nargs = frame->nargs;

    njs_set_number(&value, nargs);

    prop = njs_object_prop_alloc(vm, &njs_string_length, &value, 1);
    if (njs_slow_path(prop == NULL)) {
        return NJS_ERROR;
    }

    prop->enumerable = 0;

    lhq.value = prop;
    lhq.key_hash = NJS_LENGTH_HASH;
    njs_string_get(&prop->name, &lhq.key);

    lhq.replace = 0;
    lhq.pool = vm->mem_pool;
    lhq.proto = &njs_object_hash_proto;

    ret = njs_lvlhsh_insert(&arguments->hash, &lhq);
    if (njs_slow_path(ret != NJS_OK)) {
        njs_internal_error(vm, "lvlhsh insert failed");
        return NJS_ERROR;
    }

    for (n = 0; n < nargs; n++) {
        njs_uint32_to_string(&value, n);

        prop = njs_object_prop_alloc(vm, &value, &frame->arguments[n], 1);
        if (njs_slow_path(prop == NULL)) {
            return NJS_ERROR;
        }

        lhq.value = prop;
        njs_string_get(&prop->name, &lhq.key);
        lhq.key_hash = njs_djb_hash(lhq.key.start, lhq.key.length);

        ret = njs_lvlhsh_insert(&arguments->hash, &lhq);
        if (njs_slow_path(ret != NJS_OK)) {
            njs_internal_error(vm, "lvlhsh insert failed");
            return NJS_ERROR;
        }
    }

    frame->arguments_object = arguments;

    return NJS_OK;
}


njs_int_t
njs_function_rest_parameters_init(njs_vm_t *vm, njs_native_frame_t *frame)
{
    uint32_t     length;
    njs_uint_t   nargs, n, i;
    njs_array_t  *array;
    njs_value_t  *rest_arguments;

    nargs = frame->nargs;
    n = frame->function->u.lambda->nargs;
    length = (nargs >= n) ? (nargs - n + 1) : 0;

    array = njs_array_alloc(vm, 1, length, 0);
    if (njs_slow_path(array == NULL)) {
        return NJS_ERROR;
    }

    for (i = 0; i < length; i++) {
        array->start[i] = frame->arguments[i + n - 1];
    }

    rest_arguments = njs_mp_alloc(vm->mem_pool, sizeof(njs_value_t));
    if (njs_slow_path(rest_arguments == NULL)) {
        return NJS_ERROR;
    }

    /* GC: retain. */
    njs_set_array(rest_arguments, array);

    vm->top_frame->local[n] = rest_arguments;

    return NJS_OK;
}


static njs_int_t
njs_function_prototype_thrower(njs_vm_t *vm, njs_value_t *args,
    njs_uint_t nargs, njs_index_t unused)
{
    njs_type_error(vm, "\"caller\", \"callee\", \"arguments\" "
                   "properties may not be accessed");
    return NJS_ERROR;
}


const njs_object_prop_t  njs_arguments_object_instance_properties[] =
{
    {
        .type = NJS_PROPERTY,
        .name = njs_string("callee"),
        .value = njs_value(NJS_INVALID, 1, NAN),
        .getter = njs_native_function(njs_function_prototype_thrower, 0),
        .setter = njs_native_function(njs_function_prototype_thrower, 0),
        .writable = NJS_ATTRIBUTE_UNSET,
    },
};


const njs_object_init_t  njs_arguments_object_instance_init = {
    njs_arguments_object_instance_properties,
    njs_nitems(njs_arguments_object_instance_properties),
};


njs_int_t
njs_function_native_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)
{
    size_t              size;
    njs_uint_t          n;
    njs_value_t         *value, *bound;
    njs_native_frame_t  *frame;

    size = NJS_NATIVE_FRAME_SIZE
           + (function->args_offset + nargs) * sizeof(njs_value_t);

    frame = njs_function_frame_alloc(vm, size);
    if (njs_slow_path(frame == NULL)) {
        return NJS_ERROR;
    }

    frame->function = function;
    frame->nargs = function->args_offset + nargs;
    frame->ctor = ctor;
    frame->native = 1;
    frame->pc = NULL;

    value = (njs_value_t *) ((u_char *) frame + NJS_NATIVE_FRAME_SIZE);

    frame->arguments = value;
    frame->arguments_offset = value + function->args_offset;

    bound = function->bound;

    if (bound == NULL) {
        /* GC: njs_retain(this); */
        *value++ = *this;

    } else {
        n = function->args_offset;

        do {
            /* GC: njs_retain(bound); */
            *value++ = *bound++;
            n--;
        } while (n != 0);
    }

    if (args != NULL) {
        memcpy(value, args, nargs * sizeof(njs_value_t));
    }

    return NJS_OK;
}


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)
{
    size_t                 n, frame_size;
    uint32_t               args_count, value_count, value_size, temp_size;
    njs_value_t            *value, *bound, **new, **temp;
    njs_frame_t            *frame;
    njs_function_t         *target;
    njs_native_frame_t     *native_frame;
    njs_function_lambda_t  *lambda;

    bound = function->bound;

    if (njs_fast_path(bound == NULL)) {
        lambda = function->u.lambda;
        target = function;

    } else {
        target = function->u.bound_target;

        if (njs_slow_path(target->bound != NULL)) {

            /*
             * FIXME: bound functions should call target function with
             * bound "this" and bound args.
             */

            njs_internal_error(vm, "chain of bound function are not supported");
            return NJS_ERROR;
        }

        lambda = target->u.lambda;
    }

    args_count = function->args_offset + njs_max(nargs, lambda->nargs);
    value_count = args_count + njs_max(args_count, lambda->nlocal);

    value_size = value_count * sizeof(njs_value_t *);
    temp_size = lambda->temp * sizeof(njs_value_t *);

    frame_size = value_size + temp_size
                        + ((value_count + lambda->temp) * sizeof(njs_value_t));

    native_frame = njs_function_frame_alloc(vm, NJS_FRAME_SIZE + frame_size);
    if (njs_slow_path(native_frame == NULL)) {
        return NJS_ERROR;
    }

    /* Local */

    new = (njs_value_t **) ((u_char *) native_frame + NJS_FRAME_SIZE);
    value = (njs_value_t *) ((u_char *) new + value_size + temp_size);

    n = value_count + lambda->temp;

    while (n != 0) {
        n--;
        new[n] = &value[n];
        njs_set_invalid(new[n]);
    }

    /* Temp */

    temp = (njs_value_t **) ((u_char *) native_frame + NJS_FRAME_SIZE
                                                     + value_size);

    native_frame->arguments = value;
    native_frame->arguments_offset = value + (function->args_offset - 1);
    native_frame->local = new + args_count;
    native_frame->temp = temp;
    native_frame->function = target;
    native_frame->nargs = nargs;
    native_frame->ctor = ctor;
    native_frame->native = 0;
    native_frame->pc = NULL;

    /* Set this and bound arguments. */
    *native_frame->local[0] = *this;

    if (njs_slow_path(function->global_this
                      && njs_is_null_or_undefined(this)))
    {
        njs_set_object(native_frame->local[0], &vm->global_object);
    }

    if (bound != NULL) {
        n = function->args_offset;
        native_frame->nargs += n - 1;

        if (!ctor) {
            *native_frame->local[0] = *bound;
        }

        bound++;
        n--;

        while (n != 0) {
            *value++ = *bound++;
            n--;
        };
    }

    /* Copy arguments. */

    if (args != NULL) {
        while (nargs != 0) {
            *value++ = *args++;
            nargs--;
        }
    }

    frame = (njs_frame_t *) native_frame;
    frame->exception.catch = NULL;
    frame->exception.next = NULL;
    frame->previous_active_frame = vm->active_frame;

    return NJS_OK;
}


njs_native_frame_t *
njs_function_frame_alloc(njs_vm_t *vm, size_t size)
{
    size_t              spare_size, chunk_size;
    njs_native_frame_t  *frame;

    spare_size = vm->top_frame ? vm->top_frame->free_size : 0;

    if (njs_fast_path(size <= spare_size)) {
        frame = (njs_native_frame_t *) vm->top_frame->free;
        chunk_size = 0;

    } else {
        spare_size = size + NJS_FRAME_SPARE_SIZE;
        spare_size = njs_align_size(spare_size, NJS_FRAME_SPARE_SIZE);

        if (vm->stack_size + spare_size > NJS_MAX_STACK_SIZE) {
            njs_range_error(vm, "Maximum call stack size exceeded");
            return NULL;
        }

        frame = njs_mp_align(vm->mem_pool, sizeof(njs_value_t), spare_size);
        if (njs_slow_path(frame == NULL)) {
            njs_memory_error(vm);
            return NULL;
        }

        chunk_size = spare_size;
        vm->stack_size += spare_size;
    }

    njs_memzero(frame, sizeof(njs_native_frame_t));

    frame->size = chunk_size;
    frame->free_size = spare_size - size;
    frame->free = (u_char *) frame + size;

    frame->previous = vm->top_frame;
    vm->top_frame = frame;

    return frame;
}


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    ret;
    njs_value_t  dst njs_aligned(16);

    ret = njs_function_frame(vm, function, this, args, nargs, ctor);
    if (njs_slow_path(ret != NJS_OK)) {
        return ret;
    }

    ret = njs_function_frame_invoke(vm, &dst);

    if (ret == NJS_OK) {
        *retval = dst;
    }

    return ret;
}


njs_int_t
njs_function_lambda_call(njs_vm_t *vm)
{
    uint32_t               n;
    njs_int_t              ret;
    njs_frame_t            *frame;
    njs_value_t            *args, **local, *value;
    njs_value_t            **cur_local, **cur_closures, **cur_temp;
    njs_function_t         *function;
    njs_declaration_t      *declr;
    njs_function_lambda_t  *lambda;

    frame = (njs_frame_t *) vm->top_frame;
    function = frame->native.function;

    if (function->global && !function->closure_copied) {
        ret = njs_function_capture_global_closures(vm, function);
        if (njs_slow_path(ret != NJS_OK)) {
            return NJS_ERROR;
        }
    }

    lambda = function->u.lambda;

    args = vm->top_frame->arguments;
    local = vm->top_frame->local + function->args_offset;

    /* Move all arguments. */

    for (n = 0; n < function->args_count; n++) {
        if (!njs_is_valid(args)) {
            njs_set_undefined(args);
        }

        *local++ = args++;
    }

    /* Store current level. */

    cur_local = vm->levels[NJS_LEVEL_LOCAL];
    cur_closures = vm->levels[NJS_LEVEL_CLOSURE];
    cur_temp = vm->levels[NJS_LEVEL_TEMP];

    /* Replace current level. */

    vm->levels[NJS_LEVEL_LOCAL] = vm->top_frame->local;
    vm->levels[NJS_LEVEL_CLOSURE] = njs_function_closures(function);
    vm->levels[NJS_LEVEL_TEMP] = frame->native.temp;

    if (lambda->rest_parameters) {
        ret = njs_function_rest_parameters_init(vm, &frame->native);
        if (njs_slow_path(ret != NJS_OK)) {
            return NJS_ERROR;
        }
    }

    /* Self */

    if (lambda->self != NJS_INDEX_NONE) {
        value = njs_scope_value(vm, lambda->self);

        if (!njs_is_valid(value)) {
            njs_set_function(value, function);
        }
    }

    vm->active_frame = frame;

    /* Closures */

    n = lambda->ndeclarations;

    while (n != 0) {
        n--;

        declr = &lambda->declarations[n];
        value = njs_scope_value(vm, declr->index);

        *value = *declr->value;

        function = njs_function_value_copy(vm, value);
        if (njs_slow_path(function == NULL)) {
            return NJS_ERROR;
        }

        ret = njs_function_capture_closure(vm, function, function->u.lambda);
        if (njs_slow_path(ret != NJS_OK)) {
            return ret;
        }
    }

    ret = njs_vmcode_interpreter(vm, lambda->start);

    /* Restore current level. */
    vm->levels[NJS_LEVEL_LOCAL] = cur_local;
    vm->levels[NJS_LEVEL_CLOSURE] = cur_closures;
    vm->levels[NJS_LEVEL_TEMP] = cur_temp;

    return ret;
}


njs_int_t
njs_function_native_call(njs_vm_t *vm)
{
    njs_int_t              ret;
    njs_function_t         *function, *target;
    njs_native_frame_t     *native, *previous;
    njs_function_native_t  call;

    native = vm->top_frame;
    function = native->function;

    if (njs_fast_path(function->bound == NULL)) {
        call = function->u.native;

    } else {
        target = function->u.bound_target;

        if (njs_slow_path(target->bound != NULL)) {
            njs_internal_error(vm, "chain of bound function are not supported");
            return NJS_ERROR;
        }

        call = target->u.native;
    }

    ret = call(vm, native->arguments, native->nargs, function->magic8);
    if (njs_slow_path(ret == NJS_ERROR)) {
        return ret;
    }

    if (ret == NJS_DECLINED) {
        return NJS_OK;
    }

    previous = njs_function_previous_frame(native);

    njs_vm_scopes_restore(vm, native, previous);

    if (!native->skip) {
        *native->retval = vm->retval;
    }

    njs_function_frame_free(vm, native);

    return NJS_OK;
}


njs_int_t
njs_function_frame_invoke(njs_vm_t *vm, njs_value_t *retval)
{
    njs_native_frame_t  *frame;

    frame = vm->top_frame;
    frame->retval = retval;

    if (njs_function_object_type(vm, frame->function)
        == NJS_OBJ_TYPE_ASYNC_FUNCTION)
    {
        return njs_async_function_frame_invoke(vm, retval);
    }

    if (frame->native) {
        return njs_function_native_call(vm);

    } else {
        return njs_function_lambda_call(vm);
    }
}


void
njs_function_frame_free(njs_vm_t *vm, njs_native_frame_t *native)
{
    njs_native_frame_t  *previous;

    do {
        previous = native->previous;

        /* GC: free frame->local, etc. */

        if (native->size != 0) {
            vm->stack_size -= native->size;
            njs_mp_free(vm->mem_pool, native);
        }

        native = previous;
    } while (native->skip);
}


njs_int_t
njs_function_frame_save(njs_vm_t *vm, njs_native_frame_t *native, u_char *pc)
{
    size_t              value_count, n;
    njs_value_t         *start, *end, *p, **new, *value, **local;
    njs_function_t      *function;
    njs_native_frame_t  *active;

    active = &vm->active_frame->native;
    value_count = njs_function_frame_value_count(active);

    function = active->function;

    new = (njs_value_t **) ((u_char *) native + NJS_FRAME_SIZE);
    value = (njs_value_t *) (new + value_count
                             + function->u.lambda->temp);

    *native = *active;

    native->arguments = value;
    native->arguments_offset = value + (function->args_offset - 1);
    native->local = new + njs_function_frame_args_count(active);
    native->temp = new + value_count;
    native->pc = pc;

    start = njs_function_frame_values(active, &end);
    p = native->arguments;

    while (start < end) {
        *p = *start++;
        *new++ = p++;
    }

    /* Move all arguments. */

    p = native->arguments;
    local = native->local + function->args_offset;

    for (n = 0; n < function->args_count; n++) {
        if (!njs_is_valid(p)) {
            njs_set_undefined(p);
        }

        *local++ = p++;
    }

    return NJS_OK;
}


njs_object_type_t
njs_function_object_type(njs_vm_t *vm, njs_function_t *function)
{
    if (function->object.shared_hash.slot
        == vm->shared->async_function_instance_hash.slot)
    {
        return NJS_OBJ_TYPE_ASYNC_FUNCTION;
    }

    return NJS_OBJ_TYPE_FUNCTION;
}


njs_int_t
njs_function_capture_closure(njs_vm_t *vm, njs_function_t *function,
    njs_function_lambda_t *lambda)
{
    void                *start, *end;
    uint32_t            n;
    njs_value_t         *value, **closure;
    njs_native_frame_t  *frame;

    if (lambda->nclosures == 0) {
        return NJS_OK;
    }

    frame = &vm->active_frame->native;

    while (frame->native) {
        frame = frame->previous;
    }

    start = frame;
    end = frame->free;

    closure = njs_function_closures(function);
    n = lambda->nclosures;

    do {
        n--;

        value = njs_scope_value(vm, lambda->closures[n]);

        if (start <= (void *) value && (void *) value < end) {
            value = njs_scope_value_clone(vm, lambda->closures[n], value);
            if (njs_slow_path(value == NULL)) {
                return NJS_ERROR;
            }
        }

        closure[n] = value;

    } while (n != 0);

    return NJS_OK;
}


njs_inline njs_value_t *
njs_function_closure_value(njs_vm_t *vm, njs_value_t **scope, njs_index_t index,
    void *start, void *end)
{
    njs_value_t  *value, *newval;

    value = scope[njs_scope_index_value(index)];

    if (start <= (void *) value && end > (void *) value) {
        newval = njs_mp_alloc(vm->mem_pool, sizeof(njs_value_t));
        if (njs_slow_path(newval == NULL)) {
            njs_memory_error(vm);
            return NULL;
        }

        *newval = *value;
        value = newval;
    }

    scope[njs_scope_index_value(index)] = value;

    return value;
}


njs_int_t
njs_function_capture_global_closures(njs_vm_t *vm, njs_function_t *function)
{
    void                   *start, *end;
    uint32_t               n;
    njs_value_t            *value, **refs, **global;
    njs_index_t            *indexes, index;
    njs_native_frame_t     *native;
    njs_function_lambda_t  *lambda;

    lambda = function->u.lambda;

    if (lambda->nclosures == 0) {
        return NJS_OK;
    }

    native = vm->top_frame;

    while (native->previous->function != NULL) {
        native = native->previous;
    }

    start = native;
    end = native->free;

    indexes = lambda->closures;
    refs = njs_function_closures(function);

    global = vm->levels[NJS_LEVEL_GLOBAL];

    n = lambda->nclosures;

    while (n > 0) {
        n--;

        index = indexes[n];

        switch (njs_scope_index_type(index)) {
        case NJS_LEVEL_LOCAL:
            value = njs_function_closure_value(vm, native->local, index,
                                               start, end);
            break;

        case NJS_LEVEL_GLOBAL:
            value = njs_function_closure_value(vm, global, index, start, end);
            break;

        default:
            njs_type_error(vm, "unexpected value type for closure \"%uD\"",
                           njs_scope_index_type(index));
            return NJS_ERROR;
        }

        if (njs_slow_path(value == NULL)) {
            return NJS_ERROR;
        }

        refs[n] = value;
    }

    function->closure_copied = 1;

    return NJS_OK;
}


static njs_value_t *
njs_function_property_prototype_set(njs_vm_t *vm, njs_lvlhsh_t *hash,
    njs_value_t *prototype)
{
    njs_int_t           ret;
    njs_object_prop_t   *prop;
    njs_lvlhsh_query_t  lhq;

    const njs_value_t  proto_string = njs_string("prototype");

    prop = njs_object_prop_alloc(vm, &proto_string, prototype, 0);
    if (njs_slow_path(prop == NULL)) {
        return NULL;
    }

    prop->writable = 1;

    lhq.value = prop;
    lhq.key_hash = NJS_PROTOTYPE_HASH;
    lhq.key = njs_str_value("prototype");
    lhq.replace = 1;
    lhq.pool = vm->mem_pool;
    lhq.proto = &njs_object_hash_proto;

    ret = njs_lvlhsh_insert(hash, &lhq);

    if (njs_fast_path(ret == NJS_OK)) {
        return &prop->value;
    }

    njs_internal_error(vm, "lvlhsh insert failed");

    return NULL;
}


/*
 * The "prototype" property of user defined functions is created on
 * demand in private hash of the functions by the "prototype" getter.
 * The getter creates a copy of function which is private to nJSVM,
 * adds a "prototype" object property to the copy, and then adds a
 * "constructor" property in the prototype object.  The "constructor"
 * property points to the copy of function:
 *   "F.prototype.constructor === F"
 */

njs_int_t
njs_function_prototype_create(njs_vm_t *vm, njs_object_prop_t *prop,
    njs_value_t *value, njs_value_t *setval, njs_value_t *retval)
{
    njs_value_t     *proto, proto_value, *cons;
    njs_object_t    *prototype;
    njs_function_t  *function;

    if (setval == NULL) {
        prototype = njs_object_alloc(vm);
        if (njs_slow_path(prototype == NULL)) {
            return NJS_ERROR;
        }

        njs_set_object(&proto_value, prototype);

        setval = &proto_value;
    }

    function = njs_function_value_copy(vm, value);
    if (njs_slow_path(function == NULL)) {
        return NJS_ERROR;
    }

    proto = njs_function_property_prototype_set(vm, njs_object_hash(value),
                                                setval);
    if (njs_slow_path(proto == NULL)) {
        return NJS_ERROR;
    }

    if (setval == &proto_value && njs_is_object(proto)) {
        /* Only in getter context. */
        cons = njs_property_constructor_set(vm, njs_object_hash(proto), value);
        if (njs_slow_path(cons == NULL)) {
            return NJS_ERROR;
        }
    }

    *retval = *proto;

    return NJS_OK;
}


njs_int_t
njs_function_constructor(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
    njs_index_t async)
{
    njs_chb_t               chain;
    njs_int_t               ret;
    njs_str_t               str, file;
    njs_uint_t              i;
    njs_lexer_t             lexer;
    njs_parser_t            parser;
    njs_vm_code_t           *code;
    njs_function_t          *function;
    njs_generator_t         generator;
    njs_parser_node_t       *node;
    njs_parser_scope_t      *scope;
    njs_function_lambda_t   *lambda;
    const njs_token_type_t  *type;

    static const njs_token_type_t  safe_ast[] = {
        NJS_TOKEN_END,
        NJS_TOKEN_FUNCTION_EXPRESSION,
        NJS_TOKEN_STATEMENT,
        NJS_TOKEN_RETURN,
        NJS_TOKEN_THIS,
        NJS_TOKEN_ILLEGAL
    };

    static const njs_token_type_t  safe_ast_async[] = {
        NJS_TOKEN_END,
        NJS_TOKEN_ASYNC_FUNCTION_EXPRESSION,
        NJS_TOKEN_STATEMENT,
        NJS_TOKEN_RETURN,
        NJS_TOKEN_THIS,
        NJS_TOKEN_ILLEGAL
    };

    if (!vm->options.unsafe && nargs != 2) {
        goto fail;
    }

    njs_chb_init(&chain, vm->mem_pool);

    if (async) {
        njs_chb_append_literal(&chain, "(async function(");

    } else {
        njs_chb_append_literal(&chain, "(function(");
    }

    for (i = 1; i < nargs - 1; i++) {
        ret = njs_value_to_chain(vm, &chain, njs_argument(args, i));
        if (njs_slow_path(ret < NJS_OK)) {
            return ret;
        }

        if (i != (nargs - 2)) {
            njs_chb_append_literal(&chain, ",");
        }
    }

    njs_chb_append_literal(&chain, "){");

    ret = njs_value_to_chain(vm, &chain, njs_argument(args, nargs - 1));
    if (njs_slow_path(ret < NJS_OK)) {
        return ret;
    }

    njs_chb_append_literal(&chain, "})");

    ret = njs_chb_join(&chain, &str);
    if (njs_slow_path(ret != NJS_OK)) {
        njs_memory_error(vm);
        return NJS_ERROR;
    }

    file = njs_str_value("runtime");

    ret = njs_lexer_init(vm, &lexer, &file, str.start, str.start + str.length,
                         1);
    if (njs_slow_path(ret != NJS_OK)) {
        return ret;
    }

    njs_memzero(&parser, sizeof(njs_parser_t));

    parser.lexer = &lexer;

    ret = njs_parser(vm, &parser);
    if (njs_slow_path(ret != NJS_OK)) {
        return ret;
    }

    if (!vm->options.unsafe) {
        /*
         * Safe mode exception:
         * "(new Function('return this'))" is often used to get
         * the global object in a portable way.
         */

        node = parser.node;
        type = (async) ? &safe_ast_async[0] : &safe_ast[0];

        for (; *type != NJS_TOKEN_ILLEGAL; type++, node = node->right) {
            if (node == NULL) {
                goto fail;
            }

            if (node->left != NULL
                && node->token_type != NJS_TOKEN_FUNCTION_EXPRESSION
                && node->left->token_type != NJS_TOKEN_NAME)
            {
                goto fail;
            }

            if (node->token_type != *type) {
                goto fail;
            }
        }
    }

    scope = parser.scope;

    ret = njs_variables_copy(vm, &scope->variables, vm->variables_hash);
    if (njs_slow_path(ret != NJS_OK)) {
        return ret;
    }

    njs_memzero(&generator, sizeof(njs_generator_t));
    generator.runtime = 1;

    code = njs_generate_scope(vm, &generator, scope, &njs_entry_anonymous);
    if (njs_slow_path(code == NULL)) {
        if (!njs_is_error(&vm->retval)) {
            njs_internal_error(vm, "njs_generate_scope() failed");
        }

        return NJS_ERROR;
    }

    njs_chb_destroy(&chain);

    lambda = ((njs_vmcode_function_t *) generator.code_start)->lambda;

    function = njs_function_alloc(vm, lambda, (njs_bool_t) async);
    if (njs_slow_path(function == NULL)) {
        return NJS_ERROR;
    }

    function->global = 1;
    function->global_this = 1;
    function->args_count = lambda->nargs - lambda->rest_parameters;

    njs_set_function(&vm->retval, function);

    return NJS_OK;

fail:

    njs_type_error(vm, "function constructor is disabled in \"safe\" mode");
    return NJS_ERROR;
}


static const njs_object_prop_t  njs_function_constructor_properties[] =
{
    {
        .type = NJS_PROPERTY,
        .name = njs_string("name"),
        .value = njs_string("Function"),
        .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),
    },
};


const njs_object_init_t  njs_function_constructor_init = {
    njs_function_constructor_properties,
    njs_nitems(njs_function_constructor_properties),
};


njs_int_t
njs_function_instance_length(njs_vm_t *vm, njs_object_prop_t *prop,
    njs_value_t *value, njs_value_t *setval, njs_value_t *retval)
{
    njs_object_t    *proto;
    njs_function_t  *function;

    proto = njs_object(value);

    do {
        if (njs_fast_path(proto->type == NJS_FUNCTION)) {
            break;
        }

        proto = proto->__proto__;
    } while (proto != NULL);

    if (njs_slow_path(proto == NULL)) {
        njs_internal_error(vm, "no function in proto chain");
        return NJS_ERROR;
    }

    function = (njs_function_t *) proto;

    njs_set_number(retval, function->args_count);

    return NJS_OK;
}


static njs_int_t
njs_function_prototype_call(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
    njs_index_t unused)
{
    njs_int_t           ret;
    njs_function_t      *function;
    const njs_value_t   *this;
    njs_native_frame_t  *frame;

    if (!njs_is_function(&args[0])) {
        njs_type_error(vm, "\"this\" argument is not a function");
        return NJS_ERROR;
    }

    if (nargs > 1) {
        this = &args[1];
        nargs -= 2;

    } else {
        this = (njs_value_t *) &njs_value_undefined;
        nargs = 0;
    }

    frame = vm->top_frame;

    /* Skip the "call" method frame. */
    frame->skip = 1;

    function = njs_function(&args[0]);

    ret = njs_function_frame(vm, function, this, &args[2], nargs, 0);
    if (njs_slow_path(ret != NJS_OK)) {
        return ret;
    }

    ret = njs_function_frame_invoke(vm, frame->retval);
    if (njs_slow_path(ret != NJS_OK)) {
        return ret;
    }

    return NJS_DECLINED;
}


static njs_int_t
njs_function_prototype_apply(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
    njs_index_t unused)
{
    int64_t         i, length;
    njs_int_t       ret;
    njs_frame_t     *frame;
    njs_value_t     *this, *arr_like;
    njs_array_t     *arr;
    njs_function_t  *func;

    if (!njs_is_function(njs_arg(args, nargs, 0))) {
        njs_type_error(vm, "\"this\" argument is not a function");
        return NJS_ERROR;
    }

    func = njs_function(njs_argument(args, 0));
    this = njs_arg(args, nargs, 1);
    arr_like = njs_arg(args, nargs, 2);

    if (njs_is_null_or_undefined(arr_like)) {
        length = 0;

        goto activate;

    } else if (njs_is_array(arr_like)) {
        arr = arr_like->data.u.array;

        args = arr->start;
        length = arr->length;

        goto activate;

    } else if (njs_slow_path(!njs_is_object(arr_like))) {
        njs_type_error(vm, "second argument is not an array-like object");
        return NJS_ERROR;
    }

    ret = njs_object_length(vm, arr_like, &length);
    if (njs_slow_path(ret != NJS_OK)) {
        return ret;
    }

    arr = njs_array_alloc(vm, 1, length, NJS_ARRAY_SPARE);
    if (njs_slow_path(arr == NULL)) {
        return NJS_ERROR;
    }

    args = arr->start;

    for (i = 0; i < length; i++) {
        ret = njs_value_property_i64(vm, arr_like, i, &args[i]);
        if (njs_slow_path(ret == NJS_ERROR)) {
            return ret;
        }
    }

activate:

    /* Skip the "apply" method frame. */
    vm->top_frame->skip = 1;

    frame = (njs_frame_t *) vm->top_frame;

    ret = njs_function_frame(vm, func, this, args, length, 0);
    if (njs_slow_path(ret != NJS_OK)) {
        return ret;
    }

    ret = njs_function_frame_invoke(vm, frame->native.retval);
    if (njs_slow_path(ret != NJS_OK)) {
        return ret;
    }

    return NJS_DECLINED;
}


static njs_int_t
njs_function_prototype_bind(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
    njs_index_t unused)
{
    size_t              size;
    njs_int_t           ret;
    njs_value_t         *values, name;
    njs_function_t      *function;
    njs_lvlhsh_query_t  lhq;

    if (!njs_is_function(&args[0])) {
        njs_type_error(vm, "\"this\" argument is not a function");
        return NJS_ERROR;
    }

    function = njs_mp_alloc(vm->mem_pool, sizeof(njs_function_t));
    if (njs_slow_path(function == NULL)) {
        njs_memory_error(vm);
        return NJS_ERROR;
    }

    *function = *njs_function(&args[0]);

    njs_lvlhsh_init(&function->object.hash);

    /* Bound functions have no "prototype" property. */
    function->object.shared_hash = vm->shared->arrow_instance_hash;

    function->object.__proto__ = &vm->prototypes[NJS_OBJ_TYPE_FUNCTION].object;
    function->object.shared = 0;

    function->u.bound_target = njs_function(&args[0]);

    njs_object_property_init(&lhq, &njs_string_name, NJS_NAME_HASH);

    ret = njs_object_property(vm, &args[0], &lhq, &name);
    if (njs_slow_path(ret == NJS_ERROR)) {
        return ret;
    }

    if (!njs_is_string(&name)) {
        name = njs_string_empty;
    }

    ret = njs_function_name_set(vm, function, &name, "bound");
    if (njs_slow_path(ret == NJS_ERROR)) {
        return ret;
    }

    if (nargs == 1) {
        args = njs_value_arg(&njs_value_undefined);

    } else {
        nargs--;
        args++;
    }

    if (nargs > function->args_count) {
        function->args_count = 0;

    } else {
        function->args_count -= nargs - 1;
    }

    function->args_offset = nargs;
    size = nargs * sizeof(njs_value_t);

    values = njs_mp_alloc(vm->mem_pool, size);
    if (njs_slow_path(values == NULL)) {
        njs_memory_error(vm);
        njs_mp_free(vm->mem_pool, function);
        return NJS_ERROR;
    }

    function->bound = values;

    /* GC: ? retain args. */

    memcpy(values, args, size);

    njs_set_function(&vm->retval, function);

    return NJS_OK;
}


static const njs_object_prop_t  njs_function_prototype_properties[] =
{
    {
        .type = NJS_PROPERTY,
        .name = njs_string("name"),
        .value = njs_string(""),
        .configurable = 1,
    },

    {
        .type = NJS_PROPERTY,
        .name = njs_string("length"),
        .value = njs_value(NJS_NUMBER, 0, 0.0),
        .configurable = 1,
    },

    {
        .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_string("call"),
        .value = njs_native_function(njs_function_prototype_call, 1),
        .writable = 1,
        .configurable = 1,
    },

    {
        .type = NJS_PROPERTY,
        .name = njs_string("apply"),
        .value = njs_native_function(njs_function_prototype_apply, 2),
        .writable = 1,
        .configurable = 1,
    },

    {
        .type = NJS_PROPERTY,
        .name = njs_string("bind"),
        .value = njs_native_function(njs_function_prototype_bind, 1),
        .writable = 1,
        .configurable = 1,
    },

    {
        .type = NJS_PROPERTY,
        .name = njs_string("caller"),
        .value = njs_value(NJS_INVALID, 1, NAN),
        .getter = njs_native_function(njs_function_prototype_thrower, 0),
        .setter = njs_native_function(njs_function_prototype_thrower, 0),
        .writable = NJS_ATTRIBUTE_UNSET,
        .configurable = 1,
    },

    {
        .type = NJS_PROPERTY,
        .name = njs_string("arguments"),
        .value = njs_value(NJS_INVALID, 1, NAN),
        .getter = njs_native_function(njs_function_prototype_thrower, 0),
        .setter = njs_native_function(njs_function_prototype_thrower, 0),
        .writable = NJS_ATTRIBUTE_UNSET,
        .configurable = 1,
    },
};


const njs_object_init_t  njs_function_prototype_init = {
    njs_function_prototype_properties,
    njs_nitems(njs_function_prototype_properties),
};


const njs_object_prop_t  njs_function_instance_properties[] =
{
    {
        .type = NJS_PROPERTY_HANDLER,
        .name = njs_string("length"),
        .value = njs_prop_handler(njs_function_instance_length),
        .configurable = 1,
    },

    {
        .type = NJS_PROPERTY_HANDLER,
        .name = njs_string("prototype"),
        .value = njs_prop_handler(njs_function_prototype_create),
        .writable = 1
    },
};


const njs_object_init_t  njs_function_instance_init = {
    njs_function_instance_properties,
    njs_nitems(njs_function_instance_properties),
};


const njs_object_prop_t  njs_arrow_instance_properties[] =
{
    {
        .type = NJS_PROPERTY_HANDLER,
        .name = njs_string("length"),
        .value = njs_prop_handler(njs_function_instance_length),
        .configurable = 1,
    },
};


const njs_object_init_t  njs_arrow_instance_init = {
    njs_arrow_instance_properties,
    njs_nitems(njs_arrow_instance_properties),
};


njs_int_t
njs_eval_function(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
    njs_index_t unused)
{
    njs_internal_error(vm, "Not implemented");

    return NJS_ERROR;
}


static njs_int_t
njs_prototype_function(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
    njs_index_t unused)
{
    njs_set_undefined(&vm->retval);

    return NJS_OK;
}


const njs_object_type_init_t  njs_function_type_init = {
   .constructor = njs_native_ctor(njs_function_constructor, 1, 0),
   .constructor_props = &njs_function_constructor_init,
   .prototype_props = &njs_function_prototype_init,
   .prototype_value = { .function = { .native = 1,
                                      .args_offset = 1,
                                      .u.native = njs_prototype_function,
                                      .object = { .type = NJS_FUNCTION } } },
};
