fixed 'return' handling with 'yield' in 'for of' or with finally blocks (gihub ticket #166)
This commit is contained in:
parent
57105c7f23
commit
4bb8c35da7
3 changed files with 139 additions and 73 deletions
|
@ -182,6 +182,7 @@ DEF( goto, 5, 0, 0, label) /* must come after if_true */
|
||||||
DEF( catch, 5, 0, 1, label)
|
DEF( catch, 5, 0, 1, label)
|
||||||
DEF( gosub, 5, 0, 0, label) /* used to execute the finally block */
|
DEF( gosub, 5, 0, 0, label) /* used to execute the finally block */
|
||||||
DEF( ret, 1, 1, 0, none) /* used to return from the finally block */
|
DEF( ret, 1, 1, 0, none) /* used to return from the finally block */
|
||||||
|
DEF( nip_catch, 1, 2, 1, none) /* catch ... a -> a */
|
||||||
|
|
||||||
DEF( to_object, 1, 1, 1, none)
|
DEF( to_object, 1, 1, 1, none)
|
||||||
//DEF( to_string, 1, 1, 1, none)
|
//DEF( to_string, 1, 1, 1, none)
|
||||||
|
@ -208,7 +209,6 @@ DEF( for_of_next, 2, 3, 5, u8)
|
||||||
DEF(iterator_check_object, 1, 1, 1, none)
|
DEF(iterator_check_object, 1, 1, 1, none)
|
||||||
DEF(iterator_get_value_done, 1, 1, 2, none)
|
DEF(iterator_get_value_done, 1, 1, 2, none)
|
||||||
DEF( iterator_close, 1, 3, 0, none)
|
DEF( iterator_close, 1, 3, 0, none)
|
||||||
DEF(iterator_close_return, 1, 4, 4, none)
|
|
||||||
DEF( iterator_next, 1, 4, 4, none)
|
DEF( iterator_next, 1, 4, 4, none)
|
||||||
DEF( iterator_call, 2, 4, 5, u8)
|
DEF( iterator_call, 2, 4, 5, u8)
|
||||||
DEF( initial_yield, 1, 0, 0, none)
|
DEF( initial_yield, 1, 0, 0, none)
|
||||||
|
|
152
quickjs.c
152
quickjs.c
|
@ -87,6 +87,7 @@
|
||||||
8: dump stdlib functions
|
8: dump stdlib functions
|
||||||
16: dump bytecode in hex
|
16: dump bytecode in hex
|
||||||
32: dump line number table
|
32: dump line number table
|
||||||
|
64: dump compute_stack_size
|
||||||
*/
|
*/
|
||||||
//#define DUMP_BYTECODE (1)
|
//#define DUMP_BYTECODE (1)
|
||||||
/* dump the occurence of the automatic GC */
|
/* dump the occurence of the automatic GC */
|
||||||
|
@ -17091,26 +17092,21 @@ static JSValue JS_CallInternal(JSContext *caller_ctx, JSValueConst func_obj,
|
||||||
}
|
}
|
||||||
sp--;
|
sp--;
|
||||||
BREAK;
|
BREAK;
|
||||||
CASE(OP_iterator_close_return):
|
CASE(OP_nip_catch):
|
||||||
{
|
{
|
||||||
JSValue ret_val;
|
JSValue ret_val;
|
||||||
/* iter_obj next catch_offset ... ret_val ->
|
/* catch_offset ... ret_val -> ret_eval */
|
||||||
ret_eval iter_obj next catch_offset */
|
|
||||||
ret_val = *--sp;
|
ret_val = *--sp;
|
||||||
while (sp > stack_buf &&
|
while (sp > stack_buf &&
|
||||||
JS_VALUE_GET_TAG(sp[-1]) != JS_TAG_CATCH_OFFSET) {
|
JS_VALUE_GET_TAG(sp[-1]) != JS_TAG_CATCH_OFFSET) {
|
||||||
JS_FreeValue(ctx, *--sp);
|
JS_FreeValue(ctx, *--sp);
|
||||||
}
|
}
|
||||||
if (unlikely(sp < stack_buf + 3)) {
|
if (unlikely(sp == stack_buf)) {
|
||||||
JS_ThrowInternalError(ctx, "iterator_close_return");
|
JS_ThrowInternalError(ctx, "nip_catch");
|
||||||
JS_FreeValue(ctx, ret_val);
|
JS_FreeValue(ctx, ret_val);
|
||||||
goto exception;
|
goto exception;
|
||||||
}
|
}
|
||||||
sp[0] = sp[-1];
|
sp[-1] = ret_val;
|
||||||
sp[-1] = sp[-2];
|
|
||||||
sp[-2] = sp[-3];
|
|
||||||
sp[-3] = ret_val;
|
|
||||||
sp++;
|
|
||||||
}
|
}
|
||||||
BREAK;
|
BREAK;
|
||||||
|
|
||||||
|
@ -25438,7 +25434,6 @@ static __exception int emit_break(JSParseState *s, JSAtom name, int is_cont)
|
||||||
static void emit_return(JSParseState *s, BOOL hasval)
|
static void emit_return(JSParseState *s, BOOL hasval)
|
||||||
{
|
{
|
||||||
BlockEnv *top;
|
BlockEnv *top;
|
||||||
int drop_count;
|
|
||||||
|
|
||||||
if (s->cur_func->func_kind != JS_FUNC_NORMAL) {
|
if (s->cur_func->func_kind != JS_FUNC_NORMAL) {
|
||||||
if (!hasval) {
|
if (!hasval) {
|
||||||
|
@ -25452,26 +25447,24 @@ static void emit_return(JSParseState *s, BOOL hasval)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
drop_count = 0;
|
|
||||||
top = s->cur_func->top_break;
|
top = s->cur_func->top_break;
|
||||||
while (top != NULL) {
|
while (top != NULL) {
|
||||||
/* XXX: emit the appropriate OP_leave_scope opcodes? Probably not
|
if (top->has_iterator || top->label_finally != -1) {
|
||||||
required as all local variables will be closed upon returning
|
|
||||||
from JS_CallInternal, but not in the same order. */
|
|
||||||
if (top->has_iterator) {
|
|
||||||
/* with 'yield', the exact number of OP_drop to emit is
|
|
||||||
unknown, so we use a specific operation to look for
|
|
||||||
the catch offset */
|
|
||||||
if (!hasval) {
|
if (!hasval) {
|
||||||
emit_op(s, OP_undefined);
|
emit_op(s, OP_undefined);
|
||||||
hasval = TRUE;
|
hasval = TRUE;
|
||||||
}
|
}
|
||||||
emit_op(s, OP_iterator_close_return);
|
/* Remove the stack elements up to and including the catch
|
||||||
|
offset. When 'yield' is used in an expression we have
|
||||||
|
no easy way to count them, so we use this specific
|
||||||
|
instruction instead. */
|
||||||
|
emit_op(s, OP_nip_catch);
|
||||||
|
/* stack: iter_obj next ret_val */
|
||||||
|
if (top->has_iterator) {
|
||||||
if (s->cur_func->func_kind == JS_FUNC_ASYNC_GENERATOR) {
|
if (s->cur_func->func_kind == JS_FUNC_ASYNC_GENERATOR) {
|
||||||
int label_next, label_next2;
|
int label_next, label_next2;
|
||||||
|
emit_op(s, OP_nip); /* next */
|
||||||
emit_op(s, OP_drop); /* catch offset */
|
emit_op(s, OP_swap);
|
||||||
emit_op(s, OP_drop); /* next */
|
|
||||||
emit_op(s, OP_get_field2);
|
emit_op(s, OP_get_field2);
|
||||||
emit_atom(s, JS_ATOM_return);
|
emit_atom(s, JS_ATOM_return);
|
||||||
/* stack: iter_obj return_func */
|
/* stack: iter_obj return_func */
|
||||||
|
@ -25488,24 +25481,15 @@ static void emit_return(JSParseState *s, BOOL hasval)
|
||||||
emit_label(s, label_next2);
|
emit_label(s, label_next2);
|
||||||
emit_op(s, OP_drop);
|
emit_op(s, OP_drop);
|
||||||
} else {
|
} else {
|
||||||
|
emit_op(s, OP_rot3r);
|
||||||
|
emit_op(s, OP_undefined); /* dummy catch offset */
|
||||||
emit_op(s, OP_iterator_close);
|
emit_op(s, OP_iterator_close);
|
||||||
}
|
}
|
||||||
drop_count = -3;
|
} else {
|
||||||
}
|
/* execute the "finally" block */
|
||||||
drop_count += top->drop_count;
|
|
||||||
if (top->label_finally != -1) {
|
|
||||||
while(drop_count) {
|
|
||||||
/* must keep the stack top if hasval */
|
|
||||||
emit_op(s, hasval ? OP_nip : OP_drop);
|
|
||||||
drop_count--;
|
|
||||||
}
|
|
||||||
if (!hasval) {
|
|
||||||
/* must push return value to keep same stack size */
|
|
||||||
emit_op(s, OP_undefined);
|
|
||||||
hasval = TRUE;
|
|
||||||
}
|
|
||||||
emit_goto(s, OP_gosub, top->label_finally);
|
emit_goto(s, OP_gosub, top->label_finally);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
top = top->prev;
|
top = top->prev;
|
||||||
}
|
}
|
||||||
if (s->cur_func->is_derived_class_constructor) {
|
if (s->cur_func->is_derived_class_constructor) {
|
||||||
|
@ -31901,6 +31885,7 @@ typedef struct StackSizeState {
|
||||||
int bc_len;
|
int bc_len;
|
||||||
int stack_len_max;
|
int stack_len_max;
|
||||||
uint16_t *stack_level_tab;
|
uint16_t *stack_level_tab;
|
||||||
|
int32_t *catch_pos_tab;
|
||||||
int *pc_stack;
|
int *pc_stack;
|
||||||
int pc_stack_len;
|
int pc_stack_len;
|
||||||
int pc_stack_size;
|
int pc_stack_size;
|
||||||
|
@ -31908,7 +31893,7 @@ typedef struct StackSizeState {
|
||||||
|
|
||||||
/* 'op' is only used for error indication */
|
/* 'op' is only used for error indication */
|
||||||
static __exception int ss_check(JSContext *ctx, StackSizeState *s,
|
static __exception int ss_check(JSContext *ctx, StackSizeState *s,
|
||||||
int pos, int op, int stack_len)
|
int pos, int op, int stack_len, int catch_pos)
|
||||||
{
|
{
|
||||||
if ((unsigned)pos >= s->bc_len) {
|
if ((unsigned)pos >= s->bc_len) {
|
||||||
JS_ThrowInternalError(ctx, "bytecode buffer overflow (op=%d, pc=%d)", op, pos);
|
JS_ThrowInternalError(ctx, "bytecode buffer overflow (op=%d, pc=%d)", op, pos);
|
||||||
|
@ -31927,6 +31912,10 @@ static __exception int ss_check(JSContext *ctx, StackSizeState *s,
|
||||||
JS_ThrowInternalError(ctx, "inconsistent stack size: %d %d (pc=%d)",
|
JS_ThrowInternalError(ctx, "inconsistent stack size: %d %d (pc=%d)",
|
||||||
s->stack_level_tab[pos], stack_len, pos);
|
s->stack_level_tab[pos], stack_len, pos);
|
||||||
return -1;
|
return -1;
|
||||||
|
} else if (s->catch_pos_tab[pos] != catch_pos) {
|
||||||
|
JS_ThrowInternalError(ctx, "inconsistent catch position: %d %d (pc=%d)",
|
||||||
|
s->catch_pos_tab[pos], catch_pos, pos);
|
||||||
|
return -1;
|
||||||
} else {
|
} else {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
@ -31934,6 +31923,7 @@ static __exception int ss_check(JSContext *ctx, StackSizeState *s,
|
||||||
|
|
||||||
/* mark as explored and store the stack size */
|
/* mark as explored and store the stack size */
|
||||||
s->stack_level_tab[pos] = stack_len;
|
s->stack_level_tab[pos] = stack_len;
|
||||||
|
s->catch_pos_tab[pos] = catch_pos;
|
||||||
|
|
||||||
/* queue the new PC to explore */
|
/* queue the new PC to explore */
|
||||||
if (js_resize_array(ctx, (void **)&s->pc_stack, sizeof(s->pc_stack[0]),
|
if (js_resize_array(ctx, (void **)&s->pc_stack, sizeof(s->pc_stack[0]),
|
||||||
|
@ -31948,7 +31938,7 @@ static __exception int compute_stack_size(JSContext *ctx,
|
||||||
int *pstack_size)
|
int *pstack_size)
|
||||||
{
|
{
|
||||||
StackSizeState s_s, *s = &s_s;
|
StackSizeState s_s, *s = &s_s;
|
||||||
int i, diff, n_pop, pos_next, stack_len, pos, op;
|
int i, diff, n_pop, pos_next, stack_len, pos, op, catch_pos, catch_level;
|
||||||
const JSOpCode *oi;
|
const JSOpCode *oi;
|
||||||
const uint8_t *bc_buf;
|
const uint8_t *bc_buf;
|
||||||
|
|
||||||
|
@ -31961,24 +31951,33 @@ static __exception int compute_stack_size(JSContext *ctx,
|
||||||
return -1;
|
return -1;
|
||||||
for(i = 0; i < s->bc_len; i++)
|
for(i = 0; i < s->bc_len; i++)
|
||||||
s->stack_level_tab[i] = 0xffff;
|
s->stack_level_tab[i] = 0xffff;
|
||||||
s->stack_len_max = 0;
|
|
||||||
s->pc_stack = NULL;
|
s->pc_stack = NULL;
|
||||||
|
s->catch_pos_tab = js_malloc(ctx, sizeof(s->catch_pos_tab[0]) *
|
||||||
|
s->bc_len);
|
||||||
|
if (!s->catch_pos_tab)
|
||||||
|
goto fail;
|
||||||
|
|
||||||
|
s->stack_len_max = 0;
|
||||||
s->pc_stack_len = 0;
|
s->pc_stack_len = 0;
|
||||||
s->pc_stack_size = 0;
|
s->pc_stack_size = 0;
|
||||||
|
|
||||||
/* breadth-first graph exploration */
|
/* breadth-first graph exploration */
|
||||||
if (ss_check(ctx, s, 0, OP_invalid, 0))
|
if (ss_check(ctx, s, 0, OP_invalid, 0, -1))
|
||||||
goto fail;
|
goto fail;
|
||||||
|
|
||||||
while (s->pc_stack_len > 0) {
|
while (s->pc_stack_len > 0) {
|
||||||
pos = s->pc_stack[--s->pc_stack_len];
|
pos = s->pc_stack[--s->pc_stack_len];
|
||||||
stack_len = s->stack_level_tab[pos];
|
stack_len = s->stack_level_tab[pos];
|
||||||
|
catch_pos = s->catch_pos_tab[pos];
|
||||||
op = bc_buf[pos];
|
op = bc_buf[pos];
|
||||||
if (op == 0 || op >= OP_COUNT) {
|
if (op == 0 || op >= OP_COUNT) {
|
||||||
JS_ThrowInternalError(ctx, "invalid opcode (op=%d, pc=%d)", op, pos);
|
JS_ThrowInternalError(ctx, "invalid opcode (op=%d, pc=%d)", op, pos);
|
||||||
goto fail;
|
goto fail;
|
||||||
}
|
}
|
||||||
oi = &short_opcode_info(op);
|
oi = &short_opcode_info(op);
|
||||||
|
#if defined(DUMP_BYTECODE) && (DUMP_BYTECODE & 64)
|
||||||
|
printf("%5d: %10s %5d %5d\n", pos, oi->name, stack_len, catch_pos);
|
||||||
|
#endif
|
||||||
pos_next = pos + oi->size;
|
pos_next = pos + oi->size;
|
||||||
if (pos_next > s->bc_len) {
|
if (pos_next > s->bc_len) {
|
||||||
JS_ThrowInternalError(ctx, "bytecode buffer overflow (op=%d, pc=%d)", op, pos);
|
JS_ThrowInternalError(ctx, "bytecode buffer overflow (op=%d, pc=%d)", op, pos);
|
||||||
|
@ -32034,55 +32033,104 @@ static __exception int compute_stack_size(JSContext *ctx,
|
||||||
case OP_if_true8:
|
case OP_if_true8:
|
||||||
case OP_if_false8:
|
case OP_if_false8:
|
||||||
diff = (int8_t)bc_buf[pos + 1];
|
diff = (int8_t)bc_buf[pos + 1];
|
||||||
if (ss_check(ctx, s, pos + 1 + diff, op, stack_len))
|
if (ss_check(ctx, s, pos + 1 + diff, op, stack_len, catch_pos))
|
||||||
goto fail;
|
goto fail;
|
||||||
break;
|
break;
|
||||||
#endif
|
#endif
|
||||||
case OP_if_true:
|
case OP_if_true:
|
||||||
case OP_if_false:
|
case OP_if_false:
|
||||||
case OP_catch:
|
|
||||||
diff = get_u32(bc_buf + pos + 1);
|
diff = get_u32(bc_buf + pos + 1);
|
||||||
if (ss_check(ctx, s, pos + 1 + diff, op, stack_len))
|
if (ss_check(ctx, s, pos + 1 + diff, op, stack_len, catch_pos))
|
||||||
goto fail;
|
goto fail;
|
||||||
break;
|
break;
|
||||||
case OP_gosub:
|
case OP_gosub:
|
||||||
diff = get_u32(bc_buf + pos + 1);
|
diff = get_u32(bc_buf + pos + 1);
|
||||||
if (ss_check(ctx, s, pos + 1 + diff, op, stack_len + 1))
|
if (ss_check(ctx, s, pos + 1 + diff, op, stack_len + 1, catch_pos))
|
||||||
goto fail;
|
goto fail;
|
||||||
break;
|
break;
|
||||||
case OP_with_get_var:
|
case OP_with_get_var:
|
||||||
case OP_with_delete_var:
|
case OP_with_delete_var:
|
||||||
diff = get_u32(bc_buf + pos + 5);
|
diff = get_u32(bc_buf + pos + 5);
|
||||||
if (ss_check(ctx, s, pos + 5 + diff, op, stack_len + 1))
|
if (ss_check(ctx, s, pos + 5 + diff, op, stack_len + 1, catch_pos))
|
||||||
goto fail;
|
goto fail;
|
||||||
break;
|
break;
|
||||||
case OP_with_make_ref:
|
case OP_with_make_ref:
|
||||||
case OP_with_get_ref:
|
case OP_with_get_ref:
|
||||||
case OP_with_get_ref_undef:
|
case OP_with_get_ref_undef:
|
||||||
diff = get_u32(bc_buf + pos + 5);
|
diff = get_u32(bc_buf + pos + 5);
|
||||||
if (ss_check(ctx, s, pos + 5 + diff, op, stack_len + 2))
|
if (ss_check(ctx, s, pos + 5 + diff, op, stack_len + 2, catch_pos))
|
||||||
goto fail;
|
goto fail;
|
||||||
break;
|
break;
|
||||||
case OP_with_put_var:
|
case OP_with_put_var:
|
||||||
diff = get_u32(bc_buf + pos + 5);
|
diff = get_u32(bc_buf + pos + 5);
|
||||||
if (ss_check(ctx, s, pos + 5 + diff, op, stack_len - 1))
|
if (ss_check(ctx, s, pos + 5 + diff, op, stack_len - 1, catch_pos))
|
||||||
goto fail;
|
goto fail;
|
||||||
break;
|
break;
|
||||||
|
case OP_catch:
|
||||||
|
diff = get_u32(bc_buf + pos + 1);
|
||||||
|
if (ss_check(ctx, s, pos + 1 + diff, op, stack_len, catch_pos))
|
||||||
|
goto fail;
|
||||||
|
catch_pos = pos;
|
||||||
|
break;
|
||||||
|
case OP_for_of_start:
|
||||||
|
case OP_for_await_of_start:
|
||||||
|
catch_pos = pos;
|
||||||
|
break;
|
||||||
|
/* we assume the catch offset entry is only removed with
|
||||||
|
some op codes */
|
||||||
|
case OP_drop:
|
||||||
|
catch_level = stack_len;
|
||||||
|
goto check_catch;
|
||||||
|
case OP_nip:
|
||||||
|
catch_level = stack_len - 1;
|
||||||
|
goto check_catch;
|
||||||
|
case OP_nip1:
|
||||||
|
catch_level = stack_len - 1;
|
||||||
|
goto check_catch;
|
||||||
|
case OP_iterator_close:
|
||||||
|
catch_level = stack_len + 2;
|
||||||
|
check_catch:
|
||||||
|
/* Note: for for_of_start/for_await_of_start we consider
|
||||||
|
the catch offset is on the first stack entry instead of
|
||||||
|
the thirst */
|
||||||
|
if (catch_pos >= 0) {
|
||||||
|
int level;
|
||||||
|
level = s->stack_level_tab[catch_pos];
|
||||||
|
if (bc_buf[catch_pos] != OP_catch)
|
||||||
|
level++; /* for_of_start, for_wait_of_start */
|
||||||
|
/* catch_level = stack_level before op_catch is executed ? */
|
||||||
|
if (catch_level == level) {
|
||||||
|
catch_pos = s->catch_pos_tab[catch_pos];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case OP_nip_catch:
|
||||||
|
if (catch_pos < 0) {
|
||||||
|
JS_ThrowInternalError(ctx, "nip_catch: no catch op (pc=%d)", pos);
|
||||||
|
goto fail;
|
||||||
|
}
|
||||||
|
stack_len = s->stack_level_tab[catch_pos];
|
||||||
|
if (bc_buf[catch_pos] != OP_catch)
|
||||||
|
stack_len++; /* for_of_start, for_wait_of_start */
|
||||||
|
stack_len++; /* no stack overflow is possible by construction */
|
||||||
|
catch_pos = s->catch_pos_tab[catch_pos];
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (ss_check(ctx, s, pos_next, op, stack_len))
|
if (ss_check(ctx, s, pos_next, op, stack_len, catch_pos))
|
||||||
goto fail;
|
goto fail;
|
||||||
done_insn: ;
|
done_insn: ;
|
||||||
}
|
}
|
||||||
js_free(ctx, s->stack_level_tab);
|
|
||||||
js_free(ctx, s->pc_stack);
|
js_free(ctx, s->pc_stack);
|
||||||
|
js_free(ctx, s->catch_pos_tab);
|
||||||
|
js_free(ctx, s->stack_level_tab);
|
||||||
*pstack_size = s->stack_len_max;
|
*pstack_size = s->stack_len_max;
|
||||||
return 0;
|
return 0;
|
||||||
fail:
|
fail:
|
||||||
js_free(ctx, s->stack_level_tab);
|
|
||||||
js_free(ctx, s->pc_stack);
|
js_free(ctx, s->pc_stack);
|
||||||
|
js_free(ctx, s->catch_pos_tab);
|
||||||
|
js_free(ctx, s->stack_level_tab);
|
||||||
*pstack_size = 0;
|
*pstack_size = 0;
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
|
@ -645,6 +645,18 @@ function test_generator()
|
||||||
assert(ret, "ret_val");
|
assert(ret, "ret_val");
|
||||||
return 3;
|
return 3;
|
||||||
}
|
}
|
||||||
|
function *f3() {
|
||||||
|
var ret;
|
||||||
|
/* test stack consistency with nip_n to handle yield return +
|
||||||
|
* finally clause */
|
||||||
|
try {
|
||||||
|
ret = 2 + (yield 1);
|
||||||
|
} catch(e) {
|
||||||
|
} finally {
|
||||||
|
ret++;
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
var g, v;
|
var g, v;
|
||||||
g = f();
|
g = f();
|
||||||
v = g.next();
|
v = g.next();
|
||||||
|
@ -665,6 +677,12 @@ function test_generator()
|
||||||
assert(v.value === 3 && v.done === true);
|
assert(v.value === 3 && v.done === true);
|
||||||
v = g.next();
|
v = g.next();
|
||||||
assert(v.value === undefined && v.done === true);
|
assert(v.value === undefined && v.done === true);
|
||||||
|
|
||||||
|
g = f3();
|
||||||
|
v = g.next();
|
||||||
|
assert(v.value === 1 && v.done === false);
|
||||||
|
v = g.next(3);
|
||||||
|
assert(v.value === 6 && v.done === true);
|
||||||
}
|
}
|
||||||
|
|
||||||
test();
|
test();
|
||||||
|
|
Loading…
Reference in a new issue