From 3fe763ee442f102a7050c951e8b2436d1121489e Mon Sep 17 00:00:00 2001 From: Dmitry Volyntsev Date: Fri, 27 Feb 2026 17:41:26 -0800 Subject: [PATCH 1/9] Parser: refactor call argument setup. --- src/njs_parser.c | 48 ++++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/src/njs_parser.c b/src/njs_parser.c index e2d6f388d..0f0ef751c 100644 --- a/src/njs_parser.c +++ b/src/njs_parser.c @@ -91,6 +91,9 @@ static njs_int_t njs_parser_member_expression_new_args(njs_parser_t *parser, static njs_parser_node_t *njs_parser_create_call(njs_parser_t *parser, njs_parser_node_t *node, uint8_t ctor); +static njs_int_t njs_parser_call_arguments(njs_parser_t *parser, + njs_lexer_token_t *token, njs_queue_link_t *current, + njs_parser_node_t *func, njs_parser_state_func_t after); 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, @@ -2615,14 +2618,8 @@ njs_parser_member_expression_new_after(njs_parser_t *parser, 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); - - return njs_parser_after(parser, current, func, 1, - njs_parser_member_expression_new_args); + return njs_parser_call_arguments(parser, token, current, func, + njs_parser_member_expression_new_args); } @@ -2681,6 +2678,21 @@ njs_parser_create_call(njs_parser_t *parser, njs_parser_node_t *node, } +static njs_int_t +njs_parser_call_arguments(njs_parser_t *parser, njs_lexer_token_t *token, + njs_queue_link_t *current, njs_parser_node_t *func, + njs_parser_state_func_t after) +{ + func->token_line = token->line; + parser->node = func; + + njs_lexer_consume_token(parser->lexer, 1); + njs_parser_next(parser, njs_parser_arguments); + + return njs_parser_after(parser, current, func, 1, after); +} + + static njs_int_t njs_parser_call_expression(njs_parser_t *parser, njs_lexer_token_t *token, njs_queue_link_t *current) @@ -2737,14 +2749,8 @@ njs_parser_call_expression_args(njs_parser_t *parser, njs_lexer_token_t *token, 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); - - return njs_parser_after(parser, current, func, 1, - njs_parser_left_hand_side_expression_node); + return njs_parser_call_arguments(parser, token, current, func, + njs_parser_left_hand_side_expression_node); } @@ -2769,14 +2775,8 @@ njs_parser_call_expression_after(njs_parser_t *parser, 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); + ret = njs_parser_call_arguments(parser, token, current, func, + njs_parser_left_hand_side_expression_node); if (ret != NJS_OK) { return NJS_ERROR; } From 0ce65f7f0031e86a1a2c03927123488614ae47c6 Mon Sep 17 00:00:00 2001 From: Dmitry Volyntsev Date: Sat, 28 Feb 2026 15:52:09 -0800 Subject: [PATCH 2/9] Parser: factor right-link pop helper. --- src/njs_parser.c | 57 ++++++++++++++++++------------------------------ 1 file changed, 21 insertions(+), 36 deletions(-) diff --git a/src/njs_parser.c b/src/njs_parser.c index 0f0ef751c..16f9df4f1 100644 --- a/src/njs_parser.c +++ b/src/njs_parser.c @@ -94,6 +94,8 @@ static njs_parser_node_t *njs_parser_create_call(njs_parser_t *parser, static njs_int_t njs_parser_call_arguments(njs_parser_t *parser, njs_lexer_token_t *token, njs_queue_link_t *current, 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_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, @@ -2447,10 +2449,7 @@ njs_parser_member_expression_bracket(njs_parser_t *parser, njs_lexer_consume_token(parser->lexer, 1); - parser->target->right = parser->node; - parser->node = parser->target; - - return njs_parser_stack_pop(parser); + return njs_parser_right_link_pop(parser); } @@ -2693,6 +2692,16 @@ njs_parser_call_arguments(njs_parser_t *parser, njs_lexer_token_t *token, } +static njs_int_t +njs_parser_right_link_pop(njs_parser_t *parser) +{ + parser->target->right = parser->node; + parser->node = parser->target; + + return njs_parser_stack_pop(parser); +} + + static njs_int_t njs_parser_call_expression(njs_parser_t *parser, njs_lexer_token_t *token, njs_queue_link_t *current) @@ -3608,10 +3617,7 @@ njs_parser_await_after(njs_parser_t *parser, njs_lexer_token_t *token, return njs_parser_failed(parser); } - parser->target->right = parser->node; - parser->node = parser->target; - - return njs_parser_stack_pop(parser); + return njs_parser_right_link_pop(parser); } @@ -4522,10 +4528,7 @@ static njs_int_t njs_parser_assignment_operator_after(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); + return njs_parser_right_link_pop(parser); } @@ -5407,10 +5410,7 @@ static njs_int_t njs_parser_do_while_semicolon(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); + return njs_parser_right_link_pop(parser); } @@ -5581,10 +5581,7 @@ static njs_int_t njs_parser_after_expr(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); + return njs_parser_right_link_pop(parser); } @@ -6086,10 +6083,7 @@ static njs_int_t njs_parser_for_in_statement_after(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); + return njs_parser_right_link_pop(parser); } @@ -6342,10 +6336,7 @@ njs_parser_return_statement_after(njs_parser_t *parser, return njs_parser_failed(parser); } - parser->target->right = parser->node; - parser->node = parser->target; - - return njs_parser_stack_pop(parser); + return njs_parser_right_link_pop(parser); } @@ -6769,10 +6760,7 @@ njs_parser_throw_statement_after(njs_parser_t *parser, return njs_parser_failed(parser); } - parser->target->right = parser->node; - parser->node = parser->target; - - return njs_parser_stack_pop(parser); + return njs_parser_right_link_pop(parser); } @@ -8084,10 +8072,7 @@ njs_parser_export_after(njs_parser_t *parser, njs_lexer_token_t *token, return njs_parser_failed(parser); } - parser->target->right = parser->node; - parser->node = parser->target; - - return njs_parser_stack_pop(parser); + return njs_parser_right_link_pop(parser); } From 666dcac6f4faaf7979dfa6de2f1053875b74cb7a Mon Sep 17 00:00:00 2001 From: Dmitry Volyntsev Date: Sat, 28 Feb 2026 15:31:24 -0800 Subject: [PATCH 3/9] Generator: factor property assignment helpers. --- src/njs_generator.c | 311 ++++++++++++++++++++++++++------------------ 1 file changed, 183 insertions(+), 128 deletions(-) diff --git a/src/njs_generator.c b/src/njs_generator.c index bb443017d..8afb70124 100644 --- a/src/njs_generator.c +++ b/src/njs_generator.c @@ -238,6 +238,20 @@ static njs_int_t njs_generate_comma_expression_end(njs_vm_t *vm, static njs_int_t njs_generate_global_property_set(njs_vm_t *vm, njs_generator_t *generator, njs_parser_node_t *node_dst, njs_parser_node_t *node_src); +static njs_int_t njs_generate_preserve_property_lvalue(njs_vm_t *vm, + njs_generator_t *generator, njs_parser_node_t *lvalue, + njs_parser_node_t *expr); +static njs_index_t njs_generate_property_index(njs_vm_t *vm, + njs_generator_t *generator, njs_parser_node_t *node, + njs_parser_node_t *object, njs_parser_node_t *property); +static njs_vmcode_t njs_generate_property_get_opcode(njs_parser_node_t *node); +static njs_vmcode_t njs_generate_property_set_opcode(njs_parser_node_t *node); +static njs_int_t njs_generate_property_get(njs_vm_t *vm, + njs_generator_t *generator, njs_parser_node_t *node, njs_index_t value, + njs_index_t object, njs_index_t property); +static njs_int_t njs_generate_property_set(njs_vm_t *vm, + njs_generator_t *generator, njs_parser_node_t *node, njs_index_t value, + njs_index_t object, njs_index_t property); static njs_int_t njs_generate_assignment(njs_vm_t *vm, njs_generator_t *generator, njs_parser_node_t *node); static njs_int_t njs_generate_assignment_name(njs_vm_t *vm, @@ -3039,6 +3053,144 @@ njs_generate_global_property_set(njs_vm_t *vm, njs_generator_t *generator, } +static njs_int_t +njs_generate_preserve_property_lvalue(njs_vm_t *vm, + njs_generator_t *generator, njs_parser_node_t *lvalue, + njs_parser_node_t *expr) +{ + njs_index_t index, src; + njs_parser_node_t *object, *property; + njs_vmcode_move_t *move; + + if (!njs_slow_path(njs_parser_has_side_effect(expr))) { + return NJS_OK; + } + + object = lvalue->left; + property = lvalue->right; + + /* + * Preserve object and property values stored in variables in a case + * if the variables can be changed by side effects in expression. + */ + if (object->token_type == NJS_TOKEN_NAME) { + src = object->index; + + index = njs_generate_node_temp_index_get(vm, generator, object); + if (njs_slow_path(index == NJS_INDEX_ERROR)) { + return NJS_ERROR; + } + + njs_generate_code_move(generator, move, index, src, object); + } + + if (property->token_type == NJS_TOKEN_NAME) { + src = property->index; + + index = njs_generate_node_temp_index_get(vm, generator, property); + if (njs_slow_path(index == NJS_INDEX_ERROR)) { + return NJS_ERROR; + } + + njs_generate_code_move(generator, move, index, src, property); + } + + return NJS_OK; +} + + +static njs_index_t +njs_generate_property_index(njs_vm_t *vm, njs_generator_t *generator, + njs_parser_node_t *node, njs_parser_node_t *object, + njs_parser_node_t *property) +{ + njs_index_t prop_index; + njs_vmcode_3addr_t *to_property_key; + + prop_index = property->index; + + if (!njs_parser_is_primitive(property)) { + prop_index = njs_generate_node_temp_index_get(vm, generator, node); + if (njs_slow_path(prop_index == NJS_INDEX_ERROR)) { + return NJS_INDEX_ERROR; + } + + njs_generate_code(generator, njs_vmcode_3addr_t, to_property_key, + NJS_VMCODE_TO_PROPERTY_KEY_CHK, property); + + to_property_key->src2 = object->index; + to_property_key->src1 = property->index; + to_property_key->dst = prop_index; + } + + return prop_index; +} + + +static njs_vmcode_t +njs_generate_property_get_opcode(njs_parser_node_t *node) +{ + if (node->token_type == NJS_TOKEN_STRING + || (node->token_type == NJS_TOKEN_NUMBER + && node->u.value.atom_id != NJS_ATOM_STRING_unknown)) + { + return NJS_VMCODE_PROPERTY_ATOM_GET; + } + + return NJS_VMCODE_PROPERTY_GET; +} + + +static njs_vmcode_t +njs_generate_property_set_opcode(njs_parser_node_t *node) +{ + if (node->token_type == NJS_TOKEN_STRING + || (node->token_type == NJS_TOKEN_NUMBER + && node->u.value.atom_id != NJS_ATOM_STRING_unknown)) + { + return NJS_VMCODE_PROPERTY_ATOM_SET; + } + + return NJS_VMCODE_PROPERTY_SET; +} + + +static njs_int_t +njs_generate_property_get(njs_vm_t *vm, njs_generator_t *generator, + njs_parser_node_t *node, njs_index_t value, njs_index_t object, + njs_index_t property) +{ + njs_vmcode_prop_get_t *prop_get; + + njs_generate_code(generator, njs_vmcode_prop_get_t, prop_get, + njs_generate_property_get_opcode(node), node); + + prop_get->value = value; + prop_get->object = object; + prop_get->property = property; + + return NJS_OK; +} + + +static njs_int_t +njs_generate_property_set(njs_vm_t *vm, njs_generator_t *generator, + njs_parser_node_t *node, njs_index_t value, njs_index_t object, + njs_index_t property) +{ + njs_vmcode_prop_set_t *prop_set; + + njs_generate_code(generator, njs_vmcode_prop_set_t, prop_set, + njs_generate_property_set_opcode(node), node); + + prop_set->value = value; + prop_set->object = object; + prop_set->property = property; + + return NJS_OK; +} + + static njs_int_t njs_generate_assignment(njs_vm_t *vm, njs_generator_t *generator, njs_parser_node_t *node) @@ -3134,42 +3286,14 @@ static njs_int_t njs_generate_assignment_prop(njs_vm_t *vm, njs_generator_t *generator, njs_parser_node_t *node) { - njs_index_t index, src; - njs_parser_node_t *lvalue, *expr, *object, *property; - njs_vmcode_move_t *move; + njs_int_t ret; + njs_parser_node_t *expr; - lvalue = node->left; expr = node->right; - - object = lvalue->left; - property = lvalue->right; - - if (njs_slow_path(njs_parser_has_side_effect(expr))) { - /* - * Preserve object and property values stored in variables in a case - * if the variables can be changed by side effects in expression. - */ - if (object->token_type == NJS_TOKEN_NAME) { - src = object->index; - - index = njs_generate_node_temp_index_get(vm, generator, object); - if (njs_slow_path(index == NJS_INDEX_ERROR)) { - return NJS_ERROR; - } - - njs_generate_code_move(generator, move, index, src, object); - } - - if (property->token_type == NJS_TOKEN_NAME) { - src = property->index; - - index = njs_generate_node_temp_index_get(vm, generator, property); - if (njs_slow_path(index == NJS_INDEX_ERROR)) { - return NJS_ERROR; - } - - njs_generate_code_move(generator, move, index, src, property); - } + ret = njs_generate_preserve_property_lvalue(vm, generator, node->left, + expr); + if (njs_slow_path(ret != NJS_OK)) { + return ret; } njs_generator_next(generator, njs_generate, expr); @@ -3243,18 +3367,10 @@ njs_generate_assignment_end(njs_vm_t *vm, njs_generator_t *generator, break; default: - if (property->token_type == NJS_TOKEN_STRING - || (property->token_type == NJS_TOKEN_NUMBER - && property->u.value.atom_id != NJS_ATOM_STRING_unknown)) - { - opcode = NJS_VMCODE_PROPERTY_ATOM_SET; + opcode = njs_generate_property_set_opcode(property); - } else { - opcode = NJS_VMCODE_PROPERTY_SET; - } - - njs_generate_code(generator, njs_vmcode_prop_set_t, prop_set, - opcode, expr); + njs_generate_code(generator, njs_vmcode_prop_set_t, prop_set, opcode, + expr); } prop_set->value = expr->index; @@ -3400,59 +3516,24 @@ static njs_int_t njs_generate_operation_assignment_prop(njs_vm_t *vm, njs_generator_t *generator, njs_parser_node_t *node) { - njs_index_t index, src, prop_index; - njs_vmcode_t opcode; + njs_index_t index, prop_index; + njs_int_t ret; njs_parser_node_t *lvalue, *object, *property; - njs_vmcode_move_t *move; - njs_vmcode_3addr_t *to_property_key; - njs_vmcode_prop_get_t *prop_get; lvalue = node->left; object = lvalue->left; property = lvalue->right; - if (njs_slow_path(njs_parser_has_side_effect(node->right))) { - /* - * Preserve object and property values stored in variables in a case - * if the variables can be changed by side effects in expression. - */ - if (object->token_type == NJS_TOKEN_NAME) { - src = object->index; - - index = njs_generate_node_temp_index_get(vm, generator, object); - if (njs_slow_path(index == NJS_INDEX_ERROR)) { - return NJS_ERROR; - } - - njs_generate_code_move(generator, move, index, src, object); - } - - if (property->token_type == NJS_TOKEN_NAME) { - src = property->index; - - index = njs_generate_node_temp_index_get(vm, generator, property); - if (njs_slow_path(index == NJS_INDEX_ERROR)) { - return NJS_ERROR; - } - - njs_generate_code_move(generator, move, index, src, property); - } + ret = njs_generate_preserve_property_lvalue(vm, generator, lvalue, + node->right); + if (njs_slow_path(ret != NJS_OK)) { + return ret; } - prop_index = property->index; - - if (!njs_parser_is_primitive(property)) { - prop_index = njs_generate_node_temp_index_get(vm, generator, node); - if (njs_slow_path(prop_index == NJS_INDEX_ERROR)) { - return NJS_ERROR; - } - - njs_generate_code(generator, njs_vmcode_3addr_t, to_property_key, - NJS_VMCODE_TO_PROPERTY_KEY_CHK, property); - - to_property_key->src2 = object->index; - to_property_key->src1 = property->index; - to_property_key->dst = prop_index; + prop_index = njs_generate_property_index(vm, generator, node, object, + property); + if (njs_slow_path(prop_index == NJS_INDEX_ERROR)) { + return NJS_ERROR; } index = njs_generate_node_temp_index_get(vm, generator, node); @@ -3460,23 +3541,12 @@ njs_generate_operation_assignment_prop(njs_vm_t *vm, njs_generator_t *generator, return NJS_ERROR; } - if (property->token_type == NJS_TOKEN_STRING - || (property->token_type == NJS_TOKEN_NUMBER - && property->u.value.atom_id != NJS_ATOM_STRING_unknown)) - { - opcode = NJS_VMCODE_PROPERTY_ATOM_GET; - - } else { - opcode = NJS_VMCODE_PROPERTY_GET; + ret = njs_generate_property_get(vm, generator, property, index, + object->index, prop_index); + if (njs_slow_path(ret != NJS_OK)) { + return ret; } - njs_generate_code(generator, njs_vmcode_prop_get_t, prop_get, - opcode, property); - - prop_get->value = index; - prop_get->object = object->index; - prop_get->property = prop_index; - njs_generator_next(generator, njs_generate, node->right); return njs_generator_after(vm, generator, @@ -3490,12 +3560,10 @@ static njs_int_t njs_generate_operation_assignment_end(njs_vm_t *vm, njs_generator_t *generator, njs_parser_node_t *node) { - njs_int_t ret; - njs_index_t prop_index; - njs_vmcode_t opcode; - njs_parser_node_t *lvalue, *expr, *prop; - njs_vmcode_3addr_t *code; - njs_vmcode_prop_set_t *prop_set; + njs_int_t ret; + njs_index_t prop_index; + njs_parser_node_t *lvalue, *expr; + njs_vmcode_3addr_t *code; lvalue = node->left; expr = node->right; @@ -3508,25 +3576,12 @@ njs_generate_operation_assignment_end(njs_vm_t *vm, njs_generator_t *generator, code->src1 = node->index; code->src2 = expr->index; - prop = lvalue->right; - - if (prop->token_type == NJS_TOKEN_STRING - || (prop->token_type == NJS_TOKEN_NUMBER - && prop->u.value.atom_id != NJS_ATOM_STRING_unknown)) - { - opcode = NJS_VMCODE_PROPERTY_ATOM_SET; - - } else { - opcode = NJS_VMCODE_PROPERTY_SET; + ret = njs_generate_property_set(vm, generator, lvalue->right, node->index, + lvalue->left->index, prop_index); + if (njs_slow_path(ret != NJS_OK)) { + return ret; } - njs_generate_code(generator, njs_vmcode_prop_set_t, prop_set, - opcode, expr); - - prop_set->value = node->index; - prop_set->object = lvalue->left->index; - prop_set->property = prop_index; - ret = njs_generate_children_indexes_release(vm, generator, lvalue); if (njs_slow_path(ret != NJS_OK)) { return ret; From 187cafc57d88eef1c3203f10960c3c66e2644d42 Mon Sep 17 00:00:00 2001 From: Dmitry Volyntsev Date: Sat, 28 Feb 2026 15:31:47 -0800 Subject: [PATCH 4/9] Generator: factor test jump emission. --- src/njs_generator.c | 37 +++++++++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/src/njs_generator.c b/src/njs_generator.c index 8afb70124..38e1b358f 100644 --- a/src/njs_generator.c +++ b/src/njs_generator.c @@ -286,6 +286,9 @@ static njs_int_t njs_generate_template_literal(njs_vm_t *vm, njs_generator_t *generator, njs_parser_node_t *node); static njs_int_t njs_generate_template_literal_end(njs_vm_t *vm, njs_generator_t *generator, njs_parser_node_t *node); +static njs_int_t njs_generate_test_jump(njs_vm_t *vm, + njs_generator_t *generator, njs_parser_node_t *node, njs_vmcode_t opcode, + njs_index_t value, njs_index_t retval, njs_jump_off_t *jump_offset); static njs_int_t njs_generate_test_jump_expression(njs_vm_t *vm, njs_generator_t *generator, njs_parser_node_t *node); static njs_int_t njs_generate_test_jump_expression_after(njs_vm_t *vm, @@ -3820,6 +3823,24 @@ njs_generate_template_literal_end(njs_vm_t *vm, njs_generator_t *generator, } +static njs_int_t +njs_generate_test_jump(njs_vm_t *vm, njs_generator_t *generator, + njs_parser_node_t *node, njs_vmcode_t opcode, njs_index_t value, + njs_index_t retval, njs_jump_off_t *jump_offset) +{ + njs_vmcode_test_jump_t *test_jump; + + njs_generate_code(generator, njs_vmcode_test_jump_t, test_jump, opcode, + node); + + *jump_offset = njs_code_offset(generator, test_jump); + test_jump->value = value; + test_jump->retval = retval; + + return NJS_OK; +} + + static njs_int_t njs_generate_test_jump_expression(njs_vm_t *vm, njs_generator_t *generator, njs_parser_node_t *node) @@ -3837,20 +3858,20 @@ static njs_int_t njs_generate_test_jump_expression_after(njs_vm_t *vm, njs_generator_t *generator, njs_parser_node_t *node) { - njs_jump_off_t jump_offset; - njs_vmcode_test_jump_t *test_jump; - - njs_generate_code(generator, njs_vmcode_test_jump_t, test_jump, - node->u.operation, node); - jump_offset = njs_code_offset(generator, test_jump); - test_jump->value = node->left->index; + njs_int_t ret; + njs_jump_off_t jump_offset; 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; + ret = njs_generate_test_jump(vm, generator, node, node->u.operation, + node->left->index, node->index, + &jump_offset); + if (njs_slow_path(ret != NJS_OK)) { + return ret; + } njs_generator_next(generator, njs_generate, node->right); From 2684e7816098dd69e93df7a61038ae594df9c20c Mon Sep 17 00:00:00 2001 From: Dmitry Volyntsev Date: Sat, 28 Feb 2026 15:32:20 -0800 Subject: [PATCH 5/9] Generator: factor property assignment reads. --- src/njs_generator.c | 74 +++++++++++++++++++++++++++++---------------- 1 file changed, 48 insertions(+), 26 deletions(-) diff --git a/src/njs_generator.c b/src/njs_generator.c index 38e1b358f..ceafb62ea 100644 --- a/src/njs_generator.c +++ b/src/njs_generator.c @@ -241,6 +241,9 @@ static njs_int_t njs_generate_global_property_set(njs_vm_t *vm, static njs_int_t njs_generate_preserve_property_lvalue(njs_vm_t *vm, njs_generator_t *generator, njs_parser_node_t *lvalue, njs_parser_node_t *expr); +static njs_int_t njs_generate_read_property_assignment(njs_vm_t *vm, + njs_generator_t *generator, njs_parser_node_t *node, + njs_parser_node_t *expr, njs_index_t *prop_index); static njs_index_t njs_generate_property_index(njs_vm_t *vm, njs_generator_t *generator, njs_parser_node_t *node, njs_parser_node_t *object, njs_parser_node_t *property); @@ -3102,6 +3105,47 @@ njs_generate_preserve_property_lvalue(njs_vm_t *vm, } +static njs_int_t +njs_generate_read_property_assignment(njs_vm_t *vm, + njs_generator_t *generator, njs_parser_node_t *node, + njs_parser_node_t *expr, njs_index_t *prop_index) +{ + njs_int_t ret; + njs_index_t index; + njs_parser_node_t *lvalue, *object, *property; + + lvalue = node->left; + object = lvalue->left; + property = lvalue->right; + + ret = njs_generate_preserve_property_lvalue(vm, generator, lvalue, expr); + if (njs_slow_path(ret != NJS_OK)) { + return ret; + } + + *prop_index = njs_generate_property_index(vm, generator, node, object, + property); + if (njs_slow_path(*prop_index == NJS_INDEX_ERROR)) { + return NJS_ERROR; + } + + index = njs_generate_node_temp_index_get(vm, generator, node); + if (njs_slow_path(index == NJS_INDEX_ERROR)) { + return NJS_ERROR; + } + + ret = njs_generate_property_get(vm, generator, property, index, + object->index, *prop_index); + if (njs_slow_path(ret != NJS_OK)) { + return ret; + } + + node->index = index; + + return NJS_OK; +} + + static njs_index_t njs_generate_property_index(njs_vm_t *vm, njs_generator_t *generator, njs_parser_node_t *node, njs_parser_node_t *object, @@ -3519,33 +3563,11 @@ static njs_int_t njs_generate_operation_assignment_prop(njs_vm_t *vm, njs_generator_t *generator, njs_parser_node_t *node) { - njs_index_t index, prop_index; - njs_int_t ret; - njs_parser_node_t *lvalue, *object, *property; - - lvalue = node->left; - object = lvalue->left; - property = lvalue->right; + njs_int_t ret; + njs_index_t prop_index; - ret = njs_generate_preserve_property_lvalue(vm, generator, lvalue, - node->right); - if (njs_slow_path(ret != NJS_OK)) { - return ret; - } - - prop_index = njs_generate_property_index(vm, generator, node, object, - property); - if (njs_slow_path(prop_index == NJS_INDEX_ERROR)) { - return NJS_ERROR; - } - - index = njs_generate_node_temp_index_get(vm, generator, node); - if (njs_slow_path(index == NJS_INDEX_ERROR)) { - return NJS_ERROR; - } - - ret = njs_generate_property_get(vm, generator, property, index, - object->index, prop_index); + ret = njs_generate_read_property_assignment(vm, generator, node, + node->right, &prop_index); if (njs_slow_path(ret != NJS_OK)) { return ret; } From b8134713f145357a9e1e3b87c4e29565d963fb31 Mon Sep 17 00:00:00 2001 From: Dmitry Volyntsev Date: Sat, 28 Feb 2026 16:16:22 -0800 Subject: [PATCH 6/9] Generator: factor property lvalue setup. --- src/njs_generator.c | 90 +++++++++++++++++++++------------------------ 1 file changed, 41 insertions(+), 49 deletions(-) diff --git a/src/njs_generator.c b/src/njs_generator.c index ceafb62ea..d651a9967 100644 --- a/src/njs_generator.c +++ b/src/njs_generator.c @@ -238,6 +238,9 @@ static njs_int_t njs_generate_comma_expression_end(njs_vm_t *vm, static njs_int_t njs_generate_global_property_set(njs_vm_t *vm, njs_generator_t *generator, njs_parser_node_t *node_dst, njs_parser_node_t *node_src); +static njs_int_t njs_generate_property_lvalue(njs_vm_t *vm, + njs_generator_t *generator, njs_parser_node_t *node, + njs_generator_state_func_t after, void *ctx, size_t ctx_size); static njs_int_t njs_generate_preserve_property_lvalue(njs_vm_t *vm, njs_generator_t *generator, njs_parser_node_t *lvalue, njs_parser_node_t *expr); @@ -3059,6 +3062,35 @@ njs_generate_global_property_set(njs_vm_t *vm, njs_generator_t *generator, } +static njs_int_t +njs_generate_property_lvalue(njs_vm_t *vm, njs_generator_t *generator, + njs_parser_node_t *node, njs_generator_state_func_t after, void *ctx, + size_t ctx_size) +{ + njs_int_t ret; + njs_parser_node_t *lvalue; + + lvalue = node->left; + + /* Object. */ + + njs_generator_next(generator, njs_generate, lvalue->left); + + ret = njs_generator_after(vm, generator, + njs_queue_first(&generator->stack), node, + after, ctx, ctx_size); + if (njs_slow_path(ret != NJS_OK)) { + return ret; + } + + /* Property. */ + + return njs_generator_after(vm, generator, + njs_queue_first(&generator->stack), + lvalue->right, njs_generate, NULL, 0); +} + + static njs_int_t njs_generate_preserve_property_lvalue(njs_vm_t *vm, njs_generator_t *generator, njs_parser_node_t *lvalue, @@ -3278,22 +3310,9 @@ njs_generate_assignment(njs_vm_t *vm, njs_generator_t *generator, /* lvalue->token == NJS_TOKEN_PROPERTY(_INIT) */ - /* Object. */ - - njs_generator_next(generator, njs_generate, lvalue->left); - - ret = njs_generator_after(vm, generator, - njs_queue_first(&generator->stack), node, - njs_generate_assignment_prop, NULL, 0); - if (njs_slow_path(ret != NJS_OK)) { - return ret; - } - - /* Property. */ - - return njs_generator_after(vm, generator, - njs_queue_first(&generator->stack), - lvalue->right, njs_generate, NULL, 0); + return njs_generate_property_lvalue(vm, generator, node, + njs_generate_assignment_prop, + NULL, 0); } @@ -3500,22 +3519,9 @@ njs_generate_operation_assignment(njs_vm_t *vm, njs_generator_t *generator, /* lvalue->token == NJS_TOKEN_PROPERTY */ - /* Object. */ - - njs_generator_next(generator, njs_generate, lvalue->left); - - ret = njs_generator_after(vm, generator, - njs_queue_first(&generator->stack), node, - njs_generate_operation_assignment_prop, NULL, 0); - if (njs_slow_path(ret != NJS_OK)) { - return ret; - } - - /* Property. */ - - return njs_generator_after(vm, generator, - njs_queue_first(&generator->stack), - lvalue->right, njs_generate, NULL, 0); + return njs_generate_property_lvalue(vm, generator, node, + njs_generate_operation_assignment_prop, + NULL, 0); } @@ -4189,23 +4195,9 @@ njs_generate_inc_dec_operation(njs_vm_t *vm, njs_generator_t *generator, /* lvalue->token == NJS_TOKEN_PROPERTY */ - /* Object. */ - - njs_generator_next(generator, njs_generate, lvalue->left); - - ret = njs_generator_after(vm, generator, - njs_queue_first(&generator->stack), node, - njs_generate_inc_dec_operation_prop, - &post, sizeof(njs_bool_t)); - if (njs_slow_path(ret != NJS_OK)) { - return ret; - } - - /* Property. */ - - return njs_generator_after(vm, generator, - njs_queue_first(&generator->stack), - lvalue->right, njs_generate, NULL, 0); + return njs_generate_property_lvalue(vm, generator, node, + njs_generate_inc_dec_operation_prop, + &post, sizeof(njs_bool_t)); } From 5bce7c906a7b86fd5762b2471ab49056a35f3d01 Mon Sep 17 00:00:00 2001 From: Dmitry Volyntsev Date: Fri, 27 Feb 2026 23:42:40 -0800 Subject: [PATCH 7/9] 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 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 d651a9967..6bc09115f 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 3728e2536..5770e1c56 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 16f9df4f1..e5ec98cc7 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 c89e7f12f..32a1890d9 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 59f95e34b..6e5fd2a37 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") }, From 12bbb241fe55f68e4e2ba376d1ed26c9c137eb4d Mon Sep 17 00:00:00 2001 From: Dmitry Volyntsev Date: Wed, 11 Feb 2026 07:13:06 -0800 Subject: [PATCH 8/9] Added support for ||= and &&= logical assignment operators. Unlike regular compound assignments (+=, -=), these operators short-circuit: the RHS is not evaluated and no assignment occurs if the logical condition is already satisfied. --- src/njs_generator.c | 175 +++++++++++++++++++++++++++++++++++++++ src/njs_lexer.c | 14 +++- src/njs_lexer.h | 2 + src/njs_parser.c | 12 +++ src/test/njs_unit_test.c | 122 +++++++++++++++++++++++++++ 5 files changed, 323 insertions(+), 2 deletions(-) diff --git a/src/njs_generator.c b/src/njs_generator.c index 6bc09115f..a3a0adafa 100644 --- a/src/njs_generator.c +++ b/src/njs_generator.c @@ -103,6 +103,12 @@ typedef struct { } njs_generator_try_ctx_t; +typedef struct { + njs_jump_off_t jump_offset; + njs_index_t prop_index; +} njs_generator_log_assign_ctx_t; + + static u_char *njs_generate_reserve(njs_vm_t *vm, njs_generator_t *generator, size_t size); static njs_int_t njs_generate_code_map(njs_vm_t *vm, njs_generator_t *generator, @@ -274,6 +280,12 @@ static njs_int_t njs_generate_operation_assignment_prop(njs_vm_t *vm, njs_generator_t *generator, njs_parser_node_t *node); static njs_int_t njs_generate_operation_assignment_end(njs_vm_t *vm, njs_generator_t *generator, njs_parser_node_t *node); +static njs_int_t njs_generate_logical_assignment(njs_vm_t *vm, + njs_generator_t *generator, njs_parser_node_t *node); +static njs_int_t njs_generate_logical_assignment_prop(njs_vm_t *vm, + njs_generator_t *generator, njs_parser_node_t *node); +static njs_int_t njs_generate_logical_assignment_end(njs_vm_t *vm, + njs_generator_t *generator, njs_parser_node_t *node); static njs_int_t njs_generate_object(njs_vm_t *vm, njs_generator_t *generator, njs_parser_node_t *node); static njs_int_t njs_generate_property_accessor(njs_vm_t *vm, @@ -651,6 +663,10 @@ njs_generate(njs_vm_t *vm, njs_generator_t *generator, njs_parser_node_t *node) case NJS_TOKEN_REMAINDER_ASSIGNMENT: return njs_generate_operation_assignment(vm, generator, node); + case NJS_TOKEN_LOGICAL_OR_ASSIGNMENT: + case NJS_TOKEN_LOGICAL_AND_ASSIGNMENT: + return njs_generate_logical_assignment(vm, generator, node); + case NJS_TOKEN_BITWISE_OR: case NJS_TOKEN_BITWISE_XOR: case NJS_TOKEN_BITWISE_AND: @@ -3631,6 +3647,165 @@ njs_generate_operation_assignment_end(njs_vm_t *vm, njs_generator_t *generator, } +static njs_int_t +njs_generate_logical_assignment(njs_vm_t *vm, njs_generator_t *generator, + njs_parser_node_t *node) +{ + njs_int_t ret; + njs_variable_t *var; + njs_parser_node_t *lvalue; + njs_vmcode_variable_t *var_code; + njs_generator_log_assign_ctx_t ctx; + + lvalue = node->left; + + if (lvalue->token_type == NJS_TOKEN_NAME) { + + ret = njs_generate_variable(vm, generator, lvalue, NJS_REFERENCE, + &var); + if (njs_slow_path(ret != NJS_OK)) { + return ret; + } + + ret = njs_generate_test_jump(vm, generator, node, node->u.operation, + lvalue->index, lvalue->index, + &ctx.jump_offset); + if (njs_slow_path(ret != NJS_OK)) { + return ret; + } + + if (var != NULL && var->type == NJS_VARIABLE_CONST) { + njs_generate_code(generator, njs_vmcode_variable_t, var_code, + NJS_VMCODE_ASSIGNMENT_ERROR, node); + var_code->dst = var->index; + + njs_code_set_jump_offset(generator, njs_vmcode_test_jump_t, + ctx.jump_offset); + + node->index = lvalue->index; + + return njs_generator_stack_pop(vm, generator, NULL); + } + + ctx.prop_index = NJS_INDEX_NONE; + + njs_generator_next(generator, njs_generate, node->right); + + return njs_generator_after(vm, generator, + njs_queue_first(&generator->stack), node, + njs_generate_logical_assignment_end, + &ctx, + sizeof(njs_generator_log_assign_ctx_t)); + } + + /* lvalue->token == NJS_TOKEN_PROPERTY */ + + /* Object. */ + + njs_generator_next(generator, njs_generate, lvalue->left); + + ret = njs_generator_after(vm, generator, + njs_queue_first(&generator->stack), node, + njs_generate_logical_assignment_prop, NULL, 0); + if (njs_slow_path(ret != NJS_OK)) { + return ret; + } + + /* Property. */ + + return njs_generator_after(vm, generator, + njs_queue_first(&generator->stack), + lvalue->right, njs_generate, NULL, 0); +} + + +static njs_int_t +njs_generate_logical_assignment_prop(njs_vm_t *vm, njs_generator_t *generator, + njs_parser_node_t *node) +{ + njs_int_t ret; + njs_generator_log_assign_ctx_t ctx; + + ret = njs_generate_read_property_assignment(vm, generator, node, + node->right, &ctx.prop_index); + if (njs_slow_path(ret != NJS_OK)) { + return ret; + } + + ret = njs_generate_test_jump(vm, generator, node, node->u.operation, + node->index, node->index, + &ctx.jump_offset); + if (njs_slow_path(ret != NJS_OK)) { + return ret; + } + + njs_generator_next(generator, njs_generate, node->right); + + return njs_generator_after(vm, generator, + njs_queue_first(&generator->stack), node, + njs_generate_logical_assignment_end, + &ctx, + sizeof(njs_generator_log_assign_ctx_t)); +} + + +static njs_int_t +njs_generate_logical_assignment_end(njs_vm_t *vm, + njs_generator_t *generator, njs_parser_node_t *node) +{ + njs_int_t ret; + njs_index_t index; + njs_parser_node_t *lvalue, *expr; + njs_vmcode_move_t *move; + njs_generator_log_assign_ctx_t *ctx; + + lvalue = node->left; + expr = node->right; + + ctx = generator->context; + + index = (lvalue->token_type == NJS_TOKEN_NAME) ? lvalue->index + : node->index; + + if (index != expr->index) { + njs_generate_code_move(generator, move, index, + expr->index, node); + } + + njs_code_set_jump_offset(generator, njs_vmcode_test_jump_t, + ctx->jump_offset); + + if (ctx->prop_index == NJS_INDEX_NONE) { + ret = njs_generate_global_property_set(vm, generator, lvalue, expr); + if (njs_slow_path(ret != NJS_OK)) { + return ret; + } + + node->index = lvalue->index; + + ret = njs_generate_node_index_release(vm, generator, expr); + if (njs_slow_path(ret != NJS_OK)) { + return ret; + } + + return njs_generator_stack_pop(vm, generator, generator->context); + } + + ret = njs_generate_property_set(vm, generator, lvalue->right, node->index, + lvalue->left->index, ctx->prop_index); + if (njs_slow_path(ret != NJS_OK)) { + return ret; + } + + ret = njs_generate_children_indexes_release(vm, generator, lvalue); + if (njs_slow_path(ret != NJS_OK)) { + return ret; + } + + return njs_generate_node_index_release_pop(vm, generator, expr); +} + + static njs_int_t njs_generate_object(njs_vm_t *vm, njs_generator_t *generator, njs_parser_node_t *node) diff --git a/src/njs_lexer.c b/src/njs_lexer.c index 7827aa0ca..30febb143 100644 --- a/src/njs_lexer.c +++ b/src/njs_lexer.c @@ -205,8 +205,13 @@ static const njs_lexer_multi_t njs_remainder_token[] = { }; +static const njs_lexer_multi_t njs_logical_and_assignment_token[] = { + { '=', NJS_TOKEN_LOGICAL_AND_ASSIGNMENT, 0, NULL }, +}; + + static const njs_lexer_multi_t njs_bitwise_and_token[] = { - { '&', NJS_TOKEN_LOGICAL_AND, 0, NULL }, + { '&', NJS_TOKEN_LOGICAL_AND, 1, njs_logical_and_assignment_token }, { '=', NJS_TOKEN_BITWISE_AND_ASSIGNMENT, 0, NULL }, }; @@ -216,8 +221,13 @@ static const njs_lexer_multi_t njs_bitwise_xor_token[] = { }; +static const njs_lexer_multi_t njs_logical_or_assignment_token[] = { + { '=', NJS_TOKEN_LOGICAL_OR_ASSIGNMENT, 0, NULL }, +}; + + static const njs_lexer_multi_t njs_bitwise_or_token[] = { - { '|', NJS_TOKEN_LOGICAL_OR, 0, NULL }, + { '|', NJS_TOKEN_LOGICAL_OR, 1, njs_logical_or_assignment_token }, { '=', NJS_TOKEN_BITWISE_OR_ASSIGNMENT, 0, NULL }, }; diff --git a/src/njs_lexer.h b/src/njs_lexer.h index 5770e1c56..331eb52eb 100644 --- a/src/njs_lexer.h +++ b/src/njs_lexer.h @@ -50,6 +50,8 @@ typedef enum { NJS_TOKEN_BITWISE_OR_ASSIGNMENT, NJS_TOKEN_BITWISE_XOR_ASSIGNMENT, NJS_TOKEN_BITWISE_AND_ASSIGNMENT, + NJS_TOKEN_LOGICAL_OR_ASSIGNMENT, + NJS_TOKEN_LOGICAL_AND_ASSIGNMENT, NJS_TOKEN_INCREMENT, NJS_TOKEN_DECREMENT, diff --git a/src/njs_parser.c b/src/njs_parser.c index e5ec98cc7..8d2a778eb 100644 --- a/src/njs_parser.c +++ b/src/njs_parser.c @@ -4608,6 +4608,16 @@ njs_parser_assignment_operator(njs_parser_t *parser, njs_lexer_token_t *token, operation = NJS_VMCODE_EXPONENTIATION; break; + case NJS_TOKEN_LOGICAL_OR_ASSIGNMENT: + njs_thread_log_debug("JS: ||="); + operation = NJS_VMCODE_TEST_IF_TRUE; + break; + + case NJS_TOKEN_LOGICAL_AND_ASSIGNMENT: + njs_thread_log_debug("JS: &&="); + operation = NJS_VMCODE_TEST_IF_FALSE; + break; + default: return njs_parser_stack_pop(parser); } @@ -9556,6 +9566,8 @@ njs_parser_serialize_node(njs_chb_t *chain, njs_parser_node_t *node) njs_token_serialize(NJS_TOKEN_BITWISE_OR_ASSIGNMENT); njs_token_serialize(NJS_TOKEN_BITWISE_XOR_ASSIGNMENT); njs_token_serialize(NJS_TOKEN_BITWISE_AND_ASSIGNMENT); + njs_token_serialize(NJS_TOKEN_LOGICAL_OR_ASSIGNMENT); + njs_token_serialize(NJS_TOKEN_LOGICAL_AND_ASSIGNMENT); njs_token_serialize(NJS_TOKEN_EQUAL); njs_token_serialize(NJS_TOKEN_NOT_EQUAL); njs_token_serialize(NJS_TOKEN_STRICT_EQUAL); diff --git a/src/test/njs_unit_test.c b/src/test/njs_unit_test.c index 6e5fd2a37..bff848684 100644 --- a/src/test/njs_unit_test.c +++ b/src/test/njs_unit_test.c @@ -1503,6 +1503,128 @@ static njs_unit_test_t njs_test[] = { njs_str("null ?? 0 || 1"), njs_str("SyntaxError: Unexpected token \"||\"") }, + /* Logical assignment: ||= */ + + { njs_str("var a = 0; a ||= 5; a"), + njs_str("5") }, + { njs_str("var a = 1; a ||= 5; a"), + njs_str("1") }, + { njs_str("var a = ''; a ||= 'x'; a"), + njs_str("x") }, + { njs_str("var a = 'y'; a ||= 'x'; a"), + njs_str("y") }, + { njs_str("var a = null; a ||= 42; a"), + njs_str("42") }, + { njs_str("var a = undefined; a ||= 1"), + njs_str("1") }, + + /* ||= short-circuit: RHS not evaluated */ + + { njs_str("var a = 1; var b = 0; a ||= (b = 2); b"), + njs_str("0") }, + { njs_str("var a = 0; var b = 0; a ||= (b = 2); b"), + njs_str("2") }, + + /* Logical assignment: &&= */ + + { njs_str("var a = 1; a &&= 5; a"), + njs_str("5") }, + { njs_str("var a = 0; a &&= 5; a"), + njs_str("0") }, + { njs_str("var a = 'y'; a &&= 'x'; a"), + njs_str("x") }, + { njs_str("var a = ''; a &&= 'x'; a"), + njs_str("") }, + + /* &&= short-circuit: RHS not evaluated */ + + { njs_str("var a = 0; var b = 0; a &&= (b = 2); b"), + njs_str("0") }, + { njs_str("var a = 1; var b = 0; a &&= (b = 2); b"), + njs_str("2") }, + + /* Logical assignment: property targets */ + + { njs_str("var o = {a: 0}; o.a ||= 5; o.a"), + njs_str("5") }, + { njs_str("var o = {a: 1}; o.a ||= 5; o.a"), + njs_str("1") }, + { njs_str("var o = {a: 1}; o.a &&= 5; o.a"), + njs_str("5") }, + { njs_str("var o = {a: 0}; o.a &&= 5; o.a"), + njs_str("0") }, + + /* Logical assignment: bracket property targets */ + + { njs_str("var o = {a: 0}; o['a'] ||= 5; o.a"), + njs_str("5") }, + { njs_str("var o = {a: 1}; o['a'] &&= 5; o.a"), + njs_str("5") }, + + /* Logical assignment: expression result value */ + + { njs_str("var a = 1; (a ||= 5)"), + njs_str("1") }, + { njs_str("var a = 0; (a ||= 5)"), + njs_str("5") }, + { njs_str("var a = 1; (a &&= 5)"), + njs_str("5") }, + { njs_str("var a = 0; (a &&= 5)"), + njs_str("0") }, + + /* Logical assignment: const error */ + + { njs_str("const a = 1; a ||= 2"), + njs_str("1") }, + { njs_str("const a = 0; a ||= 2"), + njs_str("TypeError: assignment to constant variable") }, + { njs_str("const a = 1; a &&= 2"), + njs_str("TypeError: assignment to constant variable") }, + { njs_str("const a = 0; a &&= 2"), + njs_str("0") }, + + /* Logical assignment: getter/setter short-circuit. */ + + { njs_str("var log = '';" + "var o = {" + " get x() {log += 'g'; return 1}," + " set x(v) {log += 's'}" + "};" + "o.x ||= 2;" + "log"), + njs_str("g") }, + { njs_str("var log = '';" + "var o = {" + " get x() {log += 'g'; return 0}," + " set x(v) {log += 's'}" + "};" + "o.x ||= 2;" + "log"), + njs_str("gs") }, + { njs_str("var log = '';" + "var o = {" + " get x() {log += 'g'; return 0}," + " set x(v) {log += 's'}" + "};" + "o.x &&= 2;" + "log"), + njs_str("g") }, + { njs_str("var log = '';" + "var o = {" + " get x() {log += 'g'; return 1}," + " set x(v) {log += 's'}" + "};" + "o.x &&= 2;" + "log"), + njs_str("gs") }, + + /* Logical assignment: non-lvalue error */ + + { njs_str("1 ||= 2"), + njs_str("ReferenceError: Invalid left-hand side in assignment") }, + { njs_str("1 &&= 2"), + njs_str("ReferenceError: Invalid left-hand side in assignment") }, + /* Optional chaining: property access. */ { njs_str("var o = {a: 1}; o?.a"), From 4e3cffd7441f294287dc0424a88e83770dcaf94f Mon Sep 17 00:00:00 2001 From: Dmitry Volyntsev Date: Wed, 11 Feb 2026 08:16:00 -0800 Subject: [PATCH 9/9] Added support for ??= operator. --- src/njs_generator.c | 1 + src/njs_lexer.c | 7 +++- src/njs_lexer.h | 1 + src/njs_parser.c | 6 ++++ src/test/njs_unit_test.c | 69 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 83 insertions(+), 1 deletion(-) diff --git a/src/njs_generator.c b/src/njs_generator.c index a3a0adafa..9de1a1450 100644 --- a/src/njs_generator.c +++ b/src/njs_generator.c @@ -665,6 +665,7 @@ njs_generate(njs_vm_t *vm, njs_generator_t *generator, njs_parser_node_t *node) case NJS_TOKEN_LOGICAL_OR_ASSIGNMENT: case NJS_TOKEN_LOGICAL_AND_ASSIGNMENT: + case NJS_TOKEN_COALESCE_ASSIGNMENT: return njs_generate_logical_assignment(vm, generator, node); case NJS_TOKEN_BITWISE_OR: diff --git a/src/njs_lexer.c b/src/njs_lexer.c index 30febb143..b5c2ce121 100644 --- a/src/njs_lexer.c +++ b/src/njs_lexer.c @@ -276,8 +276,13 @@ static const njs_lexer_multi_t njs_greater_token[] = { }; +static const njs_lexer_multi_t njs_coalesce_assignment_token[] = { + { '=', NJS_TOKEN_COALESCE_ASSIGNMENT, 0, NULL }, +}; + + static const njs_lexer_multi_t njs_conditional_token[] = { - { '?', NJS_TOKEN_COALESCE, 0, NULL }, + { '?', NJS_TOKEN_COALESCE, 1, njs_coalesce_assignment_token }, }; diff --git a/src/njs_lexer.h b/src/njs_lexer.h index 331eb52eb..9dd210635 100644 --- a/src/njs_lexer.h +++ b/src/njs_lexer.h @@ -52,6 +52,7 @@ typedef enum { NJS_TOKEN_BITWISE_AND_ASSIGNMENT, NJS_TOKEN_LOGICAL_OR_ASSIGNMENT, NJS_TOKEN_LOGICAL_AND_ASSIGNMENT, + NJS_TOKEN_COALESCE_ASSIGNMENT, NJS_TOKEN_INCREMENT, NJS_TOKEN_DECREMENT, diff --git a/src/njs_parser.c b/src/njs_parser.c index 8d2a778eb..669f619bc 100644 --- a/src/njs_parser.c +++ b/src/njs_parser.c @@ -4618,6 +4618,11 @@ njs_parser_assignment_operator(njs_parser_t *parser, njs_lexer_token_t *token, operation = NJS_VMCODE_TEST_IF_FALSE; break; + case NJS_TOKEN_COALESCE_ASSIGNMENT: + njs_thread_log_debug("JS: ?\?="); + operation = NJS_VMCODE_COALESCE; + break; + default: return njs_parser_stack_pop(parser); } @@ -9568,6 +9573,7 @@ njs_parser_serialize_node(njs_chb_t *chain, njs_parser_node_t *node) njs_token_serialize(NJS_TOKEN_BITWISE_AND_ASSIGNMENT); njs_token_serialize(NJS_TOKEN_LOGICAL_OR_ASSIGNMENT); njs_token_serialize(NJS_TOKEN_LOGICAL_AND_ASSIGNMENT); + njs_token_serialize(NJS_TOKEN_COALESCE_ASSIGNMENT); njs_token_serialize(NJS_TOKEN_EQUAL); njs_token_serialize(NJS_TOKEN_NOT_EQUAL); njs_token_serialize(NJS_TOKEN_STRICT_EQUAL); diff --git a/src/test/njs_unit_test.c b/src/test/njs_unit_test.c index bff848684..bbe70ebbb 100644 --- a/src/test/njs_unit_test.c +++ b/src/test/njs_unit_test.c @@ -1625,6 +1625,75 @@ static njs_unit_test_t njs_test[] = { njs_str("1 &&= 2"), njs_str("ReferenceError: Invalid left-hand side in assignment") }, + /* Logical assignment: ??= */ + + { njs_str("var a = null; a ?\?= 5; a"), + njs_str("5") }, + { njs_str("var a = undefined; a ?\?= 5; a"), + njs_str("5") }, + { njs_str("var a = 0; a ?\?= 5; a"), + njs_str("0") }, + { njs_str("var a = ''; a ?\?= 'x'; a"), + njs_str("") }, + { njs_str("var a = false; a ?\?= true; a"), + njs_str("false") }, + { njs_str("var a = 1; a ?\?= 5; a"), + njs_str("1") }, + + /* ??= short-circuit: RHS not evaluated */ + + { njs_str("var a = 1; var b = 0; a ?\?= (b = 2); b"), + njs_str("0") }, + { njs_str("var a = null; var b = 0; a ?\?= (b = 2); b"), + njs_str("2") }, + + /* ??= property targets */ + + { njs_str("var o = {a: null}; o.a ?\?= 5; o.a"), + njs_str("5") }, + { njs_str("var o = {a: 0}; o.a ?\?= 5; o.a"), + njs_str("0") }, + { njs_str("var o = {a: null}; o['a'] ?\?= 5; o.a"), + njs_str("5") }, + + /* ??= expression result value */ + + { njs_str("var a = 1; (a ?\?= 5)"), + njs_str("1") }, + { njs_str("var a = null; (a ?\?= 5)"), + njs_str("5") }, + + /* ??= const error */ + + { njs_str("const a = 1; a ?\?= 2"), + njs_str("1") }, + { njs_str("const a = null; a ?\?= 2"), + njs_str("TypeError: assignment to constant variable") }, + + /* ??= getter/setter short-circuit */ + + { njs_str("var log = '';" + "var o = {" + " get x() {log += 'g'; return 1}," + " set x(v) {log += 's'}" + "};" + "o.x ?\?= 2;" + "log"), + njs_str("g") }, + { njs_str("var log = '';" + "var o = {" + " get x() {log += 'g'; return null}," + " set x(v) {log += 's'}" + "};" + "o.x ?\?= 2;" + "log"), + njs_str("gs") }, + + /* ??= non-lvalue error */ + + { njs_str("1 ?\?= 2"), + njs_str("ReferenceError: Invalid left-hand side in assignment") }, + /* Optional chaining: property access. */ { njs_str("var o = {a: 1}; o?.a"),