]> git.kaiwu.me - njs.git/commitdiff
Added Symbol.toPrimitive to Date and Symbol prototypes.
authorVadim Zhestikov <v.zhestikov@f5.com>
Tue, 11 Nov 2025 18:01:01 +0000 (10:01 -0800)
committerVadimZhestikov <108960056+VadimZhestikov@users.noreply.github.com>
Fri, 14 Nov 2025 00:05:28 +0000 (16:05 -0800)
src/njs_date.c
src/njs_object.h
src/njs_symbol.c
src/njs_value.c
src/njs_value.h
src/njs_value_conversion.h
src/njs_vm.c
src/njs_vmcode.c
src/test/njs_unit_test.c

index 03e866a152ab6d04f1ad3cdb55adb1e7c7c009b4..4577d9bdd7a4ac2387140f30acefcdc3b297657d 100644 (file)
@@ -411,7 +411,8 @@ njs_date_constructor(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
     } else if (nargs == 2) {
         if (njs_is_object(&args[1])) {
             if (!njs_is_date(&args[1])) {
-                ret = njs_value_to_primitive(vm, &args[1], &args[1], 0);
+                ret = njs_value_to_primitive(vm, &args[1], &args[1],
+                                             NJS_HINT_NONE);
                 if (njs_slow_path(ret != NJS_OK)) {
                     return ret;
                 }
@@ -1381,6 +1382,65 @@ njs_date_prototype_set_time(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
 }
 
 
+static njs_int_t
+njs_date_prototype_to_primitive(njs_vm_t *vm, njs_value_t *args,
+    njs_uint_t nargs, njs_index_t unused, njs_value_t *retval)
+{
+    njs_int_t   ret;
+    njs_uint_t  hint;
+
+    if (njs_slow_path(!njs_is_date(&args[0]))) {
+        njs_type_error(vm, "cannot convert %s to date",
+                       njs_type_string(args[0].type));
+
+        return NJS_ERROR;
+    }
+
+    if (njs_slow_path(nargs <= 1)) {
+        goto error;
+    }
+
+    if (njs_slow_path(!njs_is_string(&args[1]))) {
+        ret = njs_value_to_string(vm, &args[1], &args[1]);
+        if (njs_slow_path(ret != NJS_OK)) {
+            return ret;
+        }
+    }
+
+    ret = njs_atom_atomize_key(vm, &args[1]);
+    if (njs_slow_path(ret != NJS_OK)) {
+        return ret;
+    }
+
+    switch (args[1].atom_id) {
+    case NJS_ATOM_STRING_number:
+        hint = NJS_HINT_NUMBER;
+        break;
+
+    case NJS_ATOM_STRING_string:
+    case NJS_ATOM_STRING_default:
+        hint = NJS_HINT_STRING;
+        break;
+
+    default:
+error:
+        njs_type_error(vm, "invalid hint");
+        return NJS_ERROR;
+
+    }
+
+    ret = njs_value_to_primitive(vm, &args[0], &args[0],
+                                 hint | NJS_HINT_FORCE_ORDINARY);
+    if (njs_slow_path(ret != NJS_OK)) {
+        return ret;
+    }
+
+    njs_value_assign(retval, &args[0]);
+
+    return NJS_OK;
+}
+
+
 static njs_int_t
 njs_date_prototype_set_fields(njs_vm_t *vm, njs_value_t *args,
     njs_uint_t nargs, njs_index_t magic, njs_value_t *retval)
@@ -1643,6 +1703,12 @@ static const njs_object_prop_init_t  njs_date_prototype_properties[] =
     NJS_DECLARE_PROP_NATIVE(STRING_setUTCFullYear,
                             njs_date_prototype_set_fields, 3,
                             njs_date_magic2(NJS_DATE_YR, 3, 0)),
+
+    /* NJS_DECLARE_PROP_NATIVE, but not writable */
+    NJS_DECLARE_PROP_VALUE(SYMBOL_toPrimitive,
+                           njs_native_function2(njs_date_prototype_to_primitive,
+                                                1, 0),
+                           NJS_OBJECT_PROP_VALUE_C),
 };
 
 
index d2b49180c63c6e7ea138acb4cac016ea02a3219b..641a10287962762dbc420ca7bac20543a81a7432 100644 (file)
@@ -186,7 +186,8 @@ njs_value_to_key2(njs_vm_t *vm, njs_value_t *dst, njs_value_t *value,
 
         } else {
             if (convert) {
-                ret = njs_value_to_primitive(vm, &primitive, value, 1);
+                ret = njs_value_to_primitive(vm, &primitive, value,
+                                             NJS_HINT_STRING);
 
             } else {
                 ret = njs_object_to_string(vm, value, &primitive);
index 46634a611aa2868b08b0144d0cc7010482a33250..3fffdf869acfa36a1ec067893a7e91e6aa4bd5b4 100644 (file)
@@ -310,6 +310,12 @@ static const njs_object_prop_init_t  njs_symbol_prototype_properties[] =
     NJS_DECLARE_PROP_NATIVE(STRING_valueOf, njs_symbol_prototype_value_of,
                             0, 0),
 
+    /* NJS_DECLARE_PROP_NATIVE, but not writable */
+    NJS_DECLARE_PROP_VALUE(SYMBOL_toPrimitive,
+                           njs_native_function2(njs_symbol_prototype_value_of,
+                                                1, 0),
+                           NJS_OBJECT_PROP_VALUE_C),
+
     NJS_DECLARE_PROP_NATIVE(STRING_toString,
                             njs_symbol_prototype_to_string, 0, 0),
 
index 09aa9dfcd867a6ae5a8b866a4ed9033be4150e71..7d4c0a39d3401aa5169baf27eb29b12be5db121a 100644 (file)
@@ -30,19 +30,14 @@ const njs_value_t  njs_value_zero =         njs_value(NJS_NUMBER, 0, 0.0);
 const njs_value_t  njs_value_nan =          njs_value(NJS_NUMBER, 0, NAN);
 const njs_value_t  njs_value_invalid =      njs_value(NJS_INVALID, 0, 0.0);
 
-/*
- * A hint value is 0 for numbers and 1 for strings.  The value chooses
- * method calls order specified by ECMAScript 5.1: "valueOf", "toString"
- * for numbers and "toString", "valueOf" for strings.
- */
 
 njs_int_t
 njs_value_to_primitive(njs_vm_t *vm, njs_value_t *dst, njs_value_t *value,
     njs_uint_t hint)
 {
     njs_int_t            ret;
-    njs_uint_t           tries;
-    njs_value_t          method, retval;
+    njs_uint_t           tries, force_ordinary;
+    njs_value_t          method, retval, arguments[2];
     njs_flathsh_query_t  fhq;
 
     static const uint32_t atoms[] = {
@@ -50,6 +45,12 @@ njs_value_to_primitive(njs_vm_t *vm, njs_value_t *dst, njs_value_t *value,
         NJS_ATOM_STRING_toString,
     };
 
+    static const njs_uint_t atom_by_hint[] = {
+        NJS_ATOM_STRING_number,
+        NJS_ATOM_STRING_string,
+        NJS_ATOM_STRING_default,
+    };
+
     if (njs_is_primitive(value)) {
         *dst = *value;
         return NJS_OK;
@@ -58,16 +59,57 @@ njs_value_to_primitive(njs_vm_t *vm, njs_value_t *dst, njs_value_t *value,
     tries = 0;
     fhq.proto = &njs_object_hash_proto;
 
+    if (!njs_is_object(value)) {
+        goto error;
+    }
+
+    force_ordinary = hint & NJS_HINT_FORCE_ORDINARY;
+    hint &= ~NJS_HINT_FORCE_ORDINARY;
+
+    if (force_ordinary) {
+        goto ordinary;
+    }
+
+    fhq.key_hash = NJS_ATOM_SYMBOL_toPrimitive;
+
+    ret = njs_object_property(vm, njs_object(value), &fhq, &method);
+    if (njs_slow_path(ret == NJS_ERROR)) {
+        return ret;
+    }
+
+    if (njs_is_function(&method)) {
+        arguments[0] = *value;
+
+        njs_atom_to_value(vm, &arguments[1], atom_by_hint[hint]);
+
+        ret = njs_function_apply(vm, njs_function(&method), arguments, 2,
+                                 &retval);
+        if (njs_slow_path(ret != NJS_OK)) {
+            return ret;
+        }
+
+        if (njs_is_primitive(&retval)) {
+            goto done;
+        }
+
+        goto error;
+    }
+
+ordinary:
+
+    if (hint != NJS_HINT_STRING) {
+        hint = NJS_HINT_NUMBER;
+    }
+
     for ( ;; ) {
         ret = NJS_ERROR;
 
-        if (njs_is_object(value) && tries < 2) {
+        if (tries < 2) {
             hint ^= tries++;
 
             fhq.key_hash = atoms[hint];
 
             ret = njs_object_property(vm, njs_object(value), &fhq, &method);
-
             if (njs_slow_path(ret == NJS_ERROR)) {
                 return ret;
             }
@@ -75,25 +117,28 @@ njs_value_to_primitive(njs_vm_t *vm, njs_value_t *dst, njs_value_t *value,
             if (njs_is_function(&method)) {
                 ret = njs_function_apply(vm, njs_function(&method), value, 1,
                                          &retval);
-
                 if (njs_slow_path(ret != NJS_OK)) {
                     return ret;
                 }
 
                 if (njs_is_primitive(&retval)) {
-                    break;
+                    goto done;
                 }
             }
 
             /* Try the second method. */
             continue;
-         }
+        }
+
+error:
 
         njs_type_error(vm, "Cannot convert object to primitive value");
 
-        return ret;
+        return NJS_ERROR;
     }
 
+done:
+
     *dst = retval;
 
     return NJS_OK;
index 2ffa04e7d73f5d5ccae3965b4312cb49db04003a..61a46c0a81568eb0d74946c091d571ee976f0b4b 100644 (file)
@@ -940,6 +940,11 @@ njs_set_object_value(njs_value_t *value, njs_object_value_t *object_value)
 #define njs_set_invalid(value)                                                \
     (value)->type = NJS_INVALID
 
+#define NJS_HINT_NUMBER  0
+#define NJS_HINT_STRING  1
+#define NJS_HINT_NONE    2
+#define NJS_HINT_FORCE_ORDINARY (1 << 3)
+
 njs_int_t njs_value_to_primitive(njs_vm_t *vm, njs_value_t *dst,
     njs_value_t *value, njs_uint_t hint);
 njs_array_t *njs_value_enumerate(njs_vm_t *vm, njs_value_t *value,
index 80c08b4502d4795aa96054cdca008666ab5a69a2..ba3814d0b595f41b849f2fa05b6f4ed17e1ba889 100644 (file)
@@ -15,7 +15,7 @@ njs_value_to_number(njs_vm_t *vm, njs_value_t *value, double *dst)
     njs_value_t  primitive;
 
     if (njs_slow_path(!njs_is_primitive(value))) {
-        ret = njs_value_to_primitive(vm, &primitive, value, 0);
+        ret = njs_value_to_primitive(vm, &primitive, value, NJS_HINT_NUMBER);
         if (njs_slow_path(ret != NJS_OK)) {
             return NJS_ERROR;
         }
@@ -172,7 +172,8 @@ njs_value_to_chain(njs_vm_t *vm, njs_chb_t *chain, njs_value_t *value)
             value = njs_object_value(value);
 
         } else {
-            ret = njs_value_to_primitive(vm, &primitive, value, 1);
+            ret = njs_value_to_primitive(vm, &primitive, value,
+                                         NJS_HINT_STRING);
             if (njs_slow_path(ret != NJS_OK)) {
                 return ret;
             }
index 19f07db403838951499dd7d011f0daa84d05101f..fe4e3a317622506a7bc857395d733fe9f0735277 100644 (file)
@@ -1560,7 +1560,8 @@ njs_value_to_string(njs_vm_t *vm, njs_value_t *dst, njs_value_t *value)
             value = njs_object_value(value);
 
         } else {
-            ret = njs_value_to_primitive(vm, &primitive, value, 1);
+            ret = njs_value_to_primitive(vm, &primitive, value,
+                                         NJS_HINT_STRING);
             if (njs_slow_path(ret != NJS_OK)) {
                 return ret;
             }
index 70da4630f0d644a2789919e996b674ab1436bf74..626f85cff1f83e0870c94d9b2a911c380f230124 100644 (file)
@@ -88,7 +88,6 @@ njs_vmcode_interpreter(njs_vm_t *vm, u_char *pc, njs_value_t *rval,
     int32_t                      i32;
     uint32_t                     u32;
     njs_str_t                    string;
-    njs_uint_t                   hint;
     njs_bool_t                   valid, lambda_call;
     njs_value_t                  *retval, *value1, *value2;
     njs_value_t                  *src, *s1, *s2, dst;
@@ -461,7 +460,8 @@ NEXT_LBL;
         njs_vmcode_operand(vm, vmcode->operand2, value1);
 
         if (njs_slow_path(!njs_is_primitive(value1))) {
-            ret = njs_value_to_primitive(vm, &primitive1, value1, 0);
+            ret = njs_value_to_primitive(vm, &primitive1, value1,
+                                         NJS_HINT_NUMBER);
             if (ret != NJS_OK) {
                 goto error;
             }
@@ -470,7 +470,8 @@ NEXT_LBL;
         }
 
         if (njs_slow_path(!njs_is_primitive(value2))) {
-            ret = njs_value_to_primitive(vm, &primitive2, value2, 0);
+            ret = njs_value_to_primitive(vm, &primitive2, value2,
+                                         NJS_HINT_NUMBER);
             if (ret != NJS_OK) {
                 goto error;
             }
@@ -500,7 +501,8 @@ NEXT_LBL;
         njs_vmcode_operand(vm, vmcode->operand2, value1);
 
         if (njs_slow_path(!njs_is_primitive(value1))) {
-            ret = njs_value_to_primitive(vm, &primitive1, value1, 0);
+            ret = njs_value_to_primitive(vm, &primitive1, value1,
+                                         NJS_HINT_NUMBER);
             if (ret != NJS_OK) {
                 goto error;
             }
@@ -509,7 +511,8 @@ NEXT_LBL;
         }
 
         if (njs_slow_path(!njs_is_primitive(value2))) {
-            ret = njs_value_to_primitive(vm, &primitive2, value2, 0);
+            ret = njs_value_to_primitive(vm, &primitive2, value2,
+                                         NJS_HINT_NUMBER);
             if (ret != NJS_OK) {
                 goto error;
             }
@@ -539,7 +542,8 @@ NEXT_LBL;
         njs_vmcode_operand(vm, vmcode->operand2, value1);
 
         if (njs_slow_path(!njs_is_primitive(value1))) {
-            ret = njs_value_to_primitive(vm, &primitive1, value1, 0);
+            ret = njs_value_to_primitive(vm, &primitive1, value1,
+                                         NJS_HINT_NUMBER);
             if (ret != NJS_OK) {
                 goto error;
             }
@@ -548,7 +552,8 @@ NEXT_LBL;
         }
 
         if (njs_slow_path(!njs_is_primitive(value2))) {
-            ret = njs_value_to_primitive(vm, &primitive2, value2, 0);
+            ret = njs_value_to_primitive(vm, &primitive2, value2,
+                                         NJS_HINT_NUMBER);
             if (ret != NJS_OK) {
                 goto error;
             }
@@ -578,7 +583,8 @@ NEXT_LBL;
         njs_vmcode_operand(vm, vmcode->operand2, value1);
 
         if (njs_slow_path(!njs_is_primitive(value1))) {
-            ret = njs_value_to_primitive(vm, &primitive1, value1, 0);
+            ret = njs_value_to_primitive(vm, &primitive1, value1,
+                                         NJS_HINT_NUMBER);
             if (ret != NJS_OK) {
                 goto error;
             }
@@ -587,7 +593,8 @@ NEXT_LBL;
         }
 
         if (njs_slow_path(!njs_is_primitive(value2))) {
-            ret = njs_value_to_primitive(vm, &primitive2, value2, 0);
+            ret = njs_value_to_primitive(vm, &primitive2, value2,
+                                         NJS_HINT_NUMBER);
             if (ret != NJS_OK) {
                 goto error;
             }
@@ -617,8 +624,8 @@ NEXT_LBL;
         njs_vmcode_operand(vm, vmcode->operand2, value1);
 
         if (njs_slow_path(!njs_is_primitive(value1))) {
-            hint = njs_is_date(value1);
-            ret = njs_value_to_primitive(vm, &primitive1, value1, hint);
+            ret = njs_value_to_primitive(vm, &primitive1, value1,
+                                         NJS_HINT_NONE);
             if (ret != NJS_OK) {
                 goto error;
             }
@@ -627,8 +634,8 @@ NEXT_LBL;
         }
 
         if (njs_slow_path(!njs_is_primitive(value2))) {
-            hint =  njs_is_date(value2);
-            ret = njs_value_to_primitive(vm, &primitive2, value2, hint);
+            ret = njs_value_to_primitive(vm, &primitive2, value2,
+                                         NJS_HINT_NONE);
             if (ret != NJS_OK) {
                 goto error;
             }
@@ -2478,7 +2485,7 @@ again:
 
     /* "hv" is an object and "lv" is either a string or a symbol or a numeric. */
 
-    ret = njs_value_to_primitive(vm, &primitive, hv, 0);
+    ret = njs_value_to_primitive(vm, &primitive, hv, NJS_HINT_NONE);
     if (ret != NJS_OK) {
         return ret;
     }
index c0f1f1d365208364cf2db3a6192d708ae58a4762..0c5e55cc0ffd779e17f441f8143a3e838c658206 100644 (file)
@@ -13727,6 +13727,36 @@ static njs_unit_test_t  njs_test[] =
     { njs_str("Symbol.prototype.toString()"),
       njs_str("TypeError: unexpected value type:object") },
 
+    { njs_str("Symbol.prototype[Symbol.toPrimitive].length"),
+      njs_str("1") },
+
+    { njs_str("Symbol.prototype[Symbol.toPrimitive].name"),
+      njs_str("[Symbol.toPrimitive]") },
+
+    { njs_str("Symbol.prototype[Symbol.toPrimitive].call(undefined)"),
+      njs_str("TypeError: unexpected value type") },
+
+    { njs_str("Symbol.prototype[Symbol.toPrimitive].call(null)"),
+      njs_str("TypeError: unexpected value type") },
+
+    { njs_str("Symbol.prototype[Symbol.toPrimitive].call(86)"),
+      njs_str("TypeError: unexpected value type") },
+
+    { njs_str("Symbol.prototype[Symbol.toPrimitive].call('')"),
+      njs_str("TypeError: unexpected value type") },
+
+    { njs_str("Symbol.prototype[Symbol.toPrimitive].call(true)"),
+      njs_str("TypeError: unexpected value type") },
+
+    { njs_str("Symbol.prototype[Symbol.toPrimitive].call({})"),
+      njs_str("TypeError: unexpected value type") },
+
+    { njs_str("Object(Symbol.toPrimitive)[Symbol.toPrimitive]() === Symbol.toPrimitive"),
+      njs_str("true") },
+
+    { njs_str("Symbol.toPrimitive[Symbol.toPrimitive]() === Symbol.toPrimitive"),
+      njs_str("true") },
+
     { njs_str("new Symbol()"),
       njs_str("TypeError: Symbol is not a constructor") },
 
@@ -16580,13 +16610,13 @@ static njs_unit_test_t  njs_test[] =
       njs_str("-62198755200000") },
 
     { njs_str("var d = new Date(); d == Date.parse(d.toISOString())"),
-      njs_str("true") },
+      njs_str("false") },
 
     { njs_str("var s = Date(); s === Date(Date.parse(s))"),
       njs_str("true") },
 
     { njs_str("var n = Date.now(); n == new Date(n)"),
-      njs_str("true") },
+      njs_str("false") },
 
     { njs_str("var d = new Date(2011,0); d.getFullYear()"),
       njs_str("2011") },
@@ -16882,6 +16912,115 @@ static njs_unit_test_t  njs_test[] =
     { njs_str("Date.constructor === Function"),
       njs_str("true") },
 
+    { njs_str("var y = Object.defineProperty({}, Symbol.toPrimitive, {"
+              "    get: function() {"
+              "        throw new Error('expected');"
+              "    }"
+              "});"
+              "new Date(y);"),
+      njs_str("Error: expected") },
+
+    { njs_str("var y = {};"
+              "y[Symbol.toPrimitive] = function() {"
+              "     throw new Error('expected');"
+              "};"
+              "new Date(y);"),
+      njs_str("Error: expected") },
+
+    { njs_str("var y = {};"
+              "var callCount = 0;"
+              "var thisVal, args;"
+              "y[Symbol.toPrimitive] = function() {"
+              "    callCount += 1;"
+              "    thisVal = this;"
+              "    args = arguments;"
+              "};"
+              "new Date(y);"
+              "callCount === 1 && thisVal === y && args.length === 1 && args[0] === 'default'"),
+      njs_str("true") },
+
+    { njs_str("var y = {};"
+              "var retVal;"
+              "y[Symbol.toPrimitive] = function() {"
+              "    return retVal;"
+              "};"
+              "retVal = {};"
+              "new Date(y);"),
+      njs_str("TypeError") },
+
+    { njs_str("var badToPrimitive = {};"
+              "badToPrimitive[Symbol.toPrimitive] = function() {"
+              "    throw new Error('expected');"
+              "};"
+              "new Date(badToPrimitive);"),
+      njs_str("Error: expected") },
+
+    { njs_str("var spyToPrimitive = {};"
+              "var callCount = 0;"
+              "var thisValue, args;"
+              "spyToPrimitive[Symbol.toPrimitive] = function() {"
+              "    thisValue = this;"
+              "    args = arguments;"
+              "    callCount += 1;"
+              "};"
+              "new Date(spyToPrimitive);"
+              "callCount === 1 && thisValue === spyToPrimitive && args.length === 1 && args[0] === 'default';"),
+      njs_str("true") },
+
+    { njs_str("var poisonedObject = {};"
+              "var poisonedDate = new Date();"
+              "Object.defineProperty(poisonedObject, Symbol.toPrimitive, {"
+              "    get: function() {"
+              "        throw new Error('expected');"
+              "    }"
+              "});"
+              "Object.defineProperty(poisonedDate, Symbol.toPrimitive, {"
+              "    get: function() {"
+              "        throw new Error('unexpected');"
+              "    }"
+              "});"
+              "Date(poisonedObject);"
+              "new Date(poisonedDate);"
+              "new Date(poisonedObject);"),
+      njs_str("Error: expected") },
+
+    { njs_str("var faultyToPrimitive = {};"
+              "var returnValue;"
+              "faultyToPrimitive[Symbol.toPrimitive] = function() {"
+              "    return returnValue;"
+              "};"
+              "returnValue = {};"
+              "new Date(faultyToPrimitive);"),
+      njs_str("TypeError") },
+
+    { njs_str("var stringToPrimitive = {};"
+              "stringToPrimitive[Symbol.toPrimitive] = function() {"
+              "    return '2016-06-05T18:40:00.000Z';"
+              "};"
+              "new Date(stringToPrimitive).getTime() === 1465152000000"),
+      njs_str("true") },
+
+    { njs_str("Date.prototype[Symbol.toPrimitive].length === 1"),
+      njs_str("true") },
+
+    { njs_str("Date.prototype[Symbol.toPrimitive].name"),
+      njs_str("[Symbol.toPrimitive]") },
+
+    { njs_str("Date.prototype[Symbol.toPrimitive].call(undefined, 'string')"),
+      njs_str("TypeError: cannot convert") },
+
+    { njs_str("Date.prototype[Symbol.toPrimitive].call(null, 'string')"),
+      njs_str("TypeError: cannot conver") },
+
+    { njs_str("Date.prototype[Symbol.toPrimitive].call(86, 'string')"),
+      njs_str("TypeError: cannot convert") },
+
+    { njs_str("Date.prototype[Symbol.toPrimitive].call('', 'string')"),
+      njs_str("TypeError: cannot convert") },
+
+    { njs_str("Date.prototype[Symbol.toPrimitive].call(true, 'string')"),
+      njs_str("TypeError: cannot convert") },
+
     /* eval(). */
 
     { njs_str("eval.name"),