]> git.kaiwu.me - njs.git/commitdiff
Implement optional chaining.
authorDmitry Volyntsev <xeioex@nginx.com>
Sat, 28 Feb 2026 07:42:40 +0000 (23:42 -0800)
committerDmitry Volyntsev <xeioexception@gmail.com>
Mon, 2 Mar 2026 21:01:04 +0000 (13:01 -0800)
src/njs_disassembler.c
src/njs_generator.c
src/njs_lexer.h
src/njs_parser.c
src/njs_vmcode.c
src/njs_vmcode.h
src/test/njs_unit_test.c

index 72ea63b9a6dccc71fa31b6fdbed0f3ba0c7eca28..b1e7aa50b4e4653cd07c2206aff1dd0e6be8e864 100644 (file)
@@ -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;
 
index d651a99674666ba2dc94a6b9befe6384fb4199b4..6bc09115f3dff5820db37c8000fed346141c951d 100644 (file)
@@ -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);
     }
 
index 3728e2536c64e79665d90d21fffd0b6f7376bdd6..5770e1c56b430407bb2036fd004610264e4f7d1b 100644 (file)
@@ -135,6 +135,8 @@ typedef enum {
     NJS_TOKEN_PROPERTY_SETTER,
     NJS_TOKEN_PROTO_INIT,
 
+    NJS_TOKEN_OPTIONAL_CHAIN,
+
     NJS_TOKEN_ARRAY,
 
     NJS_TOKEN_GRAVE,
index 16f9df4f1e700b97d832027c3778c755a2b0ac87..e5ec98cc7bdf4ca2fc65d0d3a4a3d6a5c43adb1d 100644 (file)
@@ -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);
index c89e7f12f03b10ed4c36b201d6d05282077f367f..32a1890d99a4a14c0aea5793dde1abc2386b5577 100644 (file)
@@ -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);                \
index 5170f29fce708694c5847945f3200dabf3013267..2cdfd23b8b95657cffc99fd62e428a16e9f615fa 100644 (file)
@@ -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,
index 59f95e34b10913448e6f365834a553a2512f2169..6e5fd2a3761576f7057a59f291abe3ff0109a8dd 100644 (file)
@@ -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") },