diff --git a/src/njs_disassembler.c b/src/njs_disassembler.c index 72ea63b9a..b1e7aa50b 100644 --- a/src/njs_disassembler.c +++ b/src/njs_disassembler.c @@ -323,6 +323,21 @@ njs_disassemble(u_char *start, u_char *end, njs_int_t count, njs_arr_t *lines) continue; } + if (operation == NJS_VMCODE_OPTIONAL_CHAIN) { + test_jump = (njs_vmcode_test_jump_t *) p; + + njs_printf("%5uD | %05uz OPTIONAL CHAIN " + "%04Xz %04Xz %z\n", + line, p - start, + (size_t) test_jump->retval, + (size_t) test_jump->value, + (size_t) test_jump->offset); + + p += sizeof(njs_vmcode_test_jump_t); + + continue; + } + if (operation == NJS_VMCODE_FUNCTION_FRAME) { function = (njs_vmcode_function_frame_t *) p; diff --git a/src/njs_generator.c b/src/njs_generator.c index bb443017d..0139e1922 100644 --- a/src/njs_generator.c +++ b/src/njs_generator.c @@ -278,6 +278,12 @@ static njs_int_t njs_generate_test_jump_expression_after(njs_vm_t *vm, njs_generator_t *generator, njs_parser_node_t *node); static njs_int_t njs_generate_test_jump_expression_end(njs_vm_t *vm, njs_generator_t *generator, njs_parser_node_t *node); +static njs_int_t njs_generate_optional_chain(njs_vm_t *vm, + njs_generator_t *generator, njs_parser_node_t *node); +static njs_int_t njs_generate_optional_chain_after(njs_vm_t *vm, + njs_generator_t *generator, njs_parser_node_t *node); +static njs_int_t njs_generate_optional_chain_end(njs_vm_t *vm, + njs_generator_t *generator, njs_parser_node_t *node); static njs_int_t njs_generate_3addr_operation(njs_vm_t *vm, njs_generator_t *generator, njs_parser_node_t *node, njs_bool_t swap); static njs_int_t njs_generate_3addr_operation_name(njs_vm_t *vm, @@ -661,6 +667,12 @@ njs_generate(njs_vm_t *vm, njs_generator_t *generator, njs_parser_node_t *node) case NJS_TOKEN_COALESCE: return njs_generate_test_jump_expression(vm, generator, node); + case NJS_TOKEN_OPTIONAL_CHAIN: + return njs_generate_optional_chain(vm, generator, node); + + case NJS_TOKEN_OPTIONAL_CHAIN_REF: + return njs_generator_stack_pop(vm, generator, NULL); + case NJS_TOKEN_DELETE: case NJS_TOKEN_VOID: case NJS_TOKEN_UNARY_PLUS: @@ -3836,6 +3848,130 @@ njs_generate_test_jump_expression_end(njs_vm_t *vm, njs_generator_t *generator, } +static njs_parser_node_t * +njs_generate_optional_chain_ref(njs_parser_node_t *node) +{ + while (node != NULL) { + if (node->token_type == NJS_TOKEN_OPTIONAL_CHAIN_REF) { + return node; + } + + node = node->left; + } + + return NULL; +} + + +static njs_int_t +njs_generate_optional_chain(njs_vm_t *vm, njs_generator_t *generator, + njs_parser_node_t *node) +{ + njs_generator_next(generator, njs_generate, node->left); + + return njs_generator_after(vm, generator, + njs_queue_first(&generator->stack), + node, njs_generate_optional_chain_after, + NULL, 0); +} + + +static njs_int_t +njs_generate_optional_chain_after(njs_vm_t *vm, njs_generator_t *generator, + njs_parser_node_t *node) +{ + njs_jump_off_t jump_offset; + njs_parser_node_t *ref, *prop, *base_prop, *base, *walk; + njs_vmcode_test_jump_t *test_jump; + + njs_generate_code(generator, njs_vmcode_test_jump_t, test_jump, + NJS_VMCODE_OPTIONAL_CHAIN, node); + jump_offset = njs_code_offset(generator, test_jump); + test_jump->value = node->left->index; + + node->index = njs_generate_node_temp_index_get(vm, generator, node); + if (njs_slow_path(node->index == NJS_INDEX_ERROR)) { + return node->index; + } + + test_jump->retval = node->index; + + /* + * Walk left spine to find METHOD_CALL with two OPTIONAL_CHAIN_REF + * children (created for ?.() on a property base like o.m?.()). + */ + walk = node->right; + base_prop = NULL; + + while (walk != NULL) { + prop = walk->left; + + if (walk->token_type == NJS_TOKEN_METHOD_CALL + && prop->token_type == NJS_TOKEN_PROPERTY + && prop->right->token_type == NJS_TOKEN_OPTIONAL_CHAIN_REF) + { + base = node->left; + + if (base->token_type == NJS_TOKEN_PROPERTY) { + base_prop = base; + + } else if (base->token_type == NJS_TOKEN_OPTIONAL_CHAIN + && base->right->token_type == NJS_TOKEN_PROPERTY) + { + base_prop = base->right; + } + + if (base_prop != NULL) { + prop->left->index = base_prop->left->index; + prop->right->index = base_prop->right->index; + } + + break; + } + + walk = walk->left; + } + + if (base_prop == NULL) { + ref = njs_generate_optional_chain_ref(node->right); + if (ref != NULL) { + ref->index = node->left->index; + } + } + + njs_generator_next(generator, njs_generate, node->right); + + return njs_generator_after(vm, generator, + njs_queue_first(&generator->stack), + node, njs_generate_optional_chain_end, + &jump_offset, sizeof(njs_jump_off_t)); +} + + +static njs_int_t +njs_generate_optional_chain_end(njs_vm_t *vm, njs_generator_t *generator, + njs_parser_node_t *node) +{ + njs_int_t ret; + njs_vmcode_move_t *move; + + if (node->index != node->right->index) { + njs_generate_code_move(generator, move, node->index, + node->right->index, node); + } + + njs_code_set_jump_offset(generator, njs_vmcode_test_jump_t, + *((njs_jump_off_t *) generator->context)); + + ret = njs_generate_children_indexes_release(vm, generator, node); + if (njs_slow_path(ret != NJS_OK)) { + return ret; + } + + return njs_generator_stack_pop(vm, generator, generator->context); +} + + static njs_int_t njs_generate_3addr_operation(njs_vm_t *vm, njs_generator_t *generator, njs_parser_node_t *node, njs_bool_t swap) diff --git a/src/njs_lexer.h b/src/njs_lexer.h index 3728e2536..c3cd366ac 100644 --- a/src/njs_lexer.h +++ b/src/njs_lexer.h @@ -135,6 +135,9 @@ typedef enum { NJS_TOKEN_PROPERTY_SETTER, NJS_TOKEN_PROTO_INIT, + NJS_TOKEN_OPTIONAL_CHAIN, + NJS_TOKEN_OPTIONAL_CHAIN_REF, + NJS_TOKEN_ARRAY, NJS_TOKEN_GRAVE, diff --git a/src/njs_parser.c b/src/njs_parser.c index 60fa26934..ae2680116 100644 --- a/src/njs_parser.c +++ b/src/njs_parser.c @@ -111,6 +111,8 @@ static njs_int_t njs_parser_optional_chain(njs_parser_t *parser, njs_lexer_token_t *token, njs_queue_link_t *current); static njs_int_t njs_parser_optional_chain_after(njs_parser_t *parser, njs_lexer_token_t *token, njs_queue_link_t *current); +static njs_int_t njs_parser_optional_chain_wrap(njs_parser_t *parser, + njs_lexer_token_t *token, njs_queue_link_t *current); static njs_int_t njs_parser_new_expression(njs_parser_t *parser, njs_lexer_token_t *token, njs_queue_link_t *current); static njs_int_t njs_parser_new_expression_after(njs_parser_t *parser, @@ -2640,7 +2642,7 @@ static njs_parser_node_t * njs_parser_create_call(njs_parser_t *parser, njs_parser_node_t *node, uint8_t ctor) { - njs_parser_node_t *func; + njs_parser_node_t *func, *prop; switch (node->token_type) { case NJS_TOKEN_NAME: @@ -2658,6 +2660,31 @@ njs_parser_create_call(njs_parser_t *parser, njs_parser_node_t *node, func->left = node; break; + case NJS_TOKEN_OPTIONAL_CHAIN: + if (node->right != NULL + && node->right->token_type == NJS_TOKEN_PROPERTY) + { + prop = njs_parser_node_new(parser, NJS_TOKEN_PROPERTY); + if (prop == NULL) { + return NULL; + } + + prop->u.operation = node->right->u.operation; + prop->token_line = node->right->token_line; + prop->left = node->left; + prop->right = node->right->right; + + func = njs_parser_node_new(parser, NJS_TOKEN_METHOD_CALL); + if (func == NULL) { + return NULL; + } + + func->left = prop; + break; + } + + /* Fall through. */ + default: /* * NJS_TOKEN_METHOD_CALL, @@ -2934,6 +2961,9 @@ static njs_int_t njs_parser_optional_expression_after(njs_parser_t *parser, njs_lexer_token_t *token, njs_queue_link_t *current) { + njs_int_t ret; + njs_parser_node_t *opt, *ref; + if (token->type != NJS_TOKEN_CONDITIONAL) { return njs_parser_stack_pop(parser); } @@ -2947,8 +2977,33 @@ njs_parser_optional_expression_after(njs_parser_t *parser, return njs_parser_stack_pop(parser); } + opt = njs_parser_node_new(parser, NJS_TOKEN_OPTIONAL_CHAIN); + if (opt == NULL) { + return NJS_ERROR; + } + + opt->token_line = token->line; + opt->u.operation = NJS_VMCODE_OPTIONAL_CHAIN; + opt->left = parser->node; + opt->left->dest = opt; + + ref = njs_parser_node_new(parser, NJS_TOKEN_OPTIONAL_CHAIN_REF); + if (ref == NULL) { + return NJS_ERROR; + } + + ref->token_line = token->line; + ref->left = parser->node; + parser->node = ref; + njs_parser_next(parser, njs_parser_optional_chain); + ret = njs_parser_after(parser, current, opt, 1, + njs_parser_optional_chain_wrap); + if (ret != NJS_OK) { + return NJS_ERROR; + } + return njs_parser_after(parser, current, NULL, 1, njs_parser_optional_expression_after); } @@ -2959,17 +3014,12 @@ njs_parser_optional_chain(njs_parser_t *parser, njs_lexer_token_t *token, njs_queue_link_t *current) { njs_int_t ret; - njs_parser_node_t *func; + njs_parser_node_t *node, *func, *prop_node; /* - * ? . Arguments - * ? . [ Expression ] - * ? . IdentifierName - * ? . TemplateLiteral - * OptionalChain Arguments - * OptionalChain [ Expression ] - * OptionalChain . IdentifierName - * OptionalChain TemplateLiteral + * ?. Arguments + * ?. [ Expression ] + * ?. IdentifierName */ if (token->type != NJS_TOKEN_CONDITIONAL) { @@ -2985,7 +3035,8 @@ njs_parser_optional_chain(njs_parser_t *parser, njs_lexer_token_t *token, return njs_parser_failed(parser); } - njs_lexer_consume_token(parser->lexer, 1); + /* Consume both '?' and '.' */ + njs_lexer_consume_token(parser->lexer, 2); token = njs_lexer_token(parser->lexer, 0); if (token == NULL) { @@ -2994,6 +3045,43 @@ njs_parser_optional_chain(njs_parser_t *parser, njs_lexer_token_t *token, switch (token->type) { case NJS_TOKEN_OPEN_PARENTHESIS: + node = parser->node->left; + + if (node != NULL + && (node->token_type == NJS_TOKEN_PROPERTY + || (node->token_type == NJS_TOKEN_OPTIONAL_CHAIN + && node->right != NULL + && node->right->token_type == NJS_TOKEN_PROPERTY))) + { + /* o.m?.() or o?.m?.() METHOD_CALL for this. */ + + node = njs_parser_node_new(parser, NJS_TOKEN_OPTIONAL_CHAIN_REF); + if (node == NULL) { + return NJS_ERROR; + } + + node->token_line = token->line; + + prop_node = njs_parser_node_new(parser, + NJS_TOKEN_OPTIONAL_CHAIN_REF); + if (prop_node == NULL) { + return NJS_ERROR; + } + + prop_node->token_line = token->line; + + func = njs_parser_node_new(parser, NJS_TOKEN_PROPERTY); + if (func == NULL) { + return NJS_ERROR; + } + + func->left = node; + func->right = prop_node; + func->token_line = token->line; + + parser->node = func; + } + func = njs_parser_create_call(parser, parser->node, 0); if (func == NULL) { return NJS_ERROR; @@ -3002,7 +3090,7 @@ njs_parser_optional_chain(njs_parser_t *parser, njs_lexer_token_t *token, func->token_line = token->line; parser->node = func; - njs_lexer_consume_token(parser->lexer, 2); + njs_lexer_consume_token(parser->lexer, 1); njs_parser_next(parser, njs_parser_arguments); ret = njs_parser_after(parser, current, func, 1, @@ -3011,25 +3099,66 @@ njs_parser_optional_chain(njs_parser_t *parser, njs_lexer_token_t *token, return NJS_ERROR; } - break; + return njs_parser_after(parser, current, NULL, 1, + njs_parser_optional_chain_after); - default: - ret = njs_parser_property(parser, token, current); + case NJS_TOKEN_OPEN_BRACKET: + node = njs_parser_node_new(parser, NJS_TOKEN_PROPERTY); + if (node == NULL) { + return NJS_ERROR; + } - switch (ret) { - case NJS_DONE: - case NJS_DECLINED: + node->u.operation = NJS_VMCODE_PROPERTY_GET; + node->left = parser->node; + node->token_line = token->line; + + parser->node = NULL; + + njs_lexer_consume_token(parser->lexer, 1); + + njs_parser_next(parser, njs_parser_expression); + + ret = njs_parser_after(parser, current, node, 1, + njs_parser_member_expression_bracket); + if (ret != NJS_OK) { + return NJS_ERROR; + } + + return njs_parser_after(parser, current, NULL, 1, + njs_parser_optional_chain_after); + + default: + if (!njs_lexer_token_is_identifier_name(token)) { return njs_parser_failed(parser); + } - default: - break; + node = njs_parser_node_new(parser, NJS_TOKEN_PROPERTY); + if (node == NULL) { + return NJS_ERROR; } - break; - } + node->u.operation = NJS_VMCODE_PROPERTY_ATOM_GET; + node->token_line = token->line; - return njs_parser_after(parser, current, NULL, 1, - njs_parser_optional_chain_after); + prop_node = njs_parser_node_string(parser->vm, token, + parser); + if (prop_node == NULL) { + return NJS_ERROR; + } + + prop_node->token_line = token->line; + + node->left = parser->node; + node->right = prop_node; + + parser->node = node; + + njs_lexer_consume_token(parser->lexer, 1); + + njs_parser_next(parser, njs_parser_optional_chain_after); + + return NJS_OK; + } } @@ -3093,6 +3222,17 @@ njs_parser_optional_chain_after(njs_parser_t *parser, njs_lexer_token_t *token, } +static njs_int_t +njs_parser_optional_chain_wrap(njs_parser_t *parser, + njs_lexer_token_t *token, njs_queue_link_t *current) +{ + parser->target->right = parser->node; + parser->node = parser->target; + + return njs_parser_stack_pop(parser); +} + + static njs_int_t njs_parser_new_expression(njs_parser_t *parser, njs_lexer_token_t *token, njs_queue_link_t *current) @@ -3537,6 +3677,16 @@ njs_parser_unary_expression_next(njs_parser_t *parser, return njs_parser_stack_pop(parser); + case NJS_TOKEN_OPTIONAL_CHAIN: + if (node->right != NULL + && node->right->token_type == NJS_TOKEN_PROPERTY) + { + node->right->token_type = NJS_TOKEN_PROPERTY_DELETE; + node->right->u.operation = NJS_VMCODE_PROPERTY_DELETE; + } + + break; + case NJS_TOKEN_NAME: njs_parser_syntax_error(parser, "Delete of an unqualified identifier"); @@ -9484,6 +9634,8 @@ njs_parser_serialize_node(njs_chb_t *chain, njs_parser_node_t *node) njs_token_serialize(NJS_TOKEN_BITWISE_NOT); njs_token_serialize(NJS_TOKEN_LOGICAL_NOT); njs_token_serialize(NJS_TOKEN_COALESCE); + njs_token_serialize(NJS_TOKEN_OPTIONAL_CHAIN); + njs_token_serialize(NJS_TOKEN_OPTIONAL_CHAIN_REF); njs_token_serialize(NJS_TOKEN_IN); njs_token_serialize(NJS_TOKEN_OF); njs_token_serialize(NJS_TOKEN_INSTANCEOF); diff --git a/src/njs_vmcode.c b/src/njs_vmcode.c index 2dac79e6c..ac8abd5d9 100644 --- a/src/njs_vmcode.c +++ b/src/njs_vmcode.c @@ -212,6 +212,7 @@ njs_vmcode_interpreter(njs_vm_t *vm, u_char *pc, njs_value_t *rval, NJS_GOTO_ROW(NJS_VMCODE_TEST_IF_TRUE), NJS_GOTO_ROW(NJS_VMCODE_TEST_IF_FALSE), NJS_GOTO_ROW(NJS_VMCODE_COALESCE), + NJS_GOTO_ROW(NJS_VMCODE_OPTIONAL_CHAIN), NJS_GOTO_ROW(NJS_VMCODE_UNARY_PLUS), NJS_GOTO_ROW(NJS_VMCODE_UNARY_NEGATION), NJS_GOTO_ROW(NJS_VMCODE_BITWISE_NOT), @@ -1110,6 +1111,23 @@ NEXT_LBL; BREAK; + CASE (NJS_VMCODE_OPTIONAL_CHAIN): + njs_vmcode_debug_opcode(); + + njs_vmcode_operand(vm, vmcode->operand2, value1); + + if (njs_is_null_or_undefined(value1)) { + njs_vmcode_operand(vm, vmcode->operand1, retval); + njs_set_undefined(retval); + test_jump = (njs_vmcode_test_jump_t *) pc; + ret = test_jump->offset; + + } else { + ret = sizeof(njs_vmcode_3addr_t); + } + + BREAK; + #define NJS_PRE_UNARY \ if (njs_slow_path(!njs_is_numeric(value1))) { \ ret = njs_value_to_numeric(vm, value1, &numeric1); \ diff --git a/src/njs_vmcode.h b/src/njs_vmcode.h index 5170f29fc..2cdfd23b8 100644 --- a/src/njs_vmcode.h +++ b/src/njs_vmcode.h @@ -98,6 +98,7 @@ enum { NJS_VMCODE_TEST_IF_TRUE, NJS_VMCODE_TEST_IF_FALSE, NJS_VMCODE_COALESCE, + NJS_VMCODE_OPTIONAL_CHAIN, NJS_VMCODE_UNARY_PLUS, NJS_VMCODE_UNARY_NEGATION, NJS_VMCODE_BITWISE_NOT, diff --git a/src/test/njs_unit_test.c b/src/test/njs_unit_test.c index c81241ac0..bb47e7740 100644 --- a/src/test/njs_unit_test.c +++ b/src/test/njs_unit_test.c @@ -1503,6 +1503,137 @@ static njs_unit_test_t njs_test[] = { njs_str("null ?? 0 || 1"), njs_str("SyntaxError: Unexpected token \"||\"") }, + /* Optional chaining: property access. */ + + { njs_str("var o = {a: 1}; o?.a"), + njs_str("1") }, + + { njs_str("var o = null; o?.a"), + njs_str("undefined") }, + + { njs_str("undefined?.a"), + njs_str("undefined") }, + + { njs_str("var o = {a: {b: 2}}; o?.a.b"), + njs_str("2") }, + + { njs_str("var o = null; o?.a.b"), + njs_str("undefined") }, + + /* Optional chaining: bracket access. */ + + { njs_str("var o = {a: 1}; o?.['a']"), + njs_str("1") }, + + { njs_str("var o = null; o?.['a']"), + njs_str("undefined") }, + + /* Optional chaining: method call. */ + + { njs_str("var o = {f: function() {return 42}}; o?.f()"), + njs_str("42") }, + + { njs_str("var o = null; o?.f()"), + njs_str("undefined") }, + + { njs_str("var o = { b() { return this._b; }, _b: { c: 42 }};" + "o?.b().c"), + njs_str("42") }, + + { njs_str("var o = null;" + "o?.b().c"), + njs_str("undefined") }, + + /* Optional chaining: optional call. */ + + { njs_str("var f = function() {return 42}; f?.()"), + njs_str("42") }, + + { njs_str("var f = null; f?.()"), + njs_str("undefined") }, + + /* Optional chaining: nested. */ + + { njs_str("var o = {a: {b: 3}}; o?.a?.b"), + njs_str("3") }, + + { njs_str("var o = {a: null}; o?.a?.b"), + njs_str("undefined") }, + + { njs_str("var o = null; o?.a?.b"), + njs_str("undefined") }, + + /* Optional chaining: short-circuit side effects. */ + + { njs_str("var c = 0; var o = null; o?.a; c"), + njs_str("0") }, + + /* Optional chaining: delete semantics. */ + + { njs_str("var o = null; delete o?.a"), + njs_str("true") }, + + { njs_str("var o = null; delete o?.['a']"), + njs_str("true") }, + + { njs_str("var o = {a: 1}; delete o?.a; o.a"), + njs_str("undefined") }, + + { njs_str("var o = {a: 1}; delete o?.['a']; o.a"), + njs_str("undefined") }, + + /* Optional chaining with ??. */ + + { njs_str("var o = null; o?.a ?? 'default'"), + njs_str("default") }, + + { njs_str("var o = {a: 0}; o?.a ?? 'default'"), + njs_str("0") }, + + /* Optional chaining: advanced and corner cases. */ + + { njs_str("var i = 0; var o = null; o?.[i++]; i"), + njs_str("0") }, + + { njs_str("var i = 0; var o = null; o?.f(i++); i"), + njs_str("0") }, + + { njs_str("var o = {x: 7, m: function() {return this.x}}; o.m?.()"), + njs_str("7") }, + + { njs_str("var o = {x: 9, m: function() {return this.x}}; o?.m?.()"), + njs_str("9") }, + + { njs_str("var i = 0; var o = {m: null}; o.m?.(i++); i"), + njs_str("0") }, + + { njs_str("var o = null; (o?.a).b"), + njs_str("TypeError: cannot get property \"b\" of undefined") }, + + { njs_str("var o = {a: null}; o?.a.b"), + njs_str("TypeError: cannot get property \"b\" of null") }, + + { njs_str("var o = null; o?.().a"), + njs_str("undefined") }, + + { njs_str("var o = function() {return {a: 1}}; o?.().a"), + njs_str("1") }, + + { njs_str("var o = {}; o?.()"), + njs_str("TypeError: object is not a function") }, + + { njs_str("var o = {x: 2, m: function() {return this.x}}; (o.m)?.()"), + njs_str("2") }, + + { njs_str("var o = {x: 2, m: function() {return this.x}}; (o?.m)()"), + njs_str("2") }, + + { njs_str("var o = {a: {b: 1}}; delete o?.a?.b; o.a.b"), + njs_str("undefined") }, + + { njs_str("var o = null; delete o?.a?.b"), + njs_str("true") }, + { njs_str("var a = true; a = -~!a"), njs_str("1") },