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;
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,
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:
}
+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)
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. */
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);
}
NJS_TOKEN_PROPERTY_SETTER,
NJS_TOKEN_PROTO_INIT,
+ NJS_TOKEN_OPTIONAL_CHAIN,
+
NJS_TOKEN_ARRAY,
NJS_TOKEN_GRAVE,
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,
}
+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)
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);
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);
}
}
+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);
}
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);
}
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) {
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) {
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;
+ }
}
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 ]
* 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);
}
/* 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;
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");
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);
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),
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); \
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,
{ 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") },