]> git.kaiwu.me - njs.git/commitdiff
Added support for ||= and &&= logical assignment operators.
authorDmitry Volyntsev <xeioex@nginx.com>
Wed, 11 Feb 2026 15:13:06 +0000 (07:13 -0800)
committerDmitry Volyntsev <xeioexception@gmail.com>
Mon, 2 Mar 2026 21:01:04 +0000 (13:01 -0800)
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
src/njs_lexer.c
src/njs_lexer.h
src/njs_parser.c
src/test/njs_unit_test.c

index 6bc09115f3dff5820db37c8000fed346141c951d..a3a0adafadfcb1fadf80c9677789ba9a6bc35146 100644 (file)
@@ -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)
index 7827aa0cade4ebe97a951ff5f8f01537f8791a99..30febb1433a6c5b572805222a6efdc80afd25ae8 100644 (file)
@@ -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 },
 };
 
index 5770e1c56b430407bb2036fd004610264e4f7d1b..331eb52eb28251fc7cee4f3fb4599271b5e16648 100644 (file)
@@ -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,
index e5ec98cc7bdf4ca2fc65d0d3a4a3d6a5c43adb1d..8d2a778eb6b4bba9ce2dc39cecc7676151b9f73d 100644 (file)
@@ -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);
index 6e5fd2a3761576f7057a59f291abe3ff0109a8dd..bff8486847cce3165b31f00ccd852c23ad666950 100644 (file)
@@ -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"),