From 1a64ba684d898a2395a7bd01b5ebd215a189acec Mon Sep 17 00:00:00 2001 From: Dmitry Volyntsev Date: Wed, 11 Feb 2026 07:13:06 -0800 Subject: [PATCH] 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 6bc09115..a3a0adaf 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 7827aa0c..30febb14 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 5770e1c5..331eb52e 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 e5ec98cc..8d2a778e 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 6e5fd2a3..bff84868 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"), -- 2.47.3