From f25e5d4094a11cf098670417e8a16ffb7cbadda0 Mon Sep 17 00:00:00 2001 From: Fabrice Bellard Date: Tue, 9 Jan 2024 19:15:40 +0100 Subject: [PATCH] optional chaining fixes (github issue #103) --- quickjs-opcode.h | 2 + quickjs.c | 116 ++++++++++++++++++++++++++++++++++++++--- test262_errors.txt | 2 - tests/test_language.js | 26 +++++++++ 4 files changed, 137 insertions(+), 9 deletions(-) diff --git a/quickjs-opcode.h b/quickjs-opcode.h index e032a44..6d2d6e9 100644 --- a/quickjs-opcode.h +++ b/quickjs-opcode.h @@ -286,6 +286,8 @@ def(scope_get_private_field, 7, 1, 1, atom_u16) /* obj -> value, emitted in phas def(scope_get_private_field2, 7, 1, 2, atom_u16) /* obj -> obj value, emitted in phase 1, removed in phase 2 */ def(scope_put_private_field, 7, 2, 0, atom_u16) /* obj value ->, emitted in phase 1, removed in phase 2 */ def(scope_in_private_field, 7, 1, 1, atom_u16) /* obj -> res emitted in phase 1, removed in phase 2 */ +def(get_field_opt_chain, 5, 1, 1, atom) /* emitted in phase 1, removed in phase 2 */ +def(get_array_el_opt_chain, 1, 2, 1, none) /* emitted in phase 1, removed in phase 2 */ def( set_class_name, 5, 1, 1, u32) /* emitted in phase 1, removed in phase 2 */ def( line_num, 5, 0, 0, u32) /* emitted in phase 1, removed in phase 3 */ diff --git a/quickjs.c b/quickjs.c index a6cab1d..5160787 100644 --- a/quickjs.c +++ b/quickjs.c @@ -21530,6 +21530,14 @@ static int new_label(JSParseState *s) return new_label_fd(s->cur_func, -1); } +/* don't update the last opcode and don't emit line number info */ +static void emit_label_raw(JSParseState *s, int label) +{ + emit_u8(s, OP_label); + emit_u32(s, label); + s->cur_func->label_slots[label].pos = s->cur_func->byte_code.size; +} + /* return the label ID offset */ static int emit_label(JSParseState *s, int label) { @@ -24643,6 +24651,25 @@ static __exception int js_parse_postfix_expr(JSParseState *s, int parse_flags) fd->byte_code.buf[fd->last_opcode_pos] = OP_get_field2; drop_count = 2; break; + case OP_get_field_opt_chain: + { + int opt_chain_label, next_label; + opt_chain_label = get_u32(fd->byte_code.buf + + fd->last_opcode_pos + 1 + 4 + 1); + /* keep the object on the stack */ + fd->byte_code.buf[fd->last_opcode_pos] = OP_get_field2; + fd->byte_code.size = fd->last_opcode_pos + 1 + 4; + next_label = emit_goto(s, OP_goto, -1); + emit_label(s, opt_chain_label); + /* need an additional undefined value for the + case where the optional field does not + exists */ + emit_op(s, OP_undefined); + emit_label(s, next_label); + drop_count = 2; + opcode = OP_get_field; + } + break; case OP_scope_get_private_field: /* keep the object on the stack */ fd->byte_code.buf[fd->last_opcode_pos] = OP_scope_get_private_field2; @@ -24653,6 +24680,25 @@ static __exception int js_parse_postfix_expr(JSParseState *s, int parse_flags) fd->byte_code.buf[fd->last_opcode_pos] = OP_get_array_el2; drop_count = 2; break; + case OP_get_array_el_opt_chain: + { + int opt_chain_label, next_label; + opt_chain_label = get_u32(fd->byte_code.buf + + fd->last_opcode_pos + 1 + 1); + /* keep the object on the stack */ + fd->byte_code.buf[fd->last_opcode_pos] = OP_get_array_el2; + fd->byte_code.size = fd->last_opcode_pos + 1; + next_label = emit_goto(s, OP_goto, -1); + emit_label(s, opt_chain_label); + /* need an additional undefined value for the + case where the optional field does not + exists */ + emit_op(s, OP_undefined); + emit_label(s, next_label); + drop_count = 2; + opcode = OP_get_array_el; + } + break; case OP_scope_get_var: { JSAtom name; @@ -24935,8 +24981,23 @@ static __exception int js_parse_postfix_expr(JSParseState *s, int parse_flags) break; } } - if (optional_chaining_label >= 0) - emit_label(s, optional_chaining_label); + if (optional_chaining_label >= 0) { + JSFunctionDef *fd = s->cur_func; + int opcode; + emit_label_raw(s, optional_chaining_label); + /* modify the last opcode so that it is an indicator of an + optional chain */ + opcode = get_prev_opcode(fd); + if (opcode == OP_get_field || opcode == OP_get_array_el) { + if (opcode == OP_get_field) + opcode = OP_get_field_opt_chain; + else + opcode = OP_get_array_el_opt_chain; + fd->byte_code.buf[fd->last_opcode_pos] = opcode; + } else { + fd->last_opcode_pos = -1; + } + } return 0; } @@ -24952,27 +25013,57 @@ static __exception int js_parse_delete(JSParseState *s) return -1; switch(opcode = get_prev_opcode(fd)) { case OP_get_field: + case OP_get_field_opt_chain: { JSValue val; - int ret; - + int ret, opt_chain_label, next_label; + if (opcode == OP_get_field_opt_chain) { + opt_chain_label = get_u32(fd->byte_code.buf + + fd->last_opcode_pos + 1 + 4 + 1); + } else { + opt_chain_label = -1; + } name = get_u32(fd->byte_code.buf + fd->last_opcode_pos + 1); fd->byte_code.size = fd->last_opcode_pos; - fd->last_opcode_pos = -1; val = JS_AtomToValue(s->ctx, name); ret = emit_push_const(s, val, 1); JS_FreeValue(s->ctx, val); JS_FreeAtom(s->ctx, name); if (ret) return ret; + emit_op(s, OP_delete); + if (opt_chain_label >= 0) { + next_label = emit_goto(s, OP_goto, -1); + emit_label(s, opt_chain_label); + /* if the optional chain is not taken, return 'true' */ + emit_op(s, OP_drop); + emit_op(s, OP_push_true); + emit_label(s, next_label); + } + fd->last_opcode_pos = -1; } - goto do_delete; + break; case OP_get_array_el: fd->byte_code.size = fd->last_opcode_pos; fd->last_opcode_pos = -1; - do_delete: emit_op(s, OP_delete); break; + case OP_get_array_el_opt_chain: + { + int opt_chain_label, next_label; + opt_chain_label = get_u32(fd->byte_code.buf + + fd->last_opcode_pos + 1 + 1); + fd->byte_code.size = fd->last_opcode_pos; + emit_op(s, OP_delete); + next_label = emit_goto(s, OP_goto, -1); + emit_label(s, opt_chain_label); + /* if the optional chain is not taken, return 'true' */ + emit_op(s, OP_drop); + emit_op(s, OP_push_true); + emit_label(s, next_label); + fd->last_opcode_pos = -1; + } + break; case OP_scope_get_var: /* 'delete this': this is not a reference */ name = get_u32(fd->byte_code.buf + fd->last_opcode_pos + 1); @@ -31606,6 +31697,17 @@ static __exception int resolve_variables(JSContext *ctx, JSFunctionDef *s) case OP_set_class_name: /* only used during parsing */ break; + + case OP_get_field_opt_chain: /* equivalent to OP_get_field */ + { + JSAtom name = get_u32(bc_buf + pos + 1); + dbuf_putc(&bc_out, OP_get_field); + dbuf_put_u32(&bc_out, name); + } + break; + case OP_get_array_el_opt_chain: /* equivalent to OP_get_array_el */ + dbuf_putc(&bc_out, OP_get_array_el); + break; default: no_change: diff --git a/test262_errors.txt b/test262_errors.txt index 5818f77..1572eb5 100644 --- a/test262_errors.txt +++ b/test262_errors.txt @@ -9,6 +9,4 @@ test262/test/language/expressions/dynamic-import/usage-from-eval.js:26: TypeErro test262/test/language/expressions/dynamic-import/usage-from-eval.js:26: strict mode: TypeError: $DONE() not called test262/test/language/expressions/in/private-field-invalid-assignment-target.js:23: unexpected error type: Test262: This statement should not be evaluated. test262/test/language/expressions/in/private-field-invalid-assignment-target.js:23: strict mode: unexpected error type: Test262: This statement should not be evaluated. -test262/test/language/expressions/optional-chaining/optional-call-preserves-this.js:21: TypeError: cannot read property 'c' of undefined -test262/test/language/expressions/optional-chaining/optional-call-preserves-this.js:15: strict mode: TypeError: cannot read property '_b' of undefined test262/test/language/global-code/script-decl-lex-var-declared-via-eval-sloppy.js:13: Test262Error: variable Expected a SyntaxError to be thrown but no exception was thrown at all diff --git a/tests/test_language.js b/tests/test_language.js index 6785416..65dab5f 100644 --- a/tests/test_language.js +++ b/tests/test_language.js @@ -558,6 +558,31 @@ function test_parse_semicolon() } } +/* optional chaining tests not present in test262 */ +function test_optional_chaining() +{ + var a, z; + z = null; + a = { b: { c: 2 } }; + assert(delete z?.b.c, true); + assert(delete a?.b.c, true); + assert(JSON.stringify(a), '{"b":{}}', "optional chaining delete"); + + a = { b: { c: 2 } }; + assert(delete z?.b["c"], true); + assert(delete a?.b["c"], true); + assert(JSON.stringify(a), '{"b":{}}'); + + a = { + b() { return this._b; }, + _b: { c: 42 } + }; + + assert((a?.b)().c, 42); + + assert((a?.["b"])().c, 42); +} + test_op1(); test_cvt(); test_eq(); @@ -578,3 +603,4 @@ test_function_length(); test_argument_scope(); test_function_expr_name(); test_parse_semicolon(); +test_optional_chaining();