/* * Call handling. * * duk_handle_call_unprotected(): * * - Unprotected call to ECMAScript or Duktape/C function, from native * code or bytecode executor. * * - Also handles Ecma-to-Ecma calls which reuses a currently running * executor instance to avoid native recursion. Call setup is done * normally, but just before calling the bytecode executor a special * return code is used to indicate that a calling executor is reused. * * - Also handles tailcalls, i.e. reuse of current duk_activation. * * - Also handles setup for initial Duktape.Thread.resume(). * * duk_handle_safe_call(): * * - Protected C call within current activation. * * setjmp() and local variables have a nasty interaction, see execution.rst; * non-volatile locals modified after setjmp() call are not guaranteed to * keep their value and can cause compiler or compiler version specific * difficult to replicate issues. * * See 'execution.rst'. */ #include "duk_internal.h" /* XXX: heap->error_not_allowed for success path too? */ /* * Limit check helpers. */ /* Allow headroom for calls during error augmentation (see GH-191). * We allow space for 10 additional recursions, with one extra * for, e.g. a print() call at the deepest level, and an extra * +1 for protected call wrapping. */ #define DUK__AUGMENT_CALL_RELAX_COUNT (10 + 2) DUK_LOCAL DUK_NOINLINE void duk__call_c_recursion_limit_check_slowpath(duk_hthread *thr) { /* When augmenting an error, the effective limit is a bit higher. * Check for it only if the fast path check fails. */ #if defined(DUK_USE_AUGMENT_ERROR_THROW) || defined(DUK_USE_AUGMENT_ERROR_CREATE) if (thr->heap->augmenting_error) { if (thr->heap->call_recursion_depth < thr->heap->call_recursion_limit + DUK__AUGMENT_CALL_RELAX_COUNT) { DUK_D(DUK_DPRINT("C recursion limit reached but augmenting error and within relaxed limit")); return; } } #endif DUK_D(DUK_DPRINT("call prevented because C recursion limit reached")); DUK_ERROR_RANGE(thr, DUK_STR_C_CALLSTACK_LIMIT); DUK_WO_NORETURN(return;); } DUK_LOCAL DUK_ALWAYS_INLINE void duk__call_c_recursion_limit_check(duk_hthread *thr) { DUK_ASSERT(thr->heap->call_recursion_depth >= 0); DUK_ASSERT(thr->heap->call_recursion_depth <= thr->heap->call_recursion_limit); /* This check is forcibly inlined because it's very cheap and almost * always passes. The slow path is forcibly noinline. */ if (DUK_LIKELY(thr->heap->call_recursion_depth < thr->heap->call_recursion_limit)) { return; } duk__call_c_recursion_limit_check_slowpath(thr); } DUK_LOCAL DUK_NOINLINE void duk__call_callstack_limit_check_slowpath(duk_hthread *thr) { /* When augmenting an error, the effective limit is a bit higher. * Check for it only if the fast path check fails. */ #if defined(DUK_USE_AUGMENT_ERROR_THROW) || defined(DUK_USE_AUGMENT_ERROR_CREATE) if (thr->heap->augmenting_error) { if (thr->callstack_top < DUK_USE_CALLSTACK_LIMIT + DUK__AUGMENT_CALL_RELAX_COUNT) { DUK_D(DUK_DPRINT("call stack limit reached but augmenting error and within relaxed limit")); return; } } #endif /* XXX: error message is a bit misleading: we reached a recursion * limit which is also essentially the same as a C callstack limit * (except perhaps with some relaxed threading assumptions). */ DUK_D(DUK_DPRINT("call prevented because call stack limit reached")); DUK_ERROR_RANGE(thr, DUK_STR_CALLSTACK_LIMIT); DUK_WO_NORETURN(return;); } DUK_LOCAL DUK_ALWAYS_INLINE void duk__call_callstack_limit_check(duk_hthread *thr) { /* This check is forcibly inlined because it's very cheap and almost * always passes. The slow path is forcibly noinline. */ if (DUK_LIKELY(thr->callstack_top < DUK_USE_CALLSTACK_LIMIT)) { return; } duk__call_callstack_limit_check_slowpath(thr); } /* * Interrupt counter fixup (for development only). */ #if defined(DUK_USE_INTERRUPT_COUNTER) && defined(DUK_USE_DEBUG) DUK_LOCAL void duk__interrupt_fixup(duk_hthread *thr, duk_hthread *entry_curr_thread) { /* Currently the bytecode executor and executor interrupt * instruction counts are off because we don't execute the * interrupt handler when we're about to exit from the initial * user call into Duktape. * * If we were to execute the interrupt handler here, the counts * would match. You can enable this block manually to check * that this is the case. */ DUK_ASSERT(thr != NULL); DUK_ASSERT(thr->heap != NULL); #if defined(DUK_USE_INTERRUPT_DEBUG_FIXUP) if (entry_curr_thread == NULL) { thr->interrupt_init = thr->interrupt_init - thr->interrupt_counter; thr->heap->inst_count_interrupt += thr->interrupt_init; DUK_DD(DUK_DDPRINT("debug test: updated interrupt count on exit to " "user code, instruction counts: executor=%ld, interrupt=%ld", (long) thr->heap->inst_count_exec, (long) thr->heap->inst_count_interrupt)); DUK_ASSERT(thr->heap->inst_count_exec == thr->heap->inst_count_interrupt); } #else DUK_UNREF(thr); DUK_UNREF(entry_curr_thread); #endif } #endif /* * Arguments object creation. * * Creating arguments objects involves many small details, see E5 Section * 10.6 for the specific requirements. Much of the arguments object exotic * behavior is implemented in duk_hobject_props.c, and is enabled by the * object flag DUK_HOBJECT_FLAG_EXOTIC_ARGUMENTS. */ DUK_LOCAL void duk__create_arguments_object(duk_hthread *thr, duk_hobject *func, duk_hobject *varenv, duk_idx_t idx_args) { duk_hobject *arg; /* 'arguments' */ duk_hobject *formals; /* formals for 'func' (may be NULL if func is a C function) */ duk_idx_t i_arg; duk_idx_t i_map; duk_idx_t i_mappednames; duk_idx_t i_formals; duk_idx_t i_argbase; duk_idx_t n_formals; duk_idx_t idx; duk_idx_t num_stack_args; duk_bool_t need_map; DUK_ASSERT(thr != NULL); DUK_ASSERT(func != NULL); DUK_ASSERT(DUK_HOBJECT_IS_NONBOUND_FUNCTION(func)); DUK_ASSERT(varenv != NULL); /* [ ... func this arg1(@idx_args) ... argN envobj ] * [ arg1(@idx_args) ... argN envobj ] (for tailcalls) */ need_map = 0; i_argbase = idx_args; num_stack_args = duk_get_top(thr) - i_argbase - 1; DUK_ASSERT(i_argbase >= 0); DUK_ASSERT(num_stack_args >= 0); duk_push_hobject(thr, func); duk_get_prop_stridx_short(thr, -1, DUK_STRIDX_INT_FORMALS); formals = duk_get_hobject(thr, -1); if (formals) { n_formals = (duk_idx_t) duk_get_length(thr, -1); } else { /* This shouldn't happen without tampering of internal * properties: if a function accesses 'arguments', _Formals * is kept. Check for the case anyway in case internal * properties have been modified manually. */ DUK_D(DUK_DPRINT("_Formals is undefined when creating arguments, use n_formals == 0")); n_formals = 0; } duk_remove_m2(thr); /* leave formals on stack for later use */ i_formals = duk_require_top_index(thr); DUK_ASSERT(n_formals >= 0); DUK_ASSERT(formals != NULL || n_formals == 0); DUK_DDD(DUK_DDDPRINT("func=%!O, formals=%!O, n_formals=%ld", (duk_heaphdr *) func, (duk_heaphdr *) formals, (long) n_formals)); /* [ ... formals ] */ /* * Create required objects: * - 'arguments' object: array-like, but not an array * - 'map' object: internal object, tied to 'arguments' * - 'mappedNames' object: temporary value used during construction */ arg = duk_push_object_helper(thr, DUK_HOBJECT_FLAG_EXTENSIBLE | DUK_HOBJECT_FLAG_FASTREFS | DUK_HOBJECT_FLAG_ARRAY_PART | DUK_HOBJECT_CLASS_AS_FLAGS(DUK_HOBJECT_CLASS_ARGUMENTS), DUK_BIDX_OBJECT_PROTOTYPE); DUK_ASSERT(arg != NULL); (void) duk_push_object_helper(thr, DUK_HOBJECT_FLAG_EXTENSIBLE | DUK_HOBJECT_FLAG_FASTREFS | DUK_HOBJECT_CLASS_AS_FLAGS(DUK_HOBJECT_CLASS_OBJECT), -1); /* no prototype */ (void) duk_push_object_helper(thr, DUK_HOBJECT_FLAG_EXTENSIBLE | DUK_HOBJECT_FLAG_FASTREFS | DUK_HOBJECT_CLASS_AS_FLAGS(DUK_HOBJECT_CLASS_OBJECT), -1); /* no prototype */ i_arg = duk_get_top(thr) - 3; i_map = i_arg + 1; i_mappednames = i_arg + 2; /* [ ... formals arguments map mappedNames ] */ DUK_DDD(DUK_DDDPRINT("created arguments related objects: " "arguments at index %ld -> %!O " "map at index %ld -> %!O " "mappednames at index %ld -> %!O", (long) i_arg, (duk_heaphdr *) duk_get_hobject(thr, i_arg), (long) i_map, (duk_heaphdr *) duk_get_hobject(thr, i_map), (long) i_mappednames, (duk_heaphdr *) duk_get_hobject(thr, i_mappednames))); /* * Init arguments properties, map, etc. */ duk_push_int(thr, num_stack_args); duk_xdef_prop_stridx(thr, i_arg, DUK_STRIDX_LENGTH, DUK_PROPDESC_FLAGS_WC); /* * Init argument related properties. */ /* step 11 */ idx = num_stack_args - 1; while (idx >= 0) { DUK_DDD(DUK_DDDPRINT("arg idx %ld, argbase=%ld, argidx=%ld", (long) idx, (long) i_argbase, (long) (i_argbase + idx))); DUK_DDD(DUK_DDDPRINT("define arguments[%ld]=arg", (long) idx)); duk_dup(thr, i_argbase + idx); duk_xdef_prop_index_wec(thr, i_arg, (duk_uarridx_t) idx); DUK_DDD(DUK_DDDPRINT("defined arguments[%ld]=arg", (long) idx)); /* step 11.c is relevant only if non-strict (checked in 11.c.ii) */ if (!DUK_HOBJECT_HAS_STRICT(func) && idx < n_formals) { DUK_ASSERT(formals != NULL); DUK_DDD(DUK_DDDPRINT("strict function, index within formals (%ld < %ld)", (long) idx, (long) n_formals)); duk_get_prop_index(thr, i_formals, (duk_uarridx_t) idx); DUK_ASSERT(duk_is_string(thr, -1)); duk_dup_top(thr); /* [ ... name name ] */ if (!duk_has_prop(thr, i_mappednames)) { /* steps 11.c.ii.1 - 11.c.ii.4, but our internal book-keeping * differs from the reference model */ /* [ ... name ] */ need_map = 1; DUK_DDD(DUK_DDDPRINT("set mappednames[%s]=%ld", (const char *) duk_get_string(thr, -1), (long) idx)); duk_dup_top(thr); /* name */ (void) duk_push_uint_to_hstring(thr, (duk_uint_t) idx); /* index */ duk_xdef_prop_wec(thr, i_mappednames); /* out of spec, must be configurable */ DUK_DDD(DUK_DDDPRINT("set map[%ld]=%s", (long) idx, duk_get_string(thr, -1))); duk_dup_top(thr); /* name */ duk_xdef_prop_index_wec(thr, i_map, (duk_uarridx_t) idx); /* out of spec, must be configurable */ } else { /* duk_has_prop() popped the second 'name' */ } /* [ ... name ] */ duk_pop(thr); /* pop 'name' */ } idx--; } DUK_DDD(DUK_DDDPRINT("actual arguments processed")); /* step 12 */ if (need_map) { DUK_DDD(DUK_DDDPRINT("adding 'map' and 'varenv' to arguments object")); /* should never happen for a strict callee */ DUK_ASSERT(!DUK_HOBJECT_HAS_STRICT(func)); duk_dup(thr, i_map); duk_xdef_prop_stridx(thr, i_arg, DUK_STRIDX_INT_MAP, DUK_PROPDESC_FLAGS_NONE); /* out of spec, don't care */ /* The variable environment for magic variable bindings needs to be * given by the caller and recorded in the arguments object. * * See E5 Section 10.6, the creation of setters/getters. * * The variable environment also provides access to the callee, so * an explicit (internal) callee property is not needed. */ duk_push_hobject(thr, varenv); duk_xdef_prop_stridx(thr, i_arg, DUK_STRIDX_INT_VARENV, DUK_PROPDESC_FLAGS_NONE); /* out of spec, don't care */ } /* steps 13-14 */ if (DUK_HOBJECT_HAS_STRICT(func)) { /* Callee/caller are throwers and are not deletable etc. They * could be implemented as virtual properties, but currently * there is no support for virtual properties which are accessors * (only plain virtual properties). This would not be difficult * to change in duk_hobject_props, but we can make the throwers * normal, concrete properties just as easily. * * Note that the specification requires that the *same* thrower * built-in object is used here! See E5 Section 10.6 main * algoritm, step 14, and Section 13.2.3 which describes the * thrower. See test case test-arguments-throwers.js. */ DUK_DDD(DUK_DDDPRINT("strict function, setting caller/callee to throwers")); duk_xdef_prop_stridx_thrower(thr, i_arg, DUK_STRIDX_CALLER); duk_xdef_prop_stridx_thrower(thr, i_arg, DUK_STRIDX_CALLEE); } else { DUK_DDD(DUK_DDDPRINT("non-strict function, setting callee to actual value")); duk_push_hobject(thr, func); duk_xdef_prop_stridx(thr, i_arg, DUK_STRIDX_CALLEE, DUK_PROPDESC_FLAGS_WC); } /* set exotic behavior only after we're done */ if (need_map) { /* Exotic behaviors are only enabled for arguments objects * which have a parameter map (see E5 Section 10.6 main * algorithm, step 12). * * In particular, a non-strict arguments object with no * mapped formals does *NOT* get exotic behavior, even * for e.g. "caller" property. This seems counterintuitive * but seems to be the case. */ /* cannot be strict (never mapped variables) */ DUK_ASSERT(!DUK_HOBJECT_HAS_STRICT(func)); DUK_DDD(DUK_DDDPRINT("enabling exotic behavior for arguments object")); DUK_HOBJECT_SET_EXOTIC_ARGUMENTS(arg); } else { DUK_DDD(DUK_DDDPRINT("not enabling exotic behavior for arguments object")); } DUK_DDD(DUK_DDDPRINT("final arguments related objects: " "arguments at index %ld -> %!O " "map at index %ld -> %!O " "mappednames at index %ld -> %!O", (long) i_arg, (duk_heaphdr *) duk_get_hobject(thr, i_arg), (long) i_map, (duk_heaphdr *) duk_get_hobject(thr, i_map), (long) i_mappednames, (duk_heaphdr *) duk_get_hobject(thr, i_mappednames))); /* [ args(n) envobj formals arguments map mappednames ] */ duk_pop_2(thr); duk_remove_m2(thr); /* [ args(n) envobj arguments ] */ } /* Helper for creating the arguments object and adding it to the env record * on top of the value stack. */ DUK_LOCAL void duk__handle_createargs_for_call(duk_hthread *thr, duk_hobject *func, duk_hobject *env, duk_idx_t idx_args) { DUK_DDD(DUK_DDDPRINT("creating arguments object for function call")); DUK_ASSERT(thr != NULL); DUK_ASSERT(func != NULL); DUK_ASSERT(env != NULL); DUK_ASSERT(DUK_HOBJECT_HAS_CREATEARGS(func)); /* [ ... arg1 ... argN envobj ] */ duk__create_arguments_object(thr, func, env, idx_args); /* [ ... arg1 ... argN envobj argobj ] */ duk_xdef_prop_stridx_short(thr, -2, DUK_STRIDX_LC_ARGUMENTS, DUK_HOBJECT_HAS_STRICT(func) ? DUK_PROPDESC_FLAGS_E : /* strict: non-deletable, non-writable */ DUK_PROPDESC_FLAGS_WE); /* non-strict: non-deletable, writable */ /* [ ... arg1 ... argN envobj ] */ } /* * Helpers for constructor call handling. * * There are two [[Construct]] operations in the specification: * * - E5 Section 13.2.2: for Function objects * - E5 Section 15.3.4.5.2: for "bound" Function objects * * The chain of bound functions is resolved in Section 15.3.4.5.2, * with arguments "piling up" until the [[Construct]] internal * method is called on the final, actual Function object. Note * that the "prototype" property is looked up *only* from the * final object, *before* calling the constructor. * * Since Duktape 2.2 bound functions are represented with the * duk_hboundfunc internal type, and bound function chains are * collapsed when a bound function is created. As a result, the * direct target of a duk_hboundfunc is always non-bound and the * this/argument lists have been resolved. * * When constructing new Array instances, an unnecessary object is * created and discarded now: the standard [[Construct]] creates an * object, and calls the Array constructor. The Array constructor * returns an Array instance, which is used as the result value for * the "new" operation; the object created before the Array constructor * call is discarded. * * This would be easy to fix, e.g. by knowing that the Array constructor * will always create a replacement object and skip creating the fallback * object in that case. */ /* Update default instance prototype for constructor call. */ DUK_LOCAL void duk__update_default_instance_proto(duk_hthread *thr, duk_idx_t idx_func) { duk_hobject *proto; duk_hobject *fallback; DUK_ASSERT(duk_is_constructable(thr, idx_func)); duk_get_prop_stridx_short(thr, idx_func, DUK_STRIDX_PROTOTYPE); proto = duk_get_hobject(thr, -1); if (proto == NULL) { DUK_DDD(DUK_DDDPRINT("constructor has no 'prototype' property, or value not an object " "-> leave standard Object prototype as fallback prototype")); } else { DUK_DDD(DUK_DDDPRINT("constructor has 'prototype' property with object value " "-> set fallback prototype to that value: %!iO", (duk_heaphdr *) proto)); /* Original fallback (default instance) is untouched when * resolving bound functions etc. */ fallback = duk_known_hobject(thr, idx_func + 1); DUK_ASSERT(fallback != NULL); DUK_HOBJECT_SET_PROTOTYPE_UPDREF(thr, fallback, proto); } duk_pop(thr); } /* Postprocess: return value special handling, error augmentation. */ DUK_INTERNAL void duk_call_construct_postprocess(duk_hthread *thr, duk_small_uint_t proxy_invariant) { /* Use either fallback (default instance) or retval depending * on retval type. Needs to be called before unwind because * the default instance is read from the current (immutable) * 'this' binding. * * For Proxy 'construct' calls the return value must be an * Object (we accept object-like values like buffers and * lightfuncs too). If not, TypeError. */ if (duk_check_type_mask(thr, -1, DUK_TYPE_MASK_OBJECT | DUK_TYPE_MASK_BUFFER | DUK_TYPE_MASK_LIGHTFUNC)) { DUK_DDD(DUK_DDDPRINT("replacement value")); } else { if (DUK_UNLIKELY(proxy_invariant != 0U)) { /* Proxy 'construct' return value invariant violated. */ DUK_ERROR_TYPE_INVALID_TRAP_RESULT(thr); DUK_WO_NORETURN(return;); } /* XXX: direct value stack access */ duk_pop(thr); duk_push_this(thr); } #if defined(DUK_USE_AUGMENT_ERROR_CREATE) /* Augment created errors upon creation, not when they are thrown or * rethrown. __FILE__ and __LINE__ are not desirable here; the call * stack reflects the caller which is correct. Skip topmost, unwound * activation when creating a traceback. If thr->ptr_curr_pc was != * NULL we'd need to sync the current PC so that the traceback comes * out right; however it is always synced here so just assert for it. */ DUK_ASSERT(thr->ptr_curr_pc == NULL); duk_err_augment_error_create(thr, thr, NULL, 0, DUK_AUGMENT_FLAG_NOBLAME_FILELINE | DUK_AUGMENT_FLAG_SKIP_ONE); #endif } /* * Helper for handling a bound function when a call is being made. * * Assumes that bound function chains have been "collapsed" so that either * the target is non-bound or there is one bound function that points to a * nonbound target. * * Prepends the bound arguments to the value stack (at idx_func + 2). * The 'this' binding is also updated if necessary (at idx_func + 1). * Note that for constructor calls the 'this' binding is never updated by * [[BoundThis]]. */ DUK_LOCAL void duk__handle_bound_chain_for_call(duk_hthread *thr, duk_idx_t idx_func, duk_bool_t is_constructor_call) { duk_tval *tv_func; duk_hobject *func; duk_idx_t len; DUK_ASSERT(thr != NULL); /* On entry, item at idx_func is a bound, non-lightweight function, * but we don't rely on that below. */ DUK_ASSERT(duk_get_top(thr) >= idx_func + 2); tv_func = duk_require_tval(thr, idx_func); DUK_ASSERT(tv_func != NULL); if (DUK_TVAL_IS_OBJECT(tv_func)) { func = DUK_TVAL_GET_OBJECT(tv_func); /* XXX: separate helper function, out of fast path? */ if (DUK_HOBJECT_HAS_BOUNDFUNC(func)) { duk_hboundfunc *h_bound; duk_tval *tv_args; duk_tval *tv_gap; h_bound = (duk_hboundfunc *) (void *) func; tv_args = h_bound->args; len = h_bound->nargs; DUK_ASSERT(len == 0 || tv_args != NULL); DUK_DDD(DUK_DDDPRINT("bound function encountered, ptr=%p: %!T", (void *) DUK_TVAL_GET_OBJECT(tv_func), tv_func)); /* [ ... func this arg1 ... argN ] */ if (is_constructor_call) { /* See: tests/ecmascript/test-spec-bound-constructor.js */ DUK_DDD(DUK_DDDPRINT("constructor call: don't update this binding")); } else { /* XXX: duk_replace_tval */ duk_push_tval(thr, &h_bound->this_binding); duk_replace(thr, idx_func + 1); /* idx_this = idx_func + 1 */ } /* [ ... func this arg1 ... argN ] */ duk_require_stack(thr, len); tv_gap = duk_reserve_gap(thr, idx_func + 2, len); duk_copy_tvals_incref(thr, tv_gap, tv_args, (duk_size_t) len); /* [ ... func this arg1 ... argN ] */ duk_push_tval(thr, &h_bound->target); duk_replace(thr, idx_func); /* replace in stack */ DUK_DDD(DUK_DDDPRINT("bound function handled, idx_func=%ld, curr func=%!T", (long) idx_func, duk_get_tval(thr, idx_func))); } } else if (DUK_TVAL_IS_LIGHTFUNC(tv_func)) { /* Lightweight function: never bound, so terminate. */ ; } else { /* Shouldn't happen, so ugly error is enough. */ DUK_ERROR_INTERNAL(thr); DUK_WO_NORETURN(return;); } DUK_ASSERT(duk_get_top(thr) >= idx_func + 2); DUK_DDD(DUK_DDDPRINT("final non-bound function is: %!T", duk_get_tval(thr, idx_func))); #if defined(DUK_USE_ASSERTIONS) tv_func = duk_require_tval(thr, idx_func); DUK_ASSERT(DUK_TVAL_IS_LIGHTFUNC(tv_func) || DUK_TVAL_IS_OBJECT(tv_func)); if (DUK_TVAL_IS_OBJECT(tv_func)) { func = DUK_TVAL_GET_OBJECT(tv_func); DUK_ASSERT(func != NULL); DUK_ASSERT(!DUK_HOBJECT_HAS_BOUNDFUNC(func)); DUK_ASSERT(DUK_HOBJECT_HAS_COMPFUNC(func) || DUK_HOBJECT_HAS_NATFUNC(func)); } #endif } /* * Helper for inline handling of .call(), .apply(), and .construct(). */ DUK_LOCAL duk_bool_t duk__handle_specialfuncs_for_call(duk_hthread *thr, duk_idx_t idx_func, duk_hobject *func, duk_small_uint_t *call_flags, duk_bool_t first) { #if defined(DUK_USE_ASSERTIONS) duk_c_function natfunc; #endif duk_tval *tv_args; DUK_ASSERT(func != NULL); DUK_ASSERT((*call_flags & DUK_CALL_FLAG_CONSTRUCT) == 0); /* Caller. */ #if defined(DUK_USE_ASSERTIONS) natfunc = ((duk_hnatfunc *) func)->func; DUK_ASSERT(natfunc != NULL); #endif /* On every round of function resolution at least target function and * 'this' binding are set. We can assume that here, and must guarantee * it on exit. Value stack reserve is extended for bound function and * .apply() unpacking so we don't need to extend it here when we need a * few slots. */ DUK_ASSERT(duk_get_top(thr) >= idx_func + 2); /* Handle native 'eval' specially. A direct eval check is only made * for the first resolution attempt; e.g. a bound eval call is -not- * a direct eval call. */ if (DUK_UNLIKELY(((duk_hnatfunc *) func)->magic == 15)) { /* For now no special handling except for direct eval * detection. */ DUK_ASSERT(((duk_hnatfunc *) func)->func == duk_bi_global_object_eval); if (first && (*call_flags & DUK_CALL_FLAG_CALLED_AS_EVAL)) { *call_flags = (*call_flags & ~DUK_CALL_FLAG_CALLED_AS_EVAL) | DUK_CALL_FLAG_DIRECT_EVAL; } DUK_ASSERT(duk_get_top(thr) >= idx_func + 2); return 1; /* stop resolving */ } /* Handle special functions based on the DUK_HOBJECT_FLAG_SPECIAL_CALL * flag; their magic value is used for switch-case. * * NOTE: duk_unpack_array_like() reserves value stack space * for the result values (unlike most other value stack calls). */ switch (((duk_hnatfunc *) func)->magic) { case 0: { /* 0=Function.prototype.call() */ /* Value stack: * idx_func + 0: Function.prototype.call() [removed] * idx_func + 1: this binding for .call (target function) * idx_func + 2: 1st argument to .call, desired 'this' binding * idx_func + 3: 2nd argument to .call, desired 1st argument for ultimate target * ... * * Remove idx_func + 0 to get: * idx_func + 0: target function * idx_func + 1: this binding * idx_func + 2: call arguments * ... */ DUK_ASSERT(natfunc == duk_bi_function_prototype_call); duk_remove_unsafe(thr, idx_func); tv_args = thr->valstack_bottom + idx_func + 2; if (thr->valstack_top < tv_args) { DUK_ASSERT(tv_args <= thr->valstack_end); thr->valstack_top = tv_args; /* at least target function and 'this' binding present */ } break; } case 1: { /* 1=Function.prototype.apply() */ /* Value stack: * idx_func + 0: Function.prototype.apply() [removed] * idx_func + 1: this binding for .apply (target function) * idx_func + 2: 1st argument to .apply, desired 'this' binding * idx_func + 3: 2nd argument to .apply, argArray * [anything after this MUST be ignored] * * Remove idx_func + 0 and unpack the argArray to get: * idx_func + 0: target function * idx_func + 1: this binding * idx_func + 2: call arguments * ... */ DUK_ASSERT(natfunc == duk_bi_function_prototype_apply); duk_remove_unsafe(thr, idx_func); goto apply_shared; } #if defined(DUK_USE_REFLECT_BUILTIN) case 2: { /* 2=Reflect.apply() */ /* Value stack: * idx_func + 0: Reflect.apply() [removed] * idx_func + 1: this binding for .apply (ignored, usually Reflect) [removed] * idx_func + 2: 1st argument to .apply, target function * idx_func + 3: 2nd argument to .apply, desired 'this' binding * idx_func + 4: 3rd argument to .apply, argArray * [anything after this MUST be ignored] * * Remove idx_func + 0 and idx_func + 1, and unpack the argArray to get: * idx_func + 0: target function * idx_func + 1: this binding * idx_func + 2: call arguments * ... */ DUK_ASSERT(natfunc == duk_bi_reflect_apply); duk_remove_n_unsafe(thr, idx_func, 2); goto apply_shared; } case 3: { /* 3=Reflect.construct() */ /* Value stack: * idx_func + 0: Reflect.construct() [removed] * idx_func + 1: this binding for .construct (ignored, usually Reflect) [removed] * idx_func + 2: 1st argument to .construct, target function * idx_func + 3: 2nd argument to .construct, argArray * idx_func + 4: 3rd argument to .construct, newTarget * [anything after this MUST be ignored] * * Remove idx_func + 0 and idx_func + 1, unpack the argArray, * and insert default instance (prototype not yet updated), to get: * idx_func + 0: target function * idx_func + 1: this binding (default instance) * idx_func + 2: constructor call arguments * ... * * Call flags must be updated to reflect the fact that we're * now dealing with a constructor call, and e.g. the 'this' * binding cannot be overwritten if the target is bound. * * newTarget is checked but not yet passed onwards. */ duk_idx_t top; DUK_ASSERT(natfunc == duk_bi_reflect_construct); *call_flags |= DUK_CALL_FLAG_CONSTRUCT; duk_remove_n_unsafe(thr, idx_func, 2); top = duk_get_top(thr); if (!duk_is_constructable(thr, idx_func)) { /* Target constructability must be checked before * unpacking argArray (which may cause side effects). * Just return; caller will throw the error. */ duk_set_top_unsafe(thr, idx_func + 2); /* satisfy asserts */ break; } duk_push_object(thr); duk_insert(thr, idx_func + 1); /* default instance */ /* [ ... func default_instance argArray newTarget? ] */ top = duk_get_top(thr); if (top < idx_func + 3) { /* argArray is a mandatory argument for Reflect.construct(). */ DUK_ERROR_TYPE_INVALID_ARGS(thr); DUK_WO_NORETURN(return 0;); } if (top > idx_func + 3) { if (!duk_strict_equals(thr, idx_func, idx_func + 3)) { /* XXX: [[Construct]] newTarget currently unsupported */ DUK_ERROR_UNSUPPORTED(thr); DUK_WO_NORETURN(return 0;); } duk_set_top_unsafe(thr, idx_func + 3); /* remove any args beyond argArray */ } DUK_ASSERT(duk_get_top(thr) == idx_func + 3); DUK_ASSERT(duk_is_valid_index(thr, idx_func + 2)); (void) duk_unpack_array_like(thr, idx_func + 2); /* XXX: should also remove target to be symmetric with duk_pack()? */ duk_remove(thr, idx_func + 2); DUK_ASSERT(duk_get_top(thr) >= idx_func + 2); break; } #endif /* DUK_USE_REFLECT_BUILTIN */ default: { DUK_ASSERT(0); DUK_UNREACHABLE(); } } DUK_ASSERT(duk_get_top(thr) >= idx_func + 2); return 0; /* keep resolving */ apply_shared: tv_args = thr->valstack_bottom + idx_func + 2; if (thr->valstack_top <= tv_args) { DUK_ASSERT(tv_args <= thr->valstack_end); thr->valstack_top = tv_args; /* at least target func and 'this' binding present */ /* No need to check for argArray. */ } else { DUK_ASSERT(duk_get_top(thr) >= idx_func + 3); /* idx_func + 2 covered above */ if (thr->valstack_top > tv_args + 1) { duk_set_top_unsafe(thr, idx_func + 3); /* remove any args beyond argArray */ } DUK_ASSERT(duk_is_valid_index(thr, idx_func + 2)); if (!duk_is_callable(thr, idx_func)) { /* Avoid unpack side effects if the target isn't callable. * Calling code will throw the actual error. */ } else { (void) duk_unpack_array_like(thr, idx_func + 2); duk_remove(thr, idx_func + 2); } } DUK_ASSERT(duk_get_top(thr) >= idx_func + 2); return 0; /* keep resolving */ } /* * Helper for Proxy handling. */ #if defined(DUK_USE_ES6_PROXY) DUK_LOCAL void duk__handle_proxy_for_call(duk_hthread *thr, duk_idx_t idx_func, duk_hproxy *h_proxy, duk_small_uint_t *call_flags) { duk_bool_t rc; /* Value stack: * idx_func + 0: Proxy object * idx_func + 1: this binding for call * idx_func + 2: 1st argument for call * idx_func + 3: 2nd argument for call * ... * * If Proxy doesn't have a trap for the call ('apply' or 'construct'), * replace Proxy object with target object. * * If we're dealing with a normal call and the Proxy has an 'apply' * trap, manipulate value stack to: * * idx_func + 0: trap * idx_func + 1: Proxy's handler * idx_func + 2: Proxy's target * idx_func + 3: this binding for call (from idx_func + 1) * idx_func + 4: call arguments packed to an array * * If we're dealing with a constructor call and the Proxy has a * 'construct' trap, manipulate value stack to: * * idx_func + 0: trap * idx_func + 1: Proxy's handler * idx_func + 2: Proxy's target * idx_func + 3: call arguments packed to an array * idx_func + 4: newTarget == Proxy object here * * As we don't yet have proper newTarget support, the newTarget at * idx_func + 3 is just the original constructor being called, i.e. * the Proxy object (not the target). Note that the default instance * (original 'this' binding) is dropped and ignored. */ duk_push_hobject(thr, h_proxy->handler); rc = duk_get_prop_stridx_short(thr, -1, (*call_flags & DUK_CALL_FLAG_CONSTRUCT) ? DUK_STRIDX_CONSTRUCT : DUK_STRIDX_APPLY); if (rc == 0) { /* Not found, continue to target. If this is a construct * call, update default instance prototype using the Proxy, * not the target. */ if (*call_flags & DUK_CALL_FLAG_CONSTRUCT) { if (!(*call_flags & DUK_CALL_FLAG_DEFAULT_INSTANCE_UPDATED)) { *call_flags |= DUK_CALL_FLAG_DEFAULT_INSTANCE_UPDATED; duk__update_default_instance_proto(thr, idx_func); } } duk_pop_2(thr); duk_push_hobject(thr, h_proxy->target); duk_replace(thr, idx_func); return; } /* Here we must be careful not to replace idx_func while * h_proxy is still needed, otherwise h_proxy may become * dangling. This could be improved e.g. using a * duk_pack_slice() with a freeform slice. */ /* Here: * idx_func + 0: Proxy object * idx_func + 1: this binding for call * idx_func + 2: 1st argument for call * idx_func + 3: 2nd argument for call * ... * idx_func + N: handler * idx_func + N + 1: trap */ duk_insert(thr, idx_func + 1); duk_insert(thr, idx_func + 2); duk_push_hobject(thr, h_proxy->target); duk_insert(thr, idx_func + 3); duk_pack(thr, duk_get_top(thr) - (idx_func + 5)); /* Here: * idx_func + 0: Proxy object * idx_func + 1: trap * idx_func + 2: Proxy's handler * idx_func + 3: Proxy's target * idx_func + 4: this binding for call * idx_func + 5: arguments array */ DUK_ASSERT(duk_get_top(thr) == idx_func + 6); if (*call_flags & DUK_CALL_FLAG_CONSTRUCT) { *call_flags |= DUK_CALL_FLAG_CONSTRUCT_PROXY; /* Enable 'construct' trap return invariant check. */ *call_flags &= ~(DUK_CALL_FLAG_CONSTRUCT); /* Resume as non-constructor call to the trap. */ /* 'apply' args: target, thisArg, argArray * 'construct' args: target, argArray, newTarget */ duk_remove(thr, idx_func + 4); duk_push_hobject(thr, (duk_hobject *) h_proxy); } /* Finalize value stack layout by removing Proxy reference. */ duk_remove(thr, idx_func); h_proxy = NULL; /* invalidated */ DUK_ASSERT(duk_get_top(thr) == idx_func + 5); } #endif /* DUK_USE_ES6_PROXY */ /* * Helper for setting up var_env and lex_env of an activation, * assuming it does NOT have the DUK_HOBJECT_FLAG_NEWENV flag. */ DUK_LOCAL void duk__handle_oldenv_for_call(duk_hthread *thr, duk_hobject *func, duk_activation *act) { duk_hcompfunc *f; duk_hobject *h_lex; duk_hobject *h_var; DUK_ASSERT(thr != NULL); DUK_ASSERT(func != NULL); DUK_ASSERT(act != NULL); DUK_ASSERT(!DUK_HOBJECT_HAS_NEWENV(func)); DUK_ASSERT(!DUK_HOBJECT_HAS_CREATEARGS(func)); DUK_ASSERT(DUK_HOBJECT_IS_COMPFUNC(func)); DUK_UNREF(thr); f = (duk_hcompfunc *) func; h_lex = DUK_HCOMPFUNC_GET_LEXENV(thr->heap, f); h_var = DUK_HCOMPFUNC_GET_VARENV(thr->heap, f); DUK_ASSERT(h_lex != NULL); /* Always true for closures (not for templates) */ DUK_ASSERT(h_var != NULL); act->lex_env = h_lex; act->var_env = h_var; DUK_HOBJECT_INCREF(thr, h_lex); DUK_HOBJECT_INCREF(thr, h_var); } /* * Helper for updating callee 'caller' property. */ #if defined(DUK_USE_NONSTD_FUNC_CALLER_PROPERTY) DUK_LOCAL void duk__update_func_caller_prop(duk_hthread *thr, duk_hobject *func) { duk_tval *tv_caller; duk_hobject *h_tmp; duk_activation *act_callee; duk_activation *act_caller; DUK_ASSERT(thr != NULL); DUK_ASSERT(func != NULL); DUK_ASSERT(!DUK_HOBJECT_HAS_BOUNDFUNC(func)); /* bound chain resolved */ DUK_ASSERT(thr->callstack_top >= 1); if (DUK_HOBJECT_HAS_STRICT(func)) { /* Strict functions don't get their 'caller' updated. */ return; } DUK_ASSERT(thr->callstack_top > 0); act_callee = thr->callstack_curr; DUK_ASSERT(act_callee != NULL); act_caller = (thr->callstack_top >= 2 ? act_callee->parent : NULL); /* XXX: check .caller writability? */ /* Backup 'caller' property and update its value. */ tv_caller = duk_hobject_find_existing_entry_tval_ptr(thr->heap, func, DUK_HTHREAD_STRING_CALLER(thr)); if (tv_caller) { /* If caller is global/eval code, 'caller' should be set to * 'null'. * * XXX: there is no exotic flag to infer this correctly now. * The NEWENV flag is used now which works as intended for * everything (global code, non-strict eval code, and functions) * except strict eval code. Bound functions are never an issue * because 'func' has been resolved to a non-bound function. */ if (act_caller != NULL) { /* act_caller->func may be NULL in some finalization cases, * just treat like we don't know the caller. */ if (act_caller->func && !DUK_HOBJECT_HAS_NEWENV(act_caller->func)) { /* Setting to NULL causes 'caller' to be set to * 'null' as desired. */ act_caller = NULL; } } if (DUK_TVAL_IS_OBJECT(tv_caller)) { h_tmp = DUK_TVAL_GET_OBJECT(tv_caller); DUK_ASSERT(h_tmp != NULL); act_callee->prev_caller = h_tmp; /* Previous value doesn't need refcount changes because its ownership * is transferred to prev_caller. */ if (act_caller != NULL) { DUK_ASSERT(act_caller->func != NULL); DUK_TVAL_SET_OBJECT(tv_caller, act_caller->func); DUK_TVAL_INCREF(thr, tv_caller); } else { DUK_TVAL_SET_NULL(tv_caller); /* no incref */ } } else { /* 'caller' must only take on 'null' or function value */ DUK_ASSERT(!DUK_TVAL_IS_HEAP_ALLOCATED(tv_caller)); DUK_ASSERT(act_callee->prev_caller == NULL); if (act_caller != NULL && act_caller->func) { /* Tolerate act_caller->func == NULL which happens in * some finalization cases; treat like unknown caller. */ DUK_TVAL_SET_OBJECT(tv_caller, act_caller->func); DUK_TVAL_INCREF(thr, tv_caller); } else { DUK_TVAL_SET_NULL(tv_caller); /* no incref */ } } } } #endif /* DUK_USE_NONSTD_FUNC_CALLER_PROPERTY */ /* * Shared helpers for resolving the final, non-bound target function of the * call and the effective 'this' binding. Resolves bound functions and * applies .call(), .apply(), and .construct() inline. * * Proxy traps are also handled inline so that if the target is a Proxy with * a 'call' or 'construct' trap, the trap handler is called with a modified * argument list. * * Once the bound function / .call() / .apply() / .construct() sequence has * been resolved, the value at idx_func + 1 may need coercion described in * E5 Section 10.4.3. * * A call that begins as a non-constructor call may be converted into a * constructor call during the resolution process if Reflect.construct() * is invoked. This is handled by updating the caller's call_flags. * * For global and eval code (E5 Sections 10.4.1 and 10.4.2), we assume * that the caller has provided the correct 'this' binding explicitly * when calling, i.e.: * * - global code: this=global object * - direct eval: this=copy from eval() caller's this binding * - other eval: this=global object * * The 'this' coercion may cause a recursive function call with arbitrary * side effects, because ToObject() may be called. */ DUK_LOCAL DUK_INLINE void duk__coerce_nonstrict_this_binding(duk_hthread *thr, duk_idx_t idx_this) { duk_tval *tv_this; duk_hobject *obj_global; tv_this = thr->valstack_bottom + idx_this; switch (DUK_TVAL_GET_TAG(tv_this)) { case DUK_TAG_OBJECT: DUK_DDD(DUK_DDDPRINT("this binding: non-strict, object -> use directly")); break; case DUK_TAG_UNDEFINED: case DUK_TAG_NULL: DUK_DDD(DUK_DDDPRINT("this binding: non-strict, undefined/null -> use global object")); obj_global = thr->builtins[DUK_BIDX_GLOBAL]; /* XXX: avoid this check somehow */ if (DUK_LIKELY(obj_global != NULL)) { DUK_ASSERT(!DUK_TVAL_IS_HEAP_ALLOCATED(tv_this)); /* no need to decref previous value */ DUK_TVAL_SET_OBJECT(tv_this, obj_global); DUK_HOBJECT_INCREF(thr, obj_global); } else { /* This may only happen if built-ins are being "torn down". * This behavior is out of specification scope. */ DUK_D(DUK_DPRINT("this binding: wanted to use global object, but it is NULL -> using undefined instead")); DUK_ASSERT(!DUK_TVAL_IS_HEAP_ALLOCATED(tv_this)); /* no need to decref previous value */ DUK_TVAL_SET_UNDEFINED(tv_this); /* nothing to incref */ } break; default: /* Plain buffers and lightfuncs are object coerced. Lightfuncs * very rarely come here however, because the call target would * need to be a non-strict non-lightfunc (lightfuncs are considered * strict) with an explicit lightfunc 'this' binding. */ DUK_ASSERT(!DUK_TVAL_IS_UNUSED(tv_this)); DUK_DDD(DUK_DDDPRINT("this binding: non-strict, not object/undefined/null -> use ToObject(value)")); duk_to_object(thr, idx_this); /* may have side effects */ break; } } DUK_LOCAL DUK_ALWAYS_INLINE duk_bool_t duk__resolve_target_fastpath_check(duk_hthread *thr, duk_idx_t idx_func, duk_hobject **out_func, duk_small_uint_t call_flags) { #if defined(DUK_USE_PREFER_SIZE) DUK_UNREF(thr); DUK_UNREF(idx_func); DUK_UNREF(out_func); DUK_UNREF(call_flags); #else /* DUK_USE_PREFER_SIZE */ duk_tval *tv_func; duk_hobject *func; if (DUK_UNLIKELY(call_flags & DUK_CALL_FLAG_CONSTRUCT)) { return 0; } tv_func = DUK_GET_TVAL_POSIDX(thr, idx_func); DUK_ASSERT(tv_func != NULL); if (DUK_LIKELY(DUK_TVAL_IS_OBJECT(tv_func))) { func = DUK_TVAL_GET_OBJECT(tv_func); if (DUK_HOBJECT_IS_CALLABLE(func) && !DUK_HOBJECT_HAS_BOUNDFUNC(func) && !DUK_HOBJECT_HAS_SPECIAL_CALL(func)) { *out_func = func; if (DUK_HOBJECT_HAS_STRICT(func)) { /* Strict function: no 'this' coercion. */ return 1; } duk__coerce_nonstrict_this_binding(thr, idx_func + 1); return 1; } } else if (DUK_TVAL_IS_LIGHTFUNC(tv_func)) { *out_func = NULL; /* Lightfuncs are considered strict, so 'this' binding is * used as is. They're never bound, always constructable, * and never special functions. */ return 1; } #endif /* DUK_USE_PREFER_SIZE */ return 0; /* let slow path deal with it */ } DUK_LOCAL duk_hobject *duk__resolve_target_func_and_this_binding(duk_hthread *thr, duk_idx_t idx_func, duk_small_uint_t *call_flags) { duk_tval *tv_func; duk_hobject *func; duk_bool_t first; DUK_ASSERT(duk_get_top(thr) >= idx_func + 2); for (first = 1;; first = 0) { DUK_ASSERT(duk_get_top(thr) >= idx_func + 2); tv_func = DUK_GET_TVAL_POSIDX(thr, idx_func); DUK_ASSERT(tv_func != NULL); if (DUK_TVAL_IS_OBJECT(tv_func)) { func = DUK_TVAL_GET_OBJECT(tv_func); if (*call_flags & DUK_CALL_FLAG_CONSTRUCT) { if (DUK_UNLIKELY(!DUK_HOBJECT_HAS_CONSTRUCTABLE(func))) { goto not_constructable; } } else { if (DUK_UNLIKELY(!DUK_HOBJECT_IS_CALLABLE(func))) { goto not_callable; } } if (DUK_LIKELY(!DUK_HOBJECT_HAS_BOUNDFUNC(func) && !DUK_HOBJECT_HAS_SPECIAL_CALL(func) && !DUK_HOBJECT_HAS_EXOTIC_PROXYOBJ(func))) { /* Common case, so test for using a single bitfield test. * Break out to handle this coercion etc. */ break; } /* XXX: could set specialcall for boundfuncs too, simplify check above */ if (DUK_HOBJECT_HAS_BOUNDFUNC(func)) { DUK_ASSERT(!DUK_HOBJECT_HAS_SPECIAL_CALL(func)); DUK_ASSERT(!DUK_HOBJECT_IS_NATFUNC(func)); /* Callable/constructable flags are the same * for the bound function and its target, so * we don't need to check them here, we can * check them from the target only. */ duk__handle_bound_chain_for_call(thr, idx_func, *call_flags & DUK_CALL_FLAG_CONSTRUCT); DUK_ASSERT(DUK_TVAL_IS_OBJECT(duk_require_tval(thr, idx_func)) || DUK_TVAL_IS_LIGHTFUNC(duk_require_tval(thr, idx_func))); } else { DUK_ASSERT(DUK_HOBJECT_HAS_SPECIAL_CALL(func)); #if defined(DUK_USE_ES6_PROXY) if (DUK_HOBJECT_HAS_EXOTIC_PROXYOBJ(func)) { /* If no trap, resume processing from Proxy trap. * If trap exists, helper converts call into a trap * call; this may change a constructor call into a * normal (non-constructor) trap call. We must * continue processing even when a trap is found as * the trap may be bound. */ duk__handle_proxy_for_call(thr, idx_func, (duk_hproxy *) func, call_flags); } else #endif { DUK_ASSERT(DUK_HOBJECT_IS_NATFUNC(func)); DUK_ASSERT(DUK_HOBJECT_HAS_CALLABLE(func)); DUK_ASSERT(!DUK_HOBJECT_HAS_CONSTRUCTABLE(func)); /* Constructable check already done above. */ if (duk__handle_specialfuncs_for_call(thr, idx_func, func, call_flags, first) != 0) { /* Encountered native eval call, normal call * context. Break out, handle this coercion etc. */ break; } } } /* Retry loop. */ } else if (DUK_TVAL_IS_LIGHTFUNC(tv_func)) { /* Lightfuncs are: * - Always strict, so no 'this' coercion. * - Always callable. * - Always constructable. * - Never specialfuncs. */ func = NULL; goto finished; } else { goto not_callable; } } DUK_ASSERT(func != NULL); if (!DUK_HOBJECT_HAS_STRICT(func)) { /* Non-strict target needs 'this' coercion. * This has potential side effects invalidating * 'tv_func'. */ duk__coerce_nonstrict_this_binding(thr, idx_func + 1); } if (*call_flags & DUK_CALL_FLAG_CONSTRUCT) { if (!(*call_flags & DUK_CALL_FLAG_DEFAULT_INSTANCE_UPDATED)) { *call_flags |= DUK_CALL_FLAG_DEFAULT_INSTANCE_UPDATED; duk__update_default_instance_proto(thr, idx_func); } } finished: #if defined(DUK_USE_ASSERTIONS) { duk_tval *tv_tmp; tv_tmp = duk_get_tval(thr, idx_func); DUK_ASSERT(tv_tmp != NULL); DUK_ASSERT((DUK_TVAL_IS_OBJECT(tv_tmp) && DUK_HOBJECT_IS_CALLABLE(DUK_TVAL_GET_OBJECT(tv_tmp))) || DUK_TVAL_IS_LIGHTFUNC(tv_tmp)); DUK_ASSERT(func == NULL || !DUK_HOBJECT_HAS_BOUNDFUNC(func)); DUK_ASSERT(func == NULL || (DUK_HOBJECT_IS_COMPFUNC(func) || DUK_HOBJECT_IS_NATFUNC(func))); DUK_ASSERT(func == NULL || (DUK_HOBJECT_HAS_CONSTRUCTABLE(func) || (*call_flags & DUK_CALL_FLAG_CONSTRUCT) == 0)); } #endif return func; not_callable: DUK_ASSERT(tv_func != NULL); #if defined(DUK_USE_VERBOSE_ERRORS) /* GETPROPC delayed error handling: when target is not callable, * GETPROPC replaces idx_func+0 with an Error (non-callable) with * a hidden Symbol to signify it's to be thrown as is here. The * hidden Symbol is only checked as an own property, not inherited * (which would be dangerous). */ if (DUK_TVAL_IS_OBJECT(tv_func)) { if (duk_hobject_find_existing_entry_tval_ptr(thr->heap, DUK_TVAL_GET_OBJECT(tv_func), DUK_HTHREAD_STRING_INT_TARGET(thr)) != NULL) { duk_push_tval(thr, tv_func); (void) duk_throw(thr); DUK_WO_NORETURN(return NULL;); } } #endif #if defined(DUK_USE_VERBOSE_ERRORS) #if defined(DUK_USE_PARANOID_ERRORS) DUK_ERROR_FMT1(thr, DUK_ERR_TYPE_ERROR, "%s not callable", duk_get_type_name(thr, idx_func)); #else DUK_ERROR_FMT1(thr, DUK_ERR_TYPE_ERROR, "%s not callable", duk_push_string_tval_readable(thr, tv_func)); #endif #else DUK_ERROR_TYPE(thr, DUK_STR_NOT_CALLABLE); #endif DUK_WO_NORETURN(return NULL;); not_constructable: /* For now GETPROPC delayed error not needed for constructor calls. */ #if defined(DUK_USE_VERBOSE_ERRORS) #if defined(DUK_USE_PARANOID_ERRORS) DUK_ERROR_FMT1(thr, DUK_ERR_TYPE_ERROR, "%s not constructable", duk_get_type_name(thr, idx_func)); #else DUK_ERROR_FMT1(thr, DUK_ERR_TYPE_ERROR, "%s not constructable", duk_push_string_tval_readable(thr, tv_func)); #endif #else DUK_ERROR_TYPE(thr, DUK_STR_NOT_CONSTRUCTABLE); #endif DUK_WO_NORETURN(return NULL;); } /* * Manipulate value stack so that exactly 'num_stack_rets' return * values are at 'idx_retbase' in every case, assuming there are * 'rc' return values on top of stack. * * This is a bit tricky, because the called C function operates in * the same activation record and may have e.g. popped the stack * empty (below idx_retbase). */ DUK_LOCAL void duk__safe_call_adjust_valstack(duk_hthread *thr, duk_idx_t idx_retbase, duk_idx_t num_stack_rets, duk_idx_t num_actual_rets) { duk_idx_t idx_rcbase; DUK_ASSERT(thr != NULL); DUK_ASSERT(idx_retbase >= 0); DUK_ASSERT(num_stack_rets >= 0); DUK_ASSERT(num_actual_rets >= 0); idx_rcbase = duk_get_top(thr) - num_actual_rets; /* base of known return values */ if (DUK_UNLIKELY(idx_rcbase < 0)) { DUK_ERROR_TYPE(thr, DUK_STR_INVALID_CFUNC_RC); DUK_WO_NORETURN(return;); } DUK_DDD(DUK_DDDPRINT("adjust valstack after func call: " "num_stack_rets=%ld, num_actual_rets=%ld, stack_top=%ld, idx_retbase=%ld, idx_rcbase=%ld", (long) num_stack_rets, (long) num_actual_rets, (long) duk_get_top(thr), (long) idx_retbase, (long) idx_rcbase)); DUK_ASSERT(idx_rcbase >= 0); /* caller must check */ /* Space for num_stack_rets was reserved before the safe call. * Because value stack reserve cannot shrink except in call returns, * the reserve is still in place. Adjust valstack, carefully * ensuring we don't overstep the reserve. */ /* Match idx_rcbase with idx_retbase so that the return values * start at the correct index. */ if (idx_rcbase > idx_retbase) { duk_idx_t count = idx_rcbase - idx_retbase; DUK_DDD(DUK_DDDPRINT("elements at/after idx_retbase have enough to cover func retvals " "(idx_retbase=%ld, idx_rcbase=%ld)", (long) idx_retbase, (long) idx_rcbase)); /* Remove values between irc_rcbase (start of intended return * values) and idx_retbase to lower return values to idx_retbase. */ DUK_ASSERT(count > 0); duk_remove_n(thr, idx_retbase, count); /* may be NORZ */ } else { duk_idx_t count = idx_retbase - idx_rcbase; DUK_DDD(DUK_DDDPRINT("not enough elements at/after idx_retbase to cover func retvals " "(idx_retbase=%ld, idx_rcbase=%ld)", (long) idx_retbase, (long) idx_rcbase)); /* Insert 'undefined' at idx_rcbase (start of intended return * values) to lift return values to idx_retbase. */ DUK_ASSERT(count >= 0); DUK_ASSERT(thr->valstack_end - thr->valstack_top >= count); /* reserve cannot shrink */ duk_insert_undefined_n(thr, idx_rcbase, count); } /* Chop extra retvals away / extend with undefined. */ duk_set_top_unsafe(thr, idx_retbase + num_stack_rets); } /* * Activation setup for tailcalls and non-tailcalls. */ #if defined(DUK_USE_TAILCALL) DUK_LOCAL duk_small_uint_t duk__call_setup_act_attempt_tailcall(duk_hthread *thr, duk_small_uint_t call_flags, duk_idx_t idx_func, duk_hobject *func, duk_size_t entry_valstack_bottom_byteoff, duk_size_t entry_valstack_end_byteoff, duk_idx_t *out_nargs, duk_idx_t *out_nregs, duk_size_t *out_vs_min_bytes, duk_activation **out_act) { duk_activation *act; duk_tval *tv1, *tv2; duk_idx_t idx_args; duk_small_uint_t flags1, flags2; #if defined(DUK_USE_DEBUGGER_SUPPORT) duk_activation *prev_pause_act; #endif DUK_UNREF(entry_valstack_end_byteoff); /* Tailcall cannot be flagged to resume calls, and a * previous frame must exist. */ DUK_ASSERT(thr->callstack_top >= 1); act = thr->callstack_curr; DUK_ASSERT(act != NULL); *out_act = act; if (func == NULL || !DUK_HOBJECT_IS_COMPFUNC(func)) { DUK_DDD(DUK_DDDPRINT("tail call prevented by target not being ecma function")); return 0; } if (act->flags & DUK_ACT_FLAG_PREVENT_YIELD) { DUK_DDD(DUK_DDDPRINT("tail call prevented by current activation having DUK_ACT_FLAG_PREVENT_YIELD")); return 0; } /* Tailcall is only allowed if current and candidate * function have identical return value handling. There * are three possible return value handling cases: * 1. Normal function call, no special return value handling. * 2. Constructor call, return value replacement object check. * 3. Proxy 'construct' trap call, return value invariant check. */ flags1 = (duk_small_uint_t) ((act->flags & DUK_ACT_FLAG_CONSTRUCT) ? 1 : 0) #if defined(DUK_USE_ES6_PROXY) | (duk_small_uint_t) ((act->flags & DUK_ACT_FLAG_CONSTRUCT_PROXY) ? 2 : 0) #endif ; flags2 = (duk_small_uint_t) ((call_flags & DUK_CALL_FLAG_CONSTRUCT) ? 1 : 0) #if defined(DUK_USE_ES6_PROXY) | (duk_small_uint_t) ((call_flags & DUK_CALL_FLAG_CONSTRUCT_PROXY) ? 2 : 0); #endif ; if (flags1 != flags2) { DUK_DDD(DUK_DDDPRINT("tail call prevented by incompatible return value handling")); return 0; } DUK_ASSERT(((act->flags & DUK_ACT_FLAG_CONSTRUCT) && (call_flags & DUK_CALL_FLAG_CONSTRUCT)) || (!(act->flags & DUK_ACT_FLAG_CONSTRUCT) && !(call_flags & DUK_CALL_FLAG_CONSTRUCT))); DUK_ASSERT(((act->flags & DUK_ACT_FLAG_CONSTRUCT_PROXY) && (call_flags & DUK_CALL_FLAG_CONSTRUCT_PROXY)) || (!(act->flags & DUK_ACT_FLAG_CONSTRUCT_PROXY) && !(call_flags & DUK_CALL_FLAG_CONSTRUCT_PROXY))); if (DUK_HOBJECT_HAS_NOTAIL(func)) { /* See: test-bug-tailcall-preventyield-assert.c. */ DUK_DDD(DUK_DDDPRINT("tail call prevented by function having a notail flag")); return 0; } /* * Tailcall handling * * Although the callstack entry is reused, we need to explicitly unwind * the current activation (or simulate an unwind). In particular, the * current activation must be closed, otherwise something like * test-bug-reduce-judofyr.js results. Also catchers need to be unwound * because there may be non-error-catching label entries in valid tail calls. * * Special attention is needed for debugger and pause behavior when * reusing an activation. * - Disable StepOut processing for the activation unwind because * we reuse the activation, see: * https://github.com/svaarala/duktape/issues/1684. * - Disable line change pause flag permanently if act == dbg_pause_act * (if set) because it would no longer be relevant, see: * https://github.com/svaarala/duktape/issues/1726, * https://github.com/svaarala/duktape/issues/1786. * - Check for function entry (e.g. StepInto) pause flag here, because * the executor pause check won't trigger due to shared activation, see: * https://github.com/svaarala/duktape/issues/1726. */ DUK_DDD(DUK_DDDPRINT("is tail call, reusing activation at callstack top, at index %ld", (long) (thr->callstack_top - 1))); DUK_ASSERT(!DUK_HOBJECT_HAS_BOUNDFUNC(func)); DUK_ASSERT(!DUK_HOBJECT_HAS_NATFUNC(func)); DUK_ASSERT(DUK_HOBJECT_HAS_COMPFUNC(func)); DUK_ASSERT((act->flags & DUK_ACT_FLAG_PREVENT_YIELD) == 0); DUK_ASSERT(call_flags & DUK_CALL_FLAG_ALLOW_ECMATOECMA); /* Unwind the topmost callstack entry before reusing it. This * also unwinds the catchers related to the topmost entry. */ DUK_ASSERT(thr->callstack_top > 0); DUK_ASSERT(thr->callstack_curr != NULL); #if defined(DUK_USE_DEBUGGER_SUPPORT) if (act == thr->heap->dbg_pause_act) { thr->heap->dbg_pause_flags &= ~DUK_PAUSE_FLAG_LINE_CHANGE; } prev_pause_act = thr->heap->dbg_pause_act; thr->heap->dbg_pause_act = NULL; if (thr->heap->dbg_pause_flags & DUK_PAUSE_FLAG_FUNC_ENTRY) { DUK_D(DUK_DPRINT("PAUSE TRIGGERED by function entry (tailcall)")); duk_debug_set_paused(thr->heap); } #endif duk_hthread_activation_unwind_reuse_norz(thr); #if defined(DUK_USE_DEBUGGER_SUPPORT) thr->heap->dbg_pause_act = prev_pause_act; #endif DUK_ASSERT(act == thr->callstack_curr); /* XXX: We could restore the caller's value stack reserve * here, as if we did an actual unwind-and-call. Without * the restoration, value stack reserve may remain higher * than would otherwise be possible until we return to a * non-tailcall. */ /* Then reuse the unwound activation. */ act->cat = NULL; act->var_env = NULL; act->lex_env = NULL; DUK_ASSERT(func != NULL); DUK_ASSERT(DUK_HOBJECT_HAS_COMPFUNC(func)); act->func = func; /* don't want an intermediate exposed state with func == NULL */ #if defined(DUK_USE_NONSTD_FUNC_CALLER_PROPERTY) act->prev_caller = NULL; #endif /* don't want an intermediate exposed state with invalid pc */ act->curr_pc = DUK_HCOMPFUNC_GET_CODE_BASE(thr->heap, (duk_hcompfunc *) func); #if defined(DUK_USE_DEBUGGER_SUPPORT) act->prev_line = 0; #endif DUK_TVAL_SET_OBJECT(&act->tv_func, func); /* borrowed, no refcount */ DUK_HOBJECT_INCREF(thr, func); act->flags = DUK_ACT_FLAG_TAILCALLED; if (DUK_HOBJECT_HAS_STRICT(func)) { act->flags |= DUK_ACT_FLAG_STRICT; } if (call_flags & DUK_CALL_FLAG_CONSTRUCT) { act->flags |= DUK_ACT_FLAG_CONSTRUCT; } #if defined(DUK_USE_ES6_PROXY) if (call_flags & DUK_CALL_FLAG_CONSTRUCT_PROXY) { act->flags |= DUK_ACT_FLAG_CONSTRUCT_PROXY; } #endif DUK_ASSERT(DUK_ACT_GET_FUNC(act) == func); /* already updated */ DUK_ASSERT(act->var_env == NULL); DUK_ASSERT(act->lex_env == NULL); act->bottom_byteoff = entry_valstack_bottom_byteoff; /* tail call -> reuse current "frame" */ #if 0 /* Topmost activation retval_byteoff is considered garbage, no need to init. */ act->retval_byteoff = 0; #endif /* Filled in when final reserve is known, dummy value doesn't matter * even in error unwind because reserve_byteoff is only used when * returning to -this- activation. */ act->reserve_byteoff = 0; /* * Manipulate valstack so that args are on the current bottom and the * previous caller's 'this' binding (which is the value preceding the * current bottom) is replaced with the new 'this' binding: * * [ ... this_old | (crud) func this_new arg1 ... argN ] * --> [ ... this_new | arg1 ... argN ] * * For tail calling to work properly, the valstack bottom must not grow * here; otherwise crud would accumulate on the valstack. */ tv1 = thr->valstack_bottom - 1; tv2 = thr->valstack_bottom + idx_func + 1; DUK_ASSERT(tv1 >= thr->valstack && tv1 < thr->valstack_top); /* tv1 is -below- valstack_bottom */ DUK_ASSERT(tv2 >= thr->valstack_bottom && tv2 < thr->valstack_top); DUK_TVAL_SET_TVAL_UPDREF(thr, tv1, tv2); /* side effects */ idx_args = idx_func + 2; duk_remove_n(thr, 0, idx_args); /* may be NORZ */ idx_func = 0; DUK_UNREF(idx_func); /* really 'not applicable' anymore, should not be referenced after this */ idx_args = 0; *out_nargs = ((duk_hcompfunc *) func)->nargs; *out_nregs = ((duk_hcompfunc *) func)->nregs; DUK_ASSERT(*out_nregs >= 0); DUK_ASSERT(*out_nregs >= *out_nargs); *out_vs_min_bytes = entry_valstack_bottom_byteoff + sizeof(duk_tval) * ((duk_size_t) *out_nregs + DUK_VALSTACK_INTERNAL_EXTRA); #if defined(DUK_USE_NONSTD_FUNC_CALLER_PROPERTY) #if defined(DUK_USE_TAILCALL) #error incorrect options: tail calls enabled with function caller property #endif /* XXX: This doesn't actually work properly for tail calls, so * tail calls are disabled when DUK_USE_NONSTD_FUNC_CALLER_PROPERTY * is in use. */ duk__update_func_caller_prop(thr, func); #endif /* [ ... this_new | arg1 ... argN ] */ return 1; } #endif /* DUK_USE_TAILCALL */ DUK_LOCAL void duk__call_setup_act_not_tailcall(duk_hthread *thr, duk_small_uint_t call_flags, duk_idx_t idx_func, duk_hobject *func, duk_size_t entry_valstack_bottom_byteoff, duk_size_t entry_valstack_end_byteoff, duk_idx_t *out_nargs, duk_idx_t *out_nregs, duk_size_t *out_vs_min_bytes, duk_activation **out_act) { duk_activation *act; duk_activation *new_act; DUK_UNREF(entry_valstack_end_byteoff); DUK_DDD(DUK_DDDPRINT("not a tail call, pushing a new activation to callstack, to index %ld", (long) (thr->callstack_top))); duk__call_callstack_limit_check(thr); new_act = duk_hthread_activation_alloc(thr); DUK_ASSERT(new_act != NULL); act = thr->callstack_curr; if (act != NULL) { /* * Update return value stack index of current activation (if any). * * Although it might seem this is not necessary (bytecode executor * does this for ECMAScript-to-ECMAScript calls; other calls are * handled here), this turns out to be necessary for handling yield * and resume. For them, an ECMAScript-to-native call happens, and * the ECMAScript call's retval_byteoff must be set for things to work. */ act->retval_byteoff = entry_valstack_bottom_byteoff + (duk_size_t) idx_func * sizeof(duk_tval); } new_act->parent = act; thr->callstack_curr = new_act; thr->callstack_top++; act = new_act; *out_act = act; DUK_ASSERT(thr->valstack_top > thr->valstack_bottom); /* at least effective 'this' */ DUK_ASSERT(func == NULL || !DUK_HOBJECT_HAS_BOUNDFUNC(func)); act->cat = NULL; act->flags = 0; if (call_flags & DUK_CALL_FLAG_CONSTRUCT) { act->flags |= DUK_ACT_FLAG_CONSTRUCT; } #if defined(DUK_USE_ES6_PROXY) if (call_flags & DUK_CALL_FLAG_CONSTRUCT_PROXY) { act->flags |= DUK_ACT_FLAG_CONSTRUCT_PROXY; } #endif if (call_flags & DUK_CALL_FLAG_DIRECT_EVAL) { act->flags |= DUK_ACT_FLAG_DIRECT_EVAL; } /* start of arguments: idx_func + 2. */ act->func = func; /* NULL for lightfunc */ if (DUK_LIKELY(func != NULL)) { DUK_TVAL_SET_OBJECT(&act->tv_func, func); /* borrowed, no refcount */ if (DUK_HOBJECT_HAS_STRICT(func)) { act->flags |= DUK_ACT_FLAG_STRICT; } if (DUK_HOBJECT_IS_COMPFUNC(func)) { *out_nargs = ((duk_hcompfunc *) func)->nargs; *out_nregs = ((duk_hcompfunc *) func)->nregs; DUK_ASSERT(*out_nregs >= 0); DUK_ASSERT(*out_nregs >= *out_nargs); *out_vs_min_bytes = entry_valstack_bottom_byteoff + sizeof(duk_tval) * ((duk_size_t) idx_func + 2U + (duk_size_t) *out_nregs + DUK_VALSTACK_INTERNAL_EXTRA); } else { /* True because of call target lookup checks. */ DUK_ASSERT(DUK_HOBJECT_IS_NATFUNC(func)); *out_nargs = ((duk_hnatfunc *) func)->nargs; *out_nregs = *out_nargs; if (*out_nargs >= 0) { *out_vs_min_bytes = entry_valstack_bottom_byteoff + sizeof(duk_tval) * ((duk_size_t) idx_func + 2U + (duk_size_t) *out_nregs + DUK_VALSTACK_API_ENTRY_MINIMUM + DUK_VALSTACK_INTERNAL_EXTRA); } else { /* Vararg function. */ duk_size_t valstack_top_byteoff = (duk_size_t) ((duk_uint8_t *) thr->valstack_top - ((duk_uint8_t *) thr->valstack)); *out_vs_min_bytes = valstack_top_byteoff + sizeof(duk_tval) * (DUK_VALSTACK_API_ENTRY_MINIMUM + DUK_VALSTACK_INTERNAL_EXTRA); } } } else { duk_small_uint_t lf_flags; duk_tval *tv_func; act->flags |= DUK_ACT_FLAG_STRICT; tv_func = DUK_GET_TVAL_POSIDX(thr, idx_func); DUK_ASSERT(DUK_TVAL_IS_LIGHTFUNC(tv_func)); DUK_TVAL_SET_TVAL(&act->tv_func, tv_func); /* borrowed, no refcount */ lf_flags = DUK_TVAL_GET_LIGHTFUNC_FLAGS(tv_func); *out_nargs = DUK_LFUNC_FLAGS_GET_NARGS(lf_flags); if (*out_nargs != DUK_LFUNC_NARGS_VARARGS) { *out_vs_min_bytes = entry_valstack_bottom_byteoff + sizeof(duk_tval) * ((duk_size_t) idx_func + 2U + (duk_size_t) *out_nargs + DUK_VALSTACK_API_ENTRY_MINIMUM + DUK_VALSTACK_INTERNAL_EXTRA); } else { duk_size_t valstack_top_byteoff = (duk_size_t) ((duk_uint8_t *) thr->valstack_top - ((duk_uint8_t *) thr->valstack)); *out_vs_min_bytes = valstack_top_byteoff + sizeof(duk_tval) * (DUK_VALSTACK_API_ENTRY_MINIMUM + DUK_VALSTACK_INTERNAL_EXTRA); *out_nargs = -1; /* vararg */ } *out_nregs = *out_nargs; } act->var_env = NULL; act->lex_env = NULL; #if defined(DUK_USE_NONSTD_FUNC_CALLER_PROPERTY) act->prev_caller = NULL; #endif act->curr_pc = NULL; #if defined(DUK_USE_DEBUGGER_SUPPORT) act->prev_line = 0; #endif act->bottom_byteoff = entry_valstack_bottom_byteoff + sizeof(duk_tval) * ((duk_size_t) idx_func + 2U); #if 0 act->retval_byteoff = 0; /* topmost activation retval_byteoff is considered garbage, no need to init */ #endif /* Filled in when final reserve is known, dummy value doesn't matter * even in error unwind because reserve_byteoff is only used when * returning to -this- activation. */ act->reserve_byteoff = 0; /* filled in by caller */ /* XXX: Is this INCREF necessary? 'func' is always a borrowed * reference reachable through the value stack? If changed, stack * unwind code also needs to be fixed to match. */ DUK_HOBJECT_INCREF_ALLOWNULL(thr, func); /* act->func */ #if defined(DUK_USE_NONSTD_FUNC_CALLER_PROPERTY) if (func) { duk__update_func_caller_prop(thr, func); } #endif } /* * Environment setup. */ DUK_LOCAL void duk__call_env_setup(duk_hthread *thr, duk_hobject *func, duk_activation *act, duk_idx_t idx_args) { duk_hobject *env; DUK_ASSERT(func == NULL || !DUK_HOBJECT_HAS_BOUNDFUNC(func)); /* bound function has already been resolved */ if (DUK_LIKELY(func != NULL)) { if (DUK_LIKELY(DUK_HOBJECT_HAS_NEWENV(func))) { if (DUK_LIKELY(!DUK_HOBJECT_HAS_CREATEARGS(func))) { /* Use a new environment but there's no 'arguments' object; * delayed environment initialization. This is the most * common case. */ DUK_ASSERT(act->lex_env == NULL); DUK_ASSERT(act->var_env == NULL); } else { /* Use a new environment and there's an 'arguments' object. * We need to initialize it right now. */ /* third arg: absolute index (to entire valstack) of bottom_byteoff of new activation */ env = duk_create_activation_environment_record(thr, func, act->bottom_byteoff); DUK_ASSERT(env != NULL); /* [ ... func this arg1 ... argN envobj ] */ DUK_ASSERT(DUK_HOBJECT_HAS_CREATEARGS(func)); duk__handle_createargs_for_call(thr, func, env, idx_args); /* [ ... func this arg1 ... argN envobj ] */ act->lex_env = env; act->var_env = env; DUK_HOBJECT_INCREF(thr, env); DUK_HOBJECT_INCREF(thr, env); /* XXX: incref by count (2) directly */ duk_pop(thr); } } else { /* Use existing env (e.g. for non-strict eval); cannot have * an own 'arguments' object (but can refer to an existing one). */ DUK_ASSERT(!DUK_HOBJECT_HAS_CREATEARGS(func)); duk__handle_oldenv_for_call(thr, func, act); DUK_ASSERT(act->lex_env != NULL); DUK_ASSERT(act->var_env != NULL); } } else { /* Lightfuncs are always native functions and have "newenv". */ DUK_ASSERT(act->lex_env == NULL); DUK_ASSERT(act->var_env == NULL); } } /* * Misc shared helpers. */ /* Check thread state, update current thread. */ DUK_LOCAL void duk__call_thread_state_update(duk_hthread *thr) { DUK_ASSERT(thr != NULL); if (DUK_LIKELY(thr == thr->heap->curr_thread)) { if (DUK_UNLIKELY(thr->state != DUK_HTHREAD_STATE_RUNNING)) { /* Should actually never happen, but check anyway. */ goto thread_state_error; } } else { DUK_ASSERT(thr->heap->curr_thread == NULL || thr->heap->curr_thread->state == DUK_HTHREAD_STATE_RUNNING); if (DUK_UNLIKELY(thr->state != DUK_HTHREAD_STATE_INACTIVE)) { goto thread_state_error; } DUK_HEAP_SWITCH_THREAD(thr->heap, thr); thr->state = DUK_HTHREAD_STATE_RUNNING; /* Multiple threads may be simultaneously in the RUNNING * state, but not in the same "resume chain". */ } DUK_ASSERT(thr->heap->curr_thread == thr); DUK_ASSERT(thr->state == DUK_HTHREAD_STATE_RUNNING); return; thread_state_error: DUK_ERROR_FMT1(thr, DUK_ERR_TYPE_ERROR, "invalid thread state (%ld)", (long) thr->state); DUK_WO_NORETURN(return;); } /* * Main unprotected call handler, handles: * * - All combinations of native/ECMAScript caller and native/ECMAScript * target. * * - Optimized ECMAScript-to-ECMAScript call where call handling only * sets up a new duk_activation but reuses an existing bytecode executor * (the caller) without native recursion. * * - Tailcalls, where an activation is reused without increasing call * stack (duk_activation) depth. * * - Setup for an initial Duktape.Thread.resume(). * * The call handler doesn't provide any protection guarantees, protected calls * must be implemented e.g. by wrapping the call in a duk_safe_call(). * Call setup may fail at any stage, even when the new activation is in * place; the only guarantee is that the state is consistent for unwinding. */ DUK_LOCAL duk_int_t duk__handle_call_raw(duk_hthread *thr, duk_idx_t idx_func, duk_small_uint_t call_flags) { #if defined(DUK_USE_ASSERTIONS) duk_activation *entry_act; duk_size_t entry_callstack_top; #endif duk_size_t entry_valstack_bottom_byteoff; duk_size_t entry_valstack_end_byteoff; duk_int_t entry_call_recursion_depth; duk_hthread *entry_curr_thread; duk_uint_fast8_t entry_thread_state; duk_instr_t **entry_ptr_curr_pc; duk_idx_t idx_args; duk_idx_t nargs; /* # argument registers target function wants (< 0 => "as is") */ duk_idx_t nregs; /* # total registers target function wants on entry (< 0 => "as is") */ duk_size_t vs_min_bytes; /* minimum value stack size (bytes) for handling call */ duk_hobject *func; /* 'func' on stack (borrowed reference) */ duk_activation *act; duk_ret_t rc; duk_small_uint_t use_tailcall; DUK_ASSERT(thr != NULL); DUK_ASSERT(thr->heap != NULL); /* Asserts for heap->curr_thread omitted: it may be NULL, 'thr', or * any other thread (e.g. when heap thread is used to run finalizers). */ DUK_ASSERT_CTX_VALID(thr); DUK_ASSERT(duk_is_valid_index(thr, idx_func)); DUK_ASSERT(idx_func >= 0); DUK_STATS_INC(thr->heap, stats_call_all); /* If a tail call: * - an ECMAScript activation must be on top of the callstack * - there cannot be any catch stack entries that would catch * a return */ #if defined(DUK_USE_ASSERTIONS) if (call_flags & DUK_CALL_FLAG_TAILCALL) { duk_activation *tmp_act; duk_catcher *tmp_cat; DUK_ASSERT(thr->callstack_top >= 1); DUK_ASSERT(DUK_ACT_GET_FUNC(thr->callstack_curr) != NULL); DUK_ASSERT(DUK_HOBJECT_IS_COMPFUNC(DUK_ACT_GET_FUNC(thr->callstack_curr))); /* No entry in the catch stack which would actually catch a * throw can refer to the callstack entry being reused. * There *can* be catch stack entries referring to the current * callstack entry as long as they don't catch (e.g. label sites). */ tmp_act = thr->callstack_curr; for (tmp_cat = tmp_act->cat; tmp_cat != NULL; tmp_cat = tmp_cat->parent) { DUK_ASSERT(DUK_CAT_GET_TYPE(tmp_cat) == DUK_CAT_TYPE_LABEL); /* a non-catching entry */ } } #endif /* DUK_USE_ASSERTIONS */ /* * Store entry state. */ #if defined(DUK_USE_ASSERTIONS) entry_act = thr->callstack_curr; entry_callstack_top = thr->callstack_top; #endif entry_valstack_bottom_byteoff = (duk_size_t) ((duk_uint8_t *) thr->valstack_bottom - (duk_uint8_t *) thr->valstack); entry_valstack_end_byteoff = (duk_size_t) ((duk_uint8_t *) thr->valstack_end - (duk_uint8_t *) thr->valstack); entry_call_recursion_depth = thr->heap->call_recursion_depth; entry_curr_thread = thr->heap->curr_thread; /* may be NULL if first call */ entry_thread_state = thr->state; entry_ptr_curr_pc = thr->ptr_curr_pc; /* may be NULL */ /* If thr->ptr_curr_pc is set, sync curr_pc to act->pc. Then NULL * thr->ptr_curr_pc so that it's not accidentally used with an incorrect * activation when side effects occur. */ duk_hthread_sync_and_null_currpc(thr); DUK_ASSERT(thr->ptr_curr_pc == NULL); DUK_DD(DUK_DDPRINT("duk__handle_call_raw: thr=%p, idx_func=%ld, " "call_flags=0x%08lx (constructor=%ld), " "valstack_top=%ld, idx_func=%ld, idx_args=%ld, rec_depth=%ld/%ld, " "entry_valstack_bottom_byteoff=%ld, entry_valstack_end_byteoff=%ld, " "entry_call_recursion_depth=%ld, " "entry_curr_thread=%p, entry_thread_state=%ld", (void *) thr, (long) idx_func, (unsigned long) call_flags, (long) ((call_flags & DUK_CALL_FLAG_CONSTRUCT) != 0 ? 1 : 0), (long) duk_get_top(thr), (long) idx_func, (long) (idx_func + 2), (long) thr->heap->call_recursion_depth, (long) thr->heap->call_recursion_limit, (long) entry_valstack_bottom_byteoff, (long) entry_valstack_end_byteoff, (long) entry_call_recursion_depth, (void *) entry_curr_thread, (long) entry_thread_state)); /* * Thread state check and book-keeping. */ duk__call_thread_state_update(thr); /* * Resolve final target function; handle bound functions and special * functions like .call() and .apply(). Also figure out the effective * 'this' binding, which replaces the current value at idx_func + 1. */ if (DUK_LIKELY(duk__resolve_target_fastpath_check(thr, idx_func, &func, call_flags) != 0U)) { DUK_DDD(DUK_DDDPRINT("fast path target resolve")); } else { DUK_DDD(DUK_DDDPRINT("slow path target resolve")); func = duk__resolve_target_func_and_this_binding(thr, idx_func, &call_flags); } DUK_ASSERT(duk_get_top(thr) - idx_func >= 2); /* at least func and this present */ DUK_ASSERT(func == NULL || !DUK_HOBJECT_HAS_BOUNDFUNC(func)); DUK_ASSERT(func == NULL || (DUK_HOBJECT_IS_COMPFUNC(func) || DUK_HOBJECT_IS_NATFUNC(func))); /* [ ... func this arg1 ... argN ] */ /* * Setup a preliminary activation and figure out nargs/nregs and * value stack minimum size. * * Don't touch valstack_bottom or valstack_top yet so that Duktape API * calls work normally. * * Because 'act' is not zeroed, all fields must be filled in. */ /* Should not be necessary, but initialize to silence warnings. */ act = NULL; nargs = 0; nregs = 0; vs_min_bytes = 0; #if defined(DUK_USE_TAILCALL) use_tailcall = (call_flags & DUK_CALL_FLAG_TAILCALL); if (use_tailcall) { use_tailcall = duk__call_setup_act_attempt_tailcall(thr, call_flags, idx_func, func, entry_valstack_bottom_byteoff, entry_valstack_end_byteoff, &nargs, &nregs, &vs_min_bytes, &act); } #else DUK_ASSERT((call_flags & DUK_CALL_FLAG_TAILCALL) == 0); /* compiler ensures this */ use_tailcall = 0; #endif if (use_tailcall) { idx_args = 0; DUK_STATS_INC(thr->heap, stats_call_tailcall); } else { duk__call_setup_act_not_tailcall(thr, call_flags, idx_func, func, entry_valstack_bottom_byteoff, entry_valstack_end_byteoff, &nargs, &nregs, &vs_min_bytes, &act); idx_args = idx_func + 2; } /* After this point idx_func is no longer valid for tailcalls. */ DUK_ASSERT(act != NULL); /* [ ... func this arg1 ... argN ] */ /* * Environment record creation and 'arguments' object creation. * Named function expression name binding is handled by the * compiler; the compiled function's parent env will contain * the (immutable) binding already. * * This handling is now identical for C and ECMAScript functions. * C functions always have the 'NEWENV' flag set, so their * environment record initialization is delayed (which is good). * * Delayed creation (on demand) is handled in duk_js_var.c. */ duk__call_env_setup(thr, func, act, idx_args); /* [ ... func this arg1 ... argN ] */ /* * Setup value stack: clamp to 'nargs', fill up to 'nregs', * ensure value stack size matches target requirements, and * switch value stack bottom. Valstack top is kept. * * Value stack can only grow here. */ duk_valstack_grow_check_throw(thr, vs_min_bytes); act->reserve_byteoff = (duk_size_t) ((duk_uint8_t *) thr->valstack_end - (duk_uint8_t *) thr->valstack); if (use_tailcall) { DUK_ASSERT(nregs >= 0); DUK_ASSERT(nregs >= nargs); duk_set_top_and_wipe(thr, nregs, nargs); } else { if (nregs >= 0) { DUK_ASSERT(nregs >= nargs); duk_set_top_and_wipe(thr, idx_func + 2 + nregs, idx_func + 2 + nargs); } else { ; } thr->valstack_bottom = thr->valstack_bottom + idx_func + 2; } DUK_ASSERT(thr->valstack_bottom >= thr->valstack); DUK_ASSERT(thr->valstack_top >= thr->valstack_bottom); DUK_ASSERT(thr->valstack_end >= thr->valstack_top); /* * Make the actual call. For Ecma-to-Ecma calls detect that * setup is complete, then return with a status code that allows * the caller to reuse the running executor. */ if (func != NULL && DUK_HOBJECT_IS_COMPFUNC(func)) { /* * ECMAScript call. */ DUK_ASSERT(func != NULL); DUK_ASSERT(DUK_HOBJECT_HAS_COMPFUNC(func)); act->curr_pc = DUK_HCOMPFUNC_GET_CODE_BASE(thr->heap, (duk_hcompfunc *) func); if (call_flags & DUK_CALL_FLAG_ALLOW_ECMATOECMA) { DUK_DD(DUK_DDPRINT("avoid native call, use existing executor")); DUK_STATS_INC(thr->heap, stats_call_ecmatoecma); DUK_ASSERT((act->flags & DUK_ACT_FLAG_PREVENT_YIELD) == 0); DUK_REFZERO_CHECK_FAST(thr); DUK_ASSERT(thr->ptr_curr_pc == NULL); return 1; /* 1=reuse executor */ } DUK_ASSERT(use_tailcall == 0); /* duk_hthread_activation_unwind_norz() will decrease this on unwind */ DUK_ASSERT((act->flags & DUK_ACT_FLAG_PREVENT_YIELD) == 0); act->flags |= DUK_ACT_FLAG_PREVENT_YIELD; thr->callstack_preventcount++; /* XXX: we could just do this on entry regardless of reuse, as long * as recursion depth is decreased for e2e case. */ duk__call_c_recursion_limit_check(thr); thr->heap->call_recursion_depth++; /* [ ... func this | arg1 ... argN ] ('this' must precede new bottom) */ /* * Bytecode executor call. * * Execute bytecode, handling any recursive function calls and * thread resumptions. Returns when execution would return from * the entry level activation. When the executor returns, a * single return value is left on the stack top. * * The only possible longjmp() is an error (DUK_LJ_TYPE_THROW), * other types are handled internally by the executor. */ /* thr->ptr_curr_pc is set by bytecode executor early on entry */ DUK_ASSERT(thr->ptr_curr_pc == NULL); DUK_DDD(DUK_DDDPRINT("entering bytecode execution")); duk_js_execute_bytecode(thr); DUK_DDD(DUK_DDDPRINT("returned from bytecode execution")); } else { /* * Native call. */ DUK_ASSERT(func == NULL || ((duk_hnatfunc *) func)->func != NULL); DUK_ASSERT(use_tailcall == 0); /* [ ... func this | arg1 ... argN ] ('this' must precede new bottom) */ /* duk_hthread_activation_unwind_norz() will decrease this on unwind */ DUK_ASSERT((act->flags & DUK_ACT_FLAG_PREVENT_YIELD) == 0); act->flags |= DUK_ACT_FLAG_PREVENT_YIELD; thr->callstack_preventcount++; /* XXX: we could just do this on entry regardless of reuse, as long * as recursion depth is decreased for e2e case. */ duk__call_c_recursion_limit_check(thr); thr->heap->call_recursion_depth++; /* For native calls must be NULL so we don't sync back */ DUK_ASSERT(thr->ptr_curr_pc == NULL); /* XXX: native funcptr could come out of call setup. */ if (func) { rc = ((duk_hnatfunc *) func)->func(thr); } else { duk_tval *tv_func; duk_c_function funcptr; tv_func = &act->tv_func; DUK_ASSERT(DUK_TVAL_IS_LIGHTFUNC(tv_func)); funcptr = DUK_TVAL_GET_LIGHTFUNC_FUNCPTR(tv_func); rc = funcptr(thr); } /* Automatic error throwing, retval check. */ if (rc == 0) { DUK_ASSERT(thr->valstack < thr->valstack_end); DUK_ASSERT(DUK_TVAL_IS_UNDEFINED(thr->valstack_top)); thr->valstack_top++; } else if (rc == 1) { ; } else if (rc < 0) { duk_error_throw_from_negative_rc(thr, rc); DUK_WO_NORETURN(return 0;); } else { DUK_ERROR_TYPE(thr, DUK_STR_INVALID_CFUNC_RC); DUK_WO_NORETURN(return 0;); } } DUK_ASSERT(thr->ptr_curr_pc == NULL); DUK_ASSERT(use_tailcall == 0); /* * Constructor call post processing. */ #if defined(DUK_USE_ES6_PROXY) if (call_flags & (DUK_CALL_FLAG_CONSTRUCT | DUK_CALL_FLAG_CONSTRUCT_PROXY)) { duk_call_construct_postprocess(thr, call_flags & DUK_CALL_FLAG_CONSTRUCT_PROXY); } #else if (call_flags & DUK_CALL_FLAG_CONSTRUCT) { duk_call_construct_postprocess(thr, 0); } #endif /* * Unwind, restore valstack bottom and other book-keeping. */ DUK_ASSERT(thr->callstack_curr != NULL); DUK_ASSERT(thr->callstack_curr->parent == entry_act); DUK_ASSERT(thr->callstack_top == entry_callstack_top + 1); duk_hthread_activation_unwind_norz(thr); DUK_ASSERT(thr->callstack_curr == entry_act); DUK_ASSERT(thr->callstack_top == entry_callstack_top); thr->valstack_bottom = (duk_tval *) (void *) ((duk_uint8_t *) thr->valstack + entry_valstack_bottom_byteoff); /* keep current valstack_top */ DUK_ASSERT(thr->valstack_bottom >= thr->valstack); DUK_ASSERT(thr->valstack_top >= thr->valstack_bottom); DUK_ASSERT(thr->valstack_end >= thr->valstack_top); DUK_ASSERT(thr->valstack_top - thr->valstack_bottom >= idx_func + 1); /* Return value handling. */ /* [ ... func this (crud) retval ] */ { duk_tval *tv_ret; duk_tval *tv_funret; tv_ret = thr->valstack_bottom + idx_func; tv_funret = thr->valstack_top - 1; #if defined(DUK_USE_FASTINT) /* Explicit check for fastint downgrade. */ DUK_TVAL_CHKFAST_INPLACE_FAST(tv_funret); #endif DUK_TVAL_SET_TVAL_UPDREF(thr, tv_ret, tv_funret); /* side effects */ } duk_set_top_unsafe(thr, idx_func + 1); /* [ ... retval ] */ /* Restore caller's value stack reserve (cannot fail). */ DUK_ASSERT((duk_uint8_t *) thr->valstack + entry_valstack_end_byteoff >= (duk_uint8_t *) thr->valstack_top); DUK_ASSERT((duk_uint8_t *) thr->valstack + entry_valstack_end_byteoff <= (duk_uint8_t *) thr->valstack_alloc_end); thr->valstack_end = (duk_tval *) (void *) ((duk_uint8_t *) thr->valstack + entry_valstack_end_byteoff); /* XXX: Trial value stack shrink would be OK here, but we'd need * to prevent side effects of the potential realloc. */ /* Restore entry thread executor curr_pc stack frame pointer. */ thr->ptr_curr_pc = entry_ptr_curr_pc; DUK_HEAP_SWITCH_THREAD(thr->heap, entry_curr_thread); /* may be NULL */ thr->state = (duk_uint8_t) entry_thread_state; /* Disabled assert: triggered with some torture tests. */ #if 0 DUK_ASSERT((thr->state == DUK_HTHREAD_STATE_INACTIVE && thr->heap->curr_thread == NULL) || /* first call */ (thr->state == DUK_HTHREAD_STATE_INACTIVE && thr->heap->curr_thread != NULL) || /* other call */ (thr->state == DUK_HTHREAD_STATE_RUNNING && thr->heap->curr_thread == thr)); /* current thread */ #endif thr->heap->call_recursion_depth = entry_call_recursion_depth; /* If the debugger is active we need to force an interrupt so that * debugger breakpoints are rechecked. This is important for function * calls caused by side effects (e.g. when doing a DUK_OP_GETPROP), see * GH-303. Only needed for success path, error path always causes a * breakpoint recheck in the executor. It would be enough to set this * only when returning to an ECMAScript activation, but setting the flag * on every return should have no ill effect. */ #if defined(DUK_USE_DEBUGGER_SUPPORT) if (duk_debug_is_attached(thr->heap)) { DUK_DD(DUK_DDPRINT("returning with debugger enabled, force interrupt")); DUK_ASSERT(thr->interrupt_counter <= thr->interrupt_init); thr->interrupt_init -= thr->interrupt_counter; thr->interrupt_counter = 0; thr->heap->dbg_force_restart = 1; } #endif #if defined(DUK_USE_INTERRUPT_COUNTER) && defined(DUK_USE_DEBUG) duk__interrupt_fixup(thr, entry_curr_thread); #endif /* Restored by success path. */ DUK_ASSERT(thr->heap->call_recursion_depth == entry_call_recursion_depth); DUK_ASSERT(thr->ptr_curr_pc == entry_ptr_curr_pc); DUK_ASSERT_LJSTATE_UNSET(thr->heap); DUK_REFZERO_CHECK_FAST(thr); return 0; /* 0=call handled inline */ } DUK_INTERNAL duk_int_t duk_handle_call_unprotected_nargs(duk_hthread *thr, duk_idx_t nargs, duk_small_uint_t call_flags) { duk_idx_t idx_func; DUK_ASSERT(duk_get_top(thr) >= nargs + 2); idx_func = duk_get_top(thr) - (nargs + 2); DUK_ASSERT(idx_func >= 0); return duk_handle_call_unprotected(thr, idx_func, call_flags); } DUK_INTERNAL duk_int_t duk_handle_call_unprotected(duk_hthread *thr, duk_idx_t idx_func, duk_small_uint_t call_flags) { DUK_ASSERT(duk_is_valid_index(thr, idx_func)); DUK_ASSERT(idx_func >= 0); return duk__handle_call_raw(thr, idx_func, call_flags); } /* * duk_handle_safe_call(): make a "C protected call" within the * current activation. * * The allowed thread states for making a call are the same as for * duk_handle_call_protected(). * * Even though this call is protected, errors are thrown for insane arguments * and may result in a fatal error unless there's another protected call which * catches such errors. * * The error handling path should be error free, even for out-of-memory * errors, to ensure safe sandboxing. (As of Duktape 2.2.0 this is not * yet the case for environment closing which may run out of memory, see * XXX notes below.) */ DUK_LOCAL void duk__handle_safe_call_inner(duk_hthread *thr, duk_safe_call_function func, void *udata, #if defined(DUK_USE_ASSERTIONS) duk_size_t entry_valstack_bottom_byteoff, duk_size_t entry_callstack_top, #endif duk_hthread *entry_curr_thread, duk_uint_fast8_t entry_thread_state, duk_idx_t idx_retbase, duk_idx_t num_stack_rets) { duk_ret_t rc; DUK_ASSERT(thr != NULL); DUK_ASSERT_CTX_VALID(thr); /* * Thread state check and book-keeping. */ duk__call_thread_state_update(thr); /* * Recursion limit check. */ duk__call_c_recursion_limit_check(thr); thr->heap->call_recursion_depth++; /* * Make the C call. */ rc = func(thr, udata); DUK_DDD(DUK_DDDPRINT("safe_call, func rc=%ld", (long) rc)); /* * Valstack manipulation for results. */ /* we're running inside the caller's activation, so no change in call/catch stack or valstack bottom */ DUK_ASSERT(thr->callstack_top == entry_callstack_top); DUK_ASSERT(thr->valstack_bottom >= thr->valstack); DUK_ASSERT((duk_size_t) ((duk_uint8_t *) thr->valstack_bottom - (duk_uint8_t *) thr->valstack) == entry_valstack_bottom_byteoff); DUK_ASSERT(thr->valstack_top >= thr->valstack_bottom); DUK_ASSERT(thr->valstack_end >= thr->valstack_top); if (DUK_UNLIKELY(rc < 0)) { duk_error_throw_from_negative_rc(thr, rc); DUK_WO_NORETURN(return;); } DUK_ASSERT(rc >= 0); duk__safe_call_adjust_valstack(thr, idx_retbase, num_stack_rets, rc); /* throws for insane rc */ DUK_HEAP_SWITCH_THREAD(thr->heap, entry_curr_thread); /* may be NULL */ thr->state = (duk_uint8_t) entry_thread_state; } DUK_LOCAL void duk__handle_safe_call_error(duk_hthread *thr, duk_activation *entry_act, #if defined(DUK_USE_ASSERTIONS) duk_size_t entry_callstack_top, #endif duk_hthread *entry_curr_thread, duk_uint_fast8_t entry_thread_state, duk_idx_t idx_retbase, duk_idx_t num_stack_rets, duk_size_t entry_valstack_bottom_byteoff, duk_jmpbuf *old_jmpbuf_ptr) { DUK_ASSERT(thr != NULL); DUK_ASSERT_CTX_VALID(thr); /* * Error during call. The error value is at heap->lj.value1. * * The very first thing we do is restore the previous setjmp catcher. * This means that any error in error handling will propagate outwards * instead of causing a setjmp() re-entry above. */ DUK_DDD(DUK_DDDPRINT("error caught during protected duk_handle_safe_call()")); /* Other longjmp types are handled by executor before propagating * the error here. */ DUK_ASSERT(thr->heap->lj.type == DUK_LJ_TYPE_THROW); DUK_ASSERT_LJSTATE_SET(thr->heap); /* Either pointer may be NULL (at entry), so don't assert. */ thr->heap->lj.jmpbuf_ptr = old_jmpbuf_ptr; /* XXX: callstack unwind may now throw an error when closing * scopes; this is a sandboxing issue, described in: * https://github.com/svaarala/duktape/issues/476 */ /* XXX: "unwind to" primitive? */ DUK_ASSERT(thr->callstack_top >= entry_callstack_top); while (thr->callstack_curr != entry_act) { DUK_ASSERT(thr->callstack_curr != NULL); duk_hthread_activation_unwind_norz(thr); } DUK_ASSERT(thr->callstack_top == entry_callstack_top); /* Switch active thread before any side effects to avoid a * dangling curr_thread pointer. */ DUK_HEAP_SWITCH_THREAD(thr->heap, entry_curr_thread); /* may be NULL */ thr->state = (duk_uint8_t) entry_thread_state; DUK_ASSERT(thr->heap->curr_thread == entry_curr_thread); DUK_ASSERT(thr->state == entry_thread_state); /* Restore valstack bottom. */ thr->valstack_bottom = (duk_tval *) (void *) ((duk_uint8_t *) thr->valstack + entry_valstack_bottom_byteoff); /* [ ... | (crud) ] */ /* XXX: ensure space in valstack (now relies on internal reserve)? */ duk_push_tval(thr, &thr->heap->lj.value1); /* [ ... | (crud) errobj ] */ DUK_ASSERT(duk_get_top(thr) >= 1); /* at least errobj must be on stack */ duk__safe_call_adjust_valstack(thr, idx_retbase, num_stack_rets, 1); /* 1 = num actual 'return values' */ /* [ ... | ] or [ ... | errobj (M * undefined)] where M = num_stack_rets - 1 */ /* Reset longjmp state. */ thr->heap->lj.type = DUK_LJ_TYPE_UNKNOWN; thr->heap->lj.iserror = 0; DUK_TVAL_SET_UNDEFINED_UPDREF_NORZ(thr, &thr->heap->lj.value1); DUK_TVAL_SET_UNDEFINED_UPDREF_NORZ(thr, &thr->heap->lj.value2); /* Error handling complete, remove side effect protections. Caller * will process pending finalizers. */ #if defined(DUK_USE_ASSERTIONS) DUK_ASSERT(thr->heap->error_not_allowed == 1); thr->heap->error_not_allowed = 0; #endif DUK_ASSERT(thr->heap->pf_prevent_count > 0); thr->heap->pf_prevent_count--; DUK_DD(DUK_DDPRINT("safe call error handled, pf_prevent_count updated to %ld", (long) thr->heap->pf_prevent_count)); /* thr->ptr_curr_pc is restored by * duk__handle_safe_call_shared_unwind() which is also used for * success path. */ } DUK_LOCAL void duk__handle_safe_call_shared_unwind(duk_hthread *thr, duk_idx_t idx_retbase, duk_idx_t num_stack_rets, #if defined(DUK_USE_ASSERTIONS) duk_size_t entry_callstack_top, #endif duk_int_t entry_call_recursion_depth, duk_hthread *entry_curr_thread, duk_instr_t **entry_ptr_curr_pc) { DUK_ASSERT(thr != NULL); DUK_ASSERT_CTX_VALID(thr); DUK_UNREF(idx_retbase); DUK_UNREF(num_stack_rets); DUK_UNREF(entry_curr_thread); DUK_ASSERT(thr->callstack_top == entry_callstack_top); /* Restore entry thread executor curr_pc stack frame pointer. * XXX: would be enough to do in error path only, should nest * cleanly in success path. */ thr->ptr_curr_pc = entry_ptr_curr_pc; thr->heap->call_recursion_depth = entry_call_recursion_depth; /* stack discipline consistency check */ DUK_ASSERT(duk_get_top(thr) == idx_retbase + num_stack_rets); /* A debugger forced interrupt check is not needed here, as * problematic safe calls are not caused by side effects. */ #if defined(DUK_USE_INTERRUPT_COUNTER) && defined(DUK_USE_DEBUG) duk__interrupt_fixup(thr, entry_curr_thread); #endif } DUK_INTERNAL duk_int_t duk_handle_safe_call(duk_hthread *thr, duk_safe_call_function func, void *udata, duk_idx_t num_stack_args, duk_idx_t num_stack_rets) { duk_activation *entry_act; duk_size_t entry_valstack_bottom_byteoff; #if defined(DUK_USE_ASSERTIONS) duk_size_t entry_valstack_end_byteoff; duk_size_t entry_callstack_top; duk_size_t entry_callstack_preventcount; #endif duk_int_t entry_call_recursion_depth; duk_hthread *entry_curr_thread; duk_uint_fast8_t entry_thread_state; duk_instr_t **entry_ptr_curr_pc; duk_jmpbuf *old_jmpbuf_ptr = NULL; duk_jmpbuf our_jmpbuf; duk_idx_t idx_retbase; duk_int_t retval; DUK_ASSERT(thr != NULL); DUK_ASSERT(duk_get_top(thr) >= num_stack_args); /* Caller ensures. */ DUK_STATS_INC(thr->heap, stats_safecall_all); /* Value stack reserve handling: safe call assumes caller has reserved * space for nrets (assuming optimal unwind processing). Value stack * reserve is not stored/restored as for normal calls because a safe * call conceptually happens in the same activation. */ /* Careful with indices like '-x'; if 'x' is zero, it refers to bottom */ entry_act = thr->callstack_curr; entry_valstack_bottom_byteoff = (duk_size_t) ((duk_uint8_t *) thr->valstack_bottom - (duk_uint8_t *) thr->valstack); #if defined(DUK_USE_ASSERTIONS) entry_valstack_end_byteoff = (duk_size_t) ((duk_uint8_t *) thr->valstack_end - (duk_uint8_t *) thr->valstack); entry_callstack_top = thr->callstack_top; entry_callstack_preventcount = thr->callstack_preventcount; #endif entry_call_recursion_depth = thr->heap->call_recursion_depth; entry_curr_thread = thr->heap->curr_thread; /* may be NULL if first call */ entry_thread_state = thr->state; entry_ptr_curr_pc = thr->ptr_curr_pc; /* may be NULL */ idx_retbase = duk_get_top(thr) - num_stack_args; /* not a valid stack index if num_stack_args == 0 */ DUK_ASSERT(idx_retbase >= 0); DUK_ASSERT((duk_idx_t) (thr->valstack_top - thr->valstack_bottom) >= num_stack_args); /* Caller ensures. */ DUK_ASSERT((duk_idx_t) (thr->valstack_end - (thr->valstack_bottom + idx_retbase)) >= num_stack_rets); /* Caller ensures. */ /* Cannot portably debug print a function pointer, hence 'func' not printed! */ DUK_DD(DUK_DDPRINT("duk_handle_safe_call: thr=%p, num_stack_args=%ld, num_stack_rets=%ld, " "valstack_top=%ld, idx_retbase=%ld, rec_depth=%ld/%ld, " "entry_act=%p, entry_valstack_bottom_byteoff=%ld, entry_call_recursion_depth=%ld, " "entry_curr_thread=%p, entry_thread_state=%ld", (void *) thr, (long) num_stack_args, (long) num_stack_rets, (long) duk_get_top(thr), (long) idx_retbase, (long) thr->heap->call_recursion_depth, (long) thr->heap->call_recursion_limit, (void *) entry_act, (long) entry_valstack_bottom_byteoff, (long) entry_call_recursion_depth, (void *) entry_curr_thread, (long) entry_thread_state)); /* Setjmp catchpoint setup. */ old_jmpbuf_ptr = thr->heap->lj.jmpbuf_ptr; thr->heap->lj.jmpbuf_ptr = &our_jmpbuf; /* Prevent yields for the duration of the safe call. This only * matters if the executor makes safe calls to functions that * yield, this doesn't currently happen. */ thr->callstack_preventcount++; #if defined(DUK_USE_CPP_EXCEPTIONS) try { #else DUK_ASSERT(thr->heap->lj.jmpbuf_ptr == &our_jmpbuf); if (DUK_SETJMP(our_jmpbuf.jb) == 0) { /* Success path. */ #endif DUK_DDD(DUK_DDDPRINT("safe_call setjmp catchpoint setup complete")); duk__handle_safe_call_inner(thr, func, udata, #if defined(DUK_USE_ASSERTIONS) entry_valstack_bottom_byteoff, entry_callstack_top, #endif entry_curr_thread, entry_thread_state, idx_retbase, num_stack_rets); DUK_STATS_INC(thr->heap, stats_safecall_nothrow); /* Either pointer may be NULL (at entry), so don't assert */ thr->heap->lj.jmpbuf_ptr = old_jmpbuf_ptr; /* If calls happen inside the safe call, these are restored by * whatever calls are made. Reserve cannot decrease. */ DUK_ASSERT(thr->callstack_curr == entry_act); DUK_ASSERT((duk_size_t) ((duk_uint8_t *) thr->valstack_end - (duk_uint8_t *) thr->valstack) >= entry_valstack_end_byteoff); retval = DUK_EXEC_SUCCESS; #if defined(DUK_USE_CPP_EXCEPTIONS) } catch (duk_internal_exception &exc) { DUK_UNREF(exc); #else } else { /* Error path. */ #endif DUK_ASSERT((duk_size_t) ((duk_uint8_t *) thr->valstack_end - (duk_uint8_t *) thr->valstack) >= entry_valstack_end_byteoff); DUK_STATS_INC(thr->heap, stats_safecall_throw); duk__handle_safe_call_error(thr, entry_act, #if defined(DUK_USE_ASSERTIONS) entry_callstack_top, #endif entry_curr_thread, entry_thread_state, idx_retbase, num_stack_rets, entry_valstack_bottom_byteoff, old_jmpbuf_ptr); retval = DUK_EXEC_ERROR; } #if defined(DUK_USE_CPP_EXCEPTIONS) catch (duk_fatal_exception &exc) { DUK_D(DUK_DPRINT("rethrow duk_fatal_exception")); throw; } catch (std::exception &exc) { const char *what = exc.what(); DUK_ASSERT((duk_size_t) ((duk_uint8_t *) thr->valstack_end - (duk_uint8_t *) thr->valstack) >= entry_valstack_end_byteoff); DUK_STATS_INC(thr->heap, stats_safecall_throw); if (!what) { what = "unknown"; } DUK_D(DUK_DPRINT("unexpected c++ std::exception (perhaps thrown by user code)")); try { DUK_ERROR_FMT1(thr, DUK_ERR_TYPE_ERROR, "caught invalid c++ std::exception '%s' (perhaps thrown by user code)", what); DUK_WO_NORETURN(return 0;); } catch (duk_internal_exception exc) { DUK_D(DUK_DPRINT("caught api error thrown from unexpected c++ std::exception")); DUK_UNREF(exc); duk__handle_safe_call_error(thr, entry_act, #if defined(DUK_USE_ASSERTIONS) entry_callstack_top, #endif entry_curr_thread, entry_thread_state, idx_retbase, num_stack_rets, entry_valstack_bottom_byteoff, old_jmpbuf_ptr); retval = DUK_EXEC_ERROR; } } catch (...) { DUK_D(DUK_DPRINT("unexpected c++ exception (perhaps thrown by user code)")); DUK_ASSERT((duk_size_t) ((duk_uint8_t *) thr->valstack_end - (duk_uint8_t *) thr->valstack) >= entry_valstack_end_byteoff); DUK_STATS_INC(thr->heap, stats_safecall_throw); try { DUK_ERROR_TYPE(thr, "caught invalid c++ exception (perhaps thrown by user code)"); DUK_WO_NORETURN(return 0;); } catch (duk_internal_exception exc) { DUK_D(DUK_DPRINT("caught api error thrown from unexpected c++ exception")); DUK_UNREF(exc); duk__handle_safe_call_error(thr, entry_act, #if defined(DUK_USE_ASSERTIONS) entry_callstack_top, #endif entry_curr_thread, entry_thread_state, idx_retbase, num_stack_rets, entry_valstack_bottom_byteoff, old_jmpbuf_ptr); retval = DUK_EXEC_ERROR; } } #endif DUK_ASSERT(thr->heap->lj.jmpbuf_ptr == old_jmpbuf_ptr); /* success/error path both do this */ DUK_ASSERT_LJSTATE_UNSET(thr->heap); DUK_ASSERT((duk_size_t) ((duk_uint8_t *) thr->valstack_end - (duk_uint8_t *) thr->valstack) >= entry_valstack_end_byteoff); duk__handle_safe_call_shared_unwind(thr, idx_retbase, num_stack_rets, #if defined(DUK_USE_ASSERTIONS) entry_callstack_top, #endif entry_call_recursion_depth, entry_curr_thread, entry_ptr_curr_pc); /* Restore preventcount. */ thr->callstack_preventcount--; DUK_ASSERT(thr->callstack_preventcount == entry_callstack_preventcount); /* Final asserts. */ DUK_ASSERT(thr->callstack_curr == entry_act); DUK_ASSERT((duk_size_t) ((duk_uint8_t *) thr->valstack_bottom - (duk_uint8_t *) thr->valstack) == entry_valstack_bottom_byteoff); DUK_ASSERT((duk_size_t) ((duk_uint8_t *) thr->valstack_end - (duk_uint8_t *) thr->valstack) >= entry_valstack_end_byteoff); DUK_ASSERT(thr->callstack_top == entry_callstack_top); DUK_ASSERT(thr->heap->call_recursion_depth == entry_call_recursion_depth); DUK_ASSERT(thr->heap->curr_thread == entry_curr_thread); DUK_ASSERT(thr->state == entry_thread_state); DUK_ASSERT(thr->ptr_curr_pc == entry_ptr_curr_pc); DUK_ASSERT(duk_get_top(thr) == idx_retbase + num_stack_rets); DUK_ASSERT_LJSTATE_UNSET(thr->heap); /* Pending side effects. */ DUK_REFZERO_CHECK_FAST(thr); return retval; } /* * Property-based call (foo.noSuch()) error setup: replace target function * on stack top with a specially tagged (hidden Symbol) error which gets * thrown in call handling at the proper spot to follow ECMAScript semantics. */ #if defined(DUK_USE_VERBOSE_ERRORS) DUK_INTERNAL DUK_NOINLINE DUK_COLD void duk_call_setup_propcall_error(duk_hthread *thr, duk_tval *tv_targ, duk_tval *tv_base, duk_tval *tv_key) { const char *str1, *str2, *str3; duk_idx_t entry_top; entry_top = duk_get_top(thr); /* Must stabilize pointers first. Argument convention is a bit awkward, * it comes from the executor call site where some arguments may not be * on the value stack (consts). */ duk_push_tval(thr, tv_base); duk_push_tval(thr, tv_key); duk_push_tval(thr, tv_targ); DUK_GC_TORTURE(thr->heap); /* We only push an error, replacing the call target (at idx_func) * with the error to ensure side effects come out correctly: * - Property read * - Call argument evaluation * - Callability check and error thrown. * * A hidden Symbol on the error object pushed here is used by * call handling to figure out the error is to be thrown as is. * It is CRITICAL that the hidden Symbol can never occur on a * user visible object that may get thrown. */ #if defined(DUK_USE_PARANOID_ERRORS) str1 = duk_get_type_name(thr, -1); str2 = duk_get_type_name(thr, -2); str3 = duk_get_type_name(thr, -3); duk_push_error_object(thr, DUK_ERR_TYPE_ERROR | DUK_ERRCODE_FLAG_NOBLAME_FILELINE, "%s not callable (property %s of %s)", str1, str2, str3); #else str1 = duk_push_string_readable(thr, -1); str2 = duk_push_string_readable(thr, -3); str3 = duk_push_string_readable(thr, -5); duk_push_error_object(thr, DUK_ERR_TYPE_ERROR | DUK_ERRCODE_FLAG_NOBLAME_FILELINE, "%s not callable (property %s of %s)", str1, str2, str3); #endif duk_push_true(thr); duk_put_prop_stridx(thr, -2, DUK_STRIDX_INT_TARGET); /* Marker property, reuse _Target. */ /* [ propValue error ] */ duk_replace(thr, entry_top - 1); duk_set_top(thr, entry_top); DUK_ASSERT(!duk_is_callable(thr, -1)); /* Critical so that call handling will throw the error. */ } #endif /* DUK_USE_VERBOSE_ERRORS */