From e4f5db00441bef73f8d7d19e474e5d9782e68b68 Mon Sep 17 00:00:00 2001 From: Dmitry Volyntsev Date: Fri, 27 Feb 2026 23:42:40 -0800 Subject: [PATCH] Implement optional chaining. --- src/njs_disassembler.c | 15 ++ src/njs_generator.c | 147 +++++++++++++++++++ src/njs_lexer.h | 2 + src/njs_parser.c | 295 +++++++++++++++++++++++++++------------ src/njs_vmcode.c | 18 +++ src/njs_vmcode.h | 1 + src/test/njs_unit_test.c | 212 ++++++++++++++++++++++++++++ 7 files changed, 604 insertions(+), 86 deletions(-) diff --git a/src/njs_disassembler.c b/src/njs_disassembler.c index 72ea63b9..b1e7aa50 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 d651a996..6bc09115 100644 --- a/src/njs_generator.c +++ b/src/njs_generator.c @@ -301,6 +301,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, @@ -684,6 +690,9 @@ 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_DELETE: case NJS_TOKEN_VOID: case NJS_TOKEN_UNARY_PLUS: @@ -3940,6 +3949,135 @@ njs_generate_test_jump_expression_end(njs_vm_t *vm, njs_generator_t *generator, } +static njs_parser_node_t * +njs_generate_optional_method_call(njs_parser_node_t *node) +{ + if (node != NULL + && node->token_type == NJS_TOKEN_METHOD_CALL + && node->u.object != NULL) + { + return node; + } + + return NULL; +} + + +static njs_int_t +njs_generate_optional_chain(njs_vm_t *vm, njs_generator_t *generator, + njs_parser_node_t *node) +{ + njs_jump_off_t jump_offset; + njs_parser_node_t *call, *preserve; + + jump_offset = 0; + + call = njs_generate_optional_method_call(node->right); + if (call != NULL) { + preserve = call->u.object->left; + + if (preserve->token_type == NJS_TOKEN_PROPERTY) { + preserve->hoist = 1; + } + } + + 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, + &jump_offset, sizeof(njs_jump_off_t)); +} + + +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 *call, *prop; + njs_vmcode_test_jump_t *test_jump; + + jump_offset = generator->context; + + 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; + + call = njs_generate_optional_method_call(node->right); + if (call != NULL) { + prop = call->left; + prop->left->index = call->u.object->left->index; + prop->right->index = call->u.object->right->index; + + } else if (node->u.object != NULL) { + node->u.object->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_jump_off_t *jump_offset; + njs_vmcode_move_t *move; + njs_parser_node_t *call, *preserve; + + jump_offset = generator->context; + + 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, + *jump_offset); + + call = njs_generate_optional_method_call(node->right); + if (call != NULL) { + preserve = call->u.object->left; + + if (preserve->token_type != NJS_TOKEN_PROPERTY) { + preserve = NULL; + } + } else { + preserve = NULL; + } + + if (preserve != NULL) { + ret = njs_generate_index_release(vm, generator, + preserve->index); + if (njs_slow_path(ret != NJS_OK)) { + return ret; + } + } + + 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) @@ -4769,6 +4907,10 @@ njs_generate_method_call(njs_vm_t *vm, njs_generator_t *generator, njs_int_t ret; njs_parser_node_t *prop; + if (njs_generate_optional_method_call(node) != NULL) { + return njs_generate_method_call_arguments(vm, generator, node); + } + prop = node->left; /* Object. */ @@ -5719,6 +5861,11 @@ njs_generate_node_index_release(njs_vm_t *vm, njs_generator_t *generator, njs_parser_node_t *node) { if (node != NULL && node->temporary) { + if (node->hoist) { + node->hoist = 0; + return NJS_OK; + } + return njs_generate_index_release(vm, generator, node->index); } diff --git a/src/njs_lexer.h b/src/njs_lexer.h index 3728e253..5770e1c5 100644 --- a/src/njs_lexer.h +++ b/src/njs_lexer.h @@ -135,6 +135,8 @@ typedef enum { NJS_TOKEN_PROPERTY_SETTER, NJS_TOKEN_PROTO_INIT, + NJS_TOKEN_OPTIONAL_CHAIN, + NJS_TOKEN_ARRAY, NJS_TOKEN_GRAVE, diff --git a/src/njs_parser.c b/src/njs_parser.c index 16f9df4f..e5ec98cc 100644 --- a/src/njs_parser.c +++ b/src/njs_parser.c @@ -96,10 +96,15 @@ static njs_int_t njs_parser_call_arguments(njs_parser_t *parser, njs_parser_node_t *func, njs_parser_state_func_t after); static njs_int_t njs_parser_right_link_pop(njs_parser_t *parser); +static njs_parser_node_t *njs_parser_optional_chain_method_call( + njs_parser_t *parser, njs_parser_node_t *node, uint32_t token_line); static njs_int_t njs_parser_call_expression(njs_parser_t *parser, njs_lexer_token_t *token, njs_queue_link_t *current); static njs_int_t njs_parser_call_expression_args(njs_parser_t *parser, njs_lexer_token_t *token, njs_queue_link_t *current); +static njs_int_t njs_parser_call_or_property_after(njs_parser_t *parser, + njs_lexer_token_t *token, njs_queue_link_t *current, + njs_parser_state_func_t after); static njs_int_t njs_parser_call_expression_after(njs_parser_t *parser, njs_lexer_token_t *token, njs_queue_link_t *current); static njs_int_t njs_parser_arguments(njs_parser_t *parser, @@ -2702,6 +2707,61 @@ njs_parser_right_link_pop(njs_parser_t *parser) } +static njs_parser_node_t * +njs_parser_optional_chain_property(njs_parser_node_t *node) +{ + if (node == NULL) { + return NULL; + } + + if (node->token_type == NJS_TOKEN_PROPERTY) { + return node; + } + + if (node->token_type == NJS_TOKEN_OPTIONAL_CHAIN + && node->right != NULL + && node->right->token_type == NJS_TOKEN_PROPERTY) + { + return node->right; + } + + return NULL; +} + + +static njs_parser_node_t * +njs_parser_optional_chain_method_call(njs_parser_t *parser, + njs_parser_node_t *node, uint32_t token_line) +{ + njs_parser_node_t *func, *prop, *src; + + src = njs_parser_optional_chain_property(node); + if (src == NULL) { + return NULL; + } + + prop = njs_parser_node_new(parser, NJS_TOKEN_PROPERTY); + if (prop == NULL) { + return NULL; + } + + prop->u.operation = src->u.operation; + prop->token_line = token_line; + prop->left = src->left; + prop->right = src->right; + + func = njs_parser_node_new(parser, NJS_TOKEN_METHOD_CALL); + if (func == NULL) { + return NULL; + } + + func->left = prop; + func->u.object = src; + + return func; +} + + static njs_int_t njs_parser_call_expression(njs_parser_t *parser, njs_lexer_token_t *token, njs_queue_link_t *current) @@ -2764,19 +2824,13 @@ njs_parser_call_expression_args(njs_parser_t *parser, njs_lexer_token_t *token, static njs_int_t -njs_parser_call_expression_after(njs_parser_t *parser, - njs_lexer_token_t *token, njs_queue_link_t *current) +njs_parser_call_or_property_after(njs_parser_t *parser, + njs_lexer_token_t *token, njs_queue_link_t *current, + njs_parser_state_func_t after) { njs_int_t ret; njs_parser_node_t *func; - /* - * Arguments - * [ Expression ] - * . IdentifierName - * TemplateLiteral - */ - switch (token->type) { case NJS_TOKEN_OPEN_PARENTHESIS: func = njs_parser_create_call(parser, parser->node, 0); @@ -2812,8 +2866,23 @@ njs_parser_call_expression_after(njs_parser_t *parser, break; } - return njs_parser_after(parser, current, NULL, 1, - njs_parser_call_expression_after); + return njs_parser_after(parser, current, NULL, 1, after); +} + + +static njs_int_t +njs_parser_call_expression_after(njs_parser_t *parser, + njs_lexer_token_t *token, njs_queue_link_t *current) +{ + /* + * Arguments + * [ Expression ] + * . IdentifierName + * TemplateLiteral + */ + + return njs_parser_call_or_property_after(parser, token, current, + njs_parser_call_expression_after); } @@ -2939,10 +3008,21 @@ njs_parser_argument_list_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) +{ + return njs_parser_right_link_pop(parser); +} + + 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); } @@ -2956,8 +3036,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->left = parser->node; + opt->left->dest = opt; + + ref = njs_parser_node_new(parser, NJS_TOKEN_OBJECT_VALUE); + if (ref == NULL) { + return NJS_ERROR; + } + + ref->token_line = token->line; + ref->u.object = parser->node; + opt->u.object = ref; + 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); } @@ -2968,17 +3073,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) { @@ -2994,7 +3094,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) { @@ -3003,42 +3104,82 @@ njs_parser_optional_chain(njs_parser_t *parser, njs_lexer_token_t *token, switch (token->type) { case NJS_TOKEN_OPEN_PARENTHESIS: - func = njs_parser_create_call(parser, parser->node, 0); + func = njs_parser_optional_chain_method_call(parser, + parser->node->u.object, + token->line); if (func == NULL) { + func = njs_parser_create_call(parser, parser->node, 0); + if (func == NULL) { + return NJS_ERROR; + } + } + + ret = njs_parser_call_arguments(parser, token, current, func, + njs_parser_left_hand_side_expression_node); + if (ret != NJS_OK) { return NJS_ERROR; } - func->token_line = token->line; - parser->node = func; + return njs_parser_after(parser, current, NULL, 1, + njs_parser_optional_chain_after); - njs_lexer_consume_token(parser->lexer, 2); - njs_parser_next(parser, njs_parser_arguments); + case NJS_TOKEN_OPEN_BRACKET: + node = njs_parser_node_new(parser, NJS_TOKEN_PROPERTY); + if (node == NULL) { + return NJS_ERROR; + } - ret = njs_parser_after(parser, current, func, 1, - njs_parser_left_hand_side_expression_node); + 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; } - break; + return njs_parser_after(parser, current, NULL, 1, + njs_parser_optional_chain_after); default: - ret = njs_parser_property(parser, token, current); - - switch (ret) { - case NJS_DONE: - case NJS_DECLINED: + 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; + } } @@ -3046,9 +3187,6 @@ static njs_int_t njs_parser_optional_chain_after(njs_parser_t *parser, njs_lexer_token_t *token, njs_queue_link_t *current) { - njs_int_t ret; - njs_parser_node_t *func; - /* * OptionalChain Arguments * OptionalChain [ Expression ] @@ -3056,49 +3194,8 @@ njs_parser_optional_chain_after(njs_parser_t *parser, njs_lexer_token_t *token, * OptionalChain TemplateLiteral */ - switch (token->type) { - case NJS_TOKEN_OPEN_PARENTHESIS: - func = njs_parser_create_call(parser, parser->node, 0); - if (func == NULL) { - return NJS_ERROR; - } - - func->token_line = token->line; - parser->node = func; - - njs_lexer_consume_token(parser->lexer, 1); - njs_parser_next(parser, njs_parser_arguments); - - ret = njs_parser_after(parser, current, func, 1, - njs_parser_left_hand_side_expression_node); - if (ret != NJS_OK) { - return NJS_ERROR; - } - - break; - - default: - ret = njs_parser_property(parser, token, current); - - switch (ret) { - case NJS_AGAIN: - return NJS_OK; - - case NJS_DONE: - return njs_parser_stack_pop(parser); - - case NJS_DECLINED: - return njs_parser_failed(parser); - - default: - break; - } - - break; - } - - return njs_parser_after(parser, current, NULL, 1, - njs_parser_optional_chain_after); + return njs_parser_call_or_property_after(parser, token, current, + njs_parser_optional_chain_after); } @@ -3231,6 +3328,21 @@ njs_parser_left_hand_side_expression_after(njs_parser_t *parser, /* OptionalExpression */ case NJS_TOKEN_CONDITIONAL: + token = njs_lexer_peek_token(parser->lexer, token, 0); + if (token == NULL) { + return NJS_ERROR; + } + + if (token->type == NJS_TOKEN_DOT + && parser->node != NULL + && parser->node->ctor) + { + njs_parser_syntax_error(parser, + "Optional chaining cannot be used " + "with new"); + return NJS_DONE; + } + njs_parser_next(parser, njs_parser_optional_expression_after); break; @@ -3546,6 +3658,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"); @@ -9465,6 +9587,7 @@ 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_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 c89e7f12..32a1890d 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 5170f29f..2cdfd23b 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 59f95e34..6e5fd2a3 100644 --- a/src/test/njs_unit_test.c +++ b/src/test/njs_unit_test.c @@ -1503,6 +1503,218 @@ 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 = {m: function() {return 42}}; (o?.m)()"), + njs_str("42") }, + + { njs_str("var o = null; (o?.m)()"), + njs_str("TypeError: undefined is not a function") }, + + { 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") }, + + /* Optional chaining: nested property + optional call. */ + + { njs_str("var o = {a: {m: function() {return 42}}};" + "o.a.m?.()"), + njs_str("42") }, + + { njs_str("var o = {a: {x: 7, m: function() {return this.x}}};" + "o.a.m?.()"), + njs_str("7") }, + + { njs_str("var o = {a: {m: null}}; o.a.m?.()"), + njs_str("undefined") }, + + { njs_str("var o = {a: {m: function() {return 42}}};" + "var k = 'a'; o[k].m?.()"), + njs_str("42") }, + + { njs_str("var o = {a: {m: function() {return 42}}};" + "var e = 'a'; o?.[e].m?.()"), + njs_str("42") }, + + { njs_str("var o = null; o?.[0].m?.()"), + njs_str("undefined") }, + + { njs_str("var o = {a: {b: {m: function() {return 99}}}};" + "o.a.b.m?.()"), + njs_str("99") }, + + /* Optional chaining: chained call with continuation. */ + + { njs_str("var o = {a: function() { return {b: 42}; }};" + "o?.a().b"), + njs_str("42") }, + + { njs_str("var o = null; o?.a().b"), + njs_str("undefined") }, + + { njs_str("var o = {a: function() { return {b: 42}; }};" + "o?.a?.().b"), + njs_str("42") }, + + { njs_str("var o = null; o?.a?.().b"), + njs_str("undefined") }, + + /* Optional chaining: ternary disambiguation. */ + + { njs_str("var r = 1 ? .5 : 2; r"), + njs_str("0.5") }, + + { njs_str("var r = 0 ? .5 : 2; r"), + njs_str("2") }, + + { njs_str("var a = {b: 1}; a?.b : 2"), + njs_str("SyntaxError: Unexpected token \":\"") }, + + { njs_str("var o = {c: 3}; var r = o ? o?.c : 0; r"), + njs_str("3") }, + + /* Optional chaining: not a valid assignment target. */ + + { njs_str("var o = {a: 1}; o?.a = 2"), + njs_str("ReferenceError: Invalid left-hand side in assignment") }, + + { njs_str("var o = {a: 1}; o?.a++"), + njs_str("ReferenceError: Invalid left-hand side in postfix operation") }, + + { njs_str("var o = {a: 1}; ++o?.a"), + njs_str("ReferenceError: Invalid left-hand side in prefix operation") }, + + { njs_str("function F(){}; new F?.()"), + njs_str("SyntaxError: Optional chaining cannot be used with new") }, + + { njs_str("var o = {m: function() {}}; new o?.m()"), + njs_str("SyntaxError: Optional chaining cannot be used with new") }, + + { njs_str("var o = {m: function() {}}; new o.m?.()"), + njs_str("SyntaxError: Optional chaining cannot be used with new") }, + { njs_str("var a = true; a = -~!a"), njs_str("1") }, -- 2.47.3