]> Lady’s Gitweb - Pisces/commitdiff
Split some object code into value and unit test
authorLady <redacted>
Sun, 17 Jul 2022 20:45:08 +0000 (13:45 -0700)
committerLady <redacted>
Fri, 12 May 2023 03:56:47 +0000 (20:56 -0700)
Also adds tests for module exports.

dev-deps.js
mod.js
mod.test.js [new file with mode: 0644]
object.js
object.test.js [new file with mode: 0644]
value.js [new file with mode: 0644]
value.test.js [new file with mode: 0644]

index 49044c18e4b74186934e769ccbc01bb900c9da97..96858a643d9cde04561a55470cc6d618148c8122 100644 (file)
@@ -12,4 +12,13 @@ export {
   assertEquals,
   assertStrictEquals,
   assertThrows,
-} from "https://deno.land/std@0.134.0/testing/asserts.ts";
+} from "https://deno.land/std@0.148.0/testing/asserts.ts";
+export {
+  describe,
+  it,
+} from "https://deno.land/std@0.148.0/testing/bdd.ts";
+export {
+  assertSpyCall,
+  assertSpyCalls,
+  spy,
+} from "https://deno.land/std@0.148.0/testing/mock.ts";
diff --git a/mod.js b/mod.js
index d036cbc220f4c970d80aee972071d6e8cb2bde86..42b421c76005de2ba850807e3e6e9518dc3ccef1 100644 (file)
--- a/mod.js
+++ b/mod.js
@@ -14,3 +14,4 @@ export * from "./iri.js";
 export * from "./numeric.js";
 export * from "./object.js";
 export * from "./string.js";
+export * from "./value.js";
diff --git a/mod.test.js b/mod.test.js
new file mode 100644 (file)
index 0000000..59a7cbf
--- /dev/null
@@ -0,0 +1,25 @@
+// ♓🌟 Piscēs ∷ mod.test.js
+// ====================================================================
+//
+// Copyright © 2022 Lady [@ Lady’s Computer].
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at <https://mozilla.org/MPL/2.0/>.
+
+import { assert, describe, it } from "./dev-deps.js";
+import * as Piscēs from "./mod.js";
+
+describe("Piscēs", () => {
+  it("exports everything", async () => {
+    for await (const { name, isFile } of Deno.readDir(".")) {
+      if (isFile && /(?<!^mod|\.test|(?:^|-)deps)\.js$/u.test(name)) {
+        await import(`./${name}`).then((module) => {
+          for (const exported of Object.keys(module)) {
+            assert(exported in Piscēs);
+          }
+        });
+      }
+    }
+  });
+});
index c0a9081ad1880f069b459819b331c6695588eed2..578f84f5201f9d46907cc77ba36a30d1c984ddcb 100644 (file)
--- a/object.js
+++ b/object.js
@@ -7,40 +7,8 @@
 // License, v. 2.0. If a copy of the MPL was not distributed with this
 // file, You can obtain one at <https://mozilla.org/MPL/2.0/>.
 
-import { call } from "./function.js";
-
-export const {
-  assign: assignProperties,
-  defineProperty: defineOwnProperty,
-  defineProperties: defineOwnProperties,
-  freeze,
-  getOwnPropertyDescriptor,
-  getOwnPropertyDescriptors,
-  getOwnPropertyNames: getOwnPropertyStrings,
-  getOwnPropertySymbols,
-  getPrototypeOf: getPrototype,
-  hasOwn: hasOwnProperty,
-  isExtensible,
-  isFrozen,
-  isSealed,
-  entries: namedEntries,
-  keys: namedKeys,
-  values: namedValues,
-  create: objectCreate,
-  fromEntries: objectFromEntries,
-  preventExtensions,
-  is: sameValue,
-  seal,
-  setPrototypeOf: setPrototype,
-} = Object;
-
-export const {
-  delete: deleteOwnProperty,
-  keys: getOwnPropertyKeys,
-  get: getPropertyValue,
-  has: hasProperty,
-  set: setPropertyValue,
-} = Reflect;
+import { bind, call } from "./function.js";
+import { toPrimitive, type } from "./value.js";
 
 /**
  * A property descriptor object.
@@ -53,7 +21,7 @@ export const {
  *
  * Otherwise, the instance properties and methods are generic.
  */
-export const PropertyDescriptor = (() => {
+export const { PropertyDescriptor } = (() => {
   class PropertyDescriptor extends null {
     /**
      * Constructs a new property descriptor object from the provided
@@ -64,8 +32,8 @@ export const PropertyDescriptor = (() => {
      * boolean).
      */
     //deno-lint-ignore constructor-super
-    constructor(Obj) {
-      if (!isObject(Obj)) {
+    constructor(O) {
+      if (type(O) !== "object") {
         // The provided value is not an object.
         throw new TypeError(
           "Piscēs: Cannot convert primitive to property descriptor.",
@@ -73,38 +41,38 @@ export const PropertyDescriptor = (() => {
       } else {
         // The provided value is an object.
         const desc = objectCreate(propertyDescriptorPrototype);
-        if ("enumerable" in Obj) {
+        if ("enumerable" in O) {
           // An enumerable property is specified.
-          desc.enumerable = !!Obj.enumerable;
+          desc.enumerable = !!O.enumerable;
         } else {
           // An enumerable property is not specified.
           /* do nothing */
         }
-        if ("configurable" in Obj) {
+        if ("configurable" in O) {
           // A configurable property is specified.
-          desc.configurable = !!Obj.configurable;
+          desc.configurable = !!O.configurable;
         } else {
           // A configurable property is not specified.
           /* do nothing */
         }
-        if ("value" in Obj) {
+        if ("value" in O) {
           // A value property is specified.
-          desc.value = Obj.value;
+          desc.value = O.value;
         } else {
           // A value property is not specified.
           /* do nothing */
         }
-        if ("writable" in Obj) {
+        if ("writable" in O) {
           // A writable property is specified.
-          desc.writable = !!Obj.writable;
+          desc.writable = !!O.writable;
         } else {
           // A writable property is not specified.
           /* do nothing */
         }
-        if ("get" in Obj) {
+        if ("get" in O) {
           // A get property is specified.
-          const getter = Obj.get;
-          if (typeof getter != "function") {
+          const getter = O.get;
+          if (getter !== undefined && typeof getter !== "function") {
             // The getter is not callable.
             throw new TypeError("Piscēs: Getters must be callable.");
           } else {
@@ -115,10 +83,10 @@ export const PropertyDescriptor = (() => {
           // A get property is not specified.
           /* do nothing */
         }
-        if ("set" in Obj) {
+        if ("set" in O) {
           // A set property is specified.
-          const setter = Obj.set;
-          if (typeof setter != "function") {
+          const setter = O.set;
+          if (setter !== undefined && typeof setter !== "function") {
             // The setter is not callable.
             throw new TypeError("Piscēs: Setters must be callable.");
           } else {
@@ -200,20 +168,18 @@ export const PropertyDescriptor = (() => {
       }
     }
 
-    /** Returns whether this is an accessor descrtiptor. */
+    /** Gets whether this is an accessor descrtiptor. */
     get isAccessorDescriptor() {
       return this !== undefined && ("get" in this || "set" in this);
     }
 
-    /** Returns whether this is a data descrtiptor. */
+    /** Gets whether this is a data descrtiptor. */
     get isDataDescriptor() {
       return this !== undefined &&
         ("value" in this || "writable" in this);
     }
 
-    /**
-     * Returns whether this is a fully‐populated property descriptor.
-     */
+    /** Gets whether this is a fully‐populated property descriptor. */
     get isFullyPopulated() {
       return this !== undefined &&
         ("value" in this && "writable" in this ||
@@ -222,7 +188,7 @@ export const PropertyDescriptor = (() => {
     }
 
     /**
-     * Returns whether this is a generic (not accessor or data)
+     * Gets whether this is a generic (not accessor or data)
      * descrtiptor.
      */
     get isGenericDescriptor() {
@@ -232,7 +198,7 @@ export const PropertyDescriptor = (() => {
     }
   }
 
-  const coercePropretyDescriptorValue = (P, V) => {
+  const coercePropertyDescriptorValue = (P, V) => {
     switch (P) {
       case "configurable":
       case "enumerable":
@@ -241,7 +207,7 @@ export const PropertyDescriptor = (() => {
       case "value":
         return V;
       case "get":
-        if (typeof V != "function") {
+        if (V !== undefined && typeof V !== "function") {
           throw new TypeError(
             "Piscēs: Getters must be callable.",
           );
@@ -249,7 +215,7 @@ export const PropertyDescriptor = (() => {
           return V;
         }
       case "set":
-        if (typeof V != "function") {
+        if (V !== undefined && typeof V !== "function") {
           throw new TypeError(
             "Piscēs: Setters must be callable.",
           );
@@ -261,10 +227,12 @@ export const PropertyDescriptor = (() => {
     }
   };
 
-  const propertyDescriptorPrototype = PropertyDescriptor.prototype;
+  const {
+    prototype: propertyDescriptorPrototype,
+  } = PropertyDescriptor;
 
-  const propertyDescriptorProxyHandler = assignProperties(
-    objectCreate(null),
+  const propertyDescriptorProxyHandler = Object.assign(
+    Object.create(null),
     {
       defineProperty(O, P, Desc) {
         if (
@@ -279,9 +247,9 @@ export const PropertyDescriptor = (() => {
             throw new TypeError(
               "Piscēs: Property descriptor attributes must be data properties.",
             );
-          } else if ("value" in desc) {
-            // Desc has a value.
-            desc.value = coercePropretyDescriptorValue(P, desc.value);
+          } else if ("value" in desc || !(P in O)) {
+            // Desc has a value or P does not already exist on O.
+            desc.value = coercePropertyDescriptorValue(P, desc.value);
           } else {
             // Desc is not an accessor property descriptor and has no
             // value.
@@ -307,20 +275,36 @@ export const PropertyDescriptor = (() => {
         }
       },
       set(O, P, V, Receiver) {
-        const newValue = coercePropertyDescriptorValue(P, V);
-        const isAccessorDescriptor = "get" === P || "set" === P ||
-          "get" in O || "set" in O;
-        const isDataDescriptor = "value" === P || "writable" === P ||
-          "value" in O || "writable" in O;
-        if (isAccessorDescriptor && isDataDescriptor) {
-          // Both accessor and data attributes will be present on O
-          // after defining P.
-          throw new TypeError(
-            "Piscēs: Property descriptors cannot specify both accessor and data attributes.",
-          );
+        if (
+          P === "configurable" || P === "enumerable" ||
+          P === "writable" || P === "value" ||
+          P === "get" || P === "set"
+        ) {
+          // P is a property descriptor attribute.
+          const newValue = coercePropertyDescriptorValue(P, V);
+          const isAccessorDescriptor = "get" === P || "set" === P ||
+            "get" in O || "set" in O;
+          const isDataDescriptor = "value" === P || "writable" === P ||
+            "value" in O || "writable" in O;
+          if (isAccessorDescriptor && isDataDescriptor) {
+            // Both accessor and data attributes will be present on O
+            // after defining P.
+            throw new TypeError(
+              "Piscēs: Property descriptors cannot specify both accessor and data attributes.",
+            );
+          } else {
+            // P can be safely defined on O.
+            //
+            // ☡ Receiver will be the *proxied* object, so passing it
+            // through to setPropertyValue here would produce an
+            // infinite loop.
+            //
+            // ☡ This has implications on objects with a proxied
+            // PropertyDescriptor in their prototype.
+            return setPropertyValue(O, P, newValue, O);
+          }
         } else {
-          // P can be safely defined on O.
-          return setPropertyValue(O, prop, newValue, Receiver);
+          return setPropertyValue(O, P, V, Receiver);
         }
       },
       setPrototypeOf(O, V) {
@@ -335,160 +319,384 @@ export const PropertyDescriptor = (() => {
     },
   );
 
-  return PropertyDescriptor;
+  return { PropertyDescriptor };
 })();
 
-/**
- * Returns a new frozen shallow copy of the enumerable own properties
- * of the provided object, according to the following rules :—
- *
- * - For data properties, create a nonconfigurable, nonwritable
- *   property with the same value.
- *
- * - For accessor properties, create a nonconfigurable accessor
- *   property with the same getter *and* setter.
- *
- * The prototype for the resulting object will be taken from the
- * `prototype` property of the provided constructor, or the `prototype`
- * of the `constructor` of the provided object if the provided
- * constructor is undefined. If the used constructor has a nonnullish
- * `Symbol.species`, that will be used instead.
- */
-export const frozenCopy = (O, constructor = O?.constructor) => {
-  if (O == null) {
-    // O is null or undefined.
-    throw new TypeError(
-      "Piscēs: Cannot copy properties of null or undefined.",
-    );
-  } else {
-    // O is not null or undefined.
-    //
-    // (If not provided, the constructor will be the value of getting
-    // the `constructor` property of O.)
-    const species = constructor?.[Symbol.species] ?? constructor;
-    return preventExtensions(
-      objectCreate(
-        species == null || !("prototype" in species)
-          ? null
-          : species.prototype,
-        objectFromEntries(
-          function* () {
-            for (const P of getOwnPropertyKeys(O)) {
-              const Desc = getOwnPropertyDescriptor(O, P);
-              if (Desc.enumerable) {
-                // P is an enumerable property.
-                yield [
-                  P,
-                  "get" in Desc || "set" in Desc
-                    ? {
-                      configurable: false,
-                      enumerable: true,
-                      get: Desc.get,
-                      set: Desc.set,
-                    }
-                    : {
-                      configurable: false,
-                      enumerable: true,
-                      value: Desc.value,
-                      writable: false,
-                    },
-                ];
-              } else {
-                // P is not an enumerable property.
-                /* do nothing */
-              }
-            }
-          }(),
-        ),
-      ),
-    );
-  }
-};
+export const {
+  /**
+   * Defines own properties on the provided object using the
+   * descriptors on the enumerable own properties of the provided
+   * additional objects.
+   *
+   * ※ This differs from Object.defineProperties in that it can take
+   * multiple source objects.
+   */
+  defineOwnProperties,
+} = (() => {
+  const { defineProperties } = Object;
+  const { forEach: arrayForEach } = Array.prototype;
+  return {
+    defineOwnProperties: (O, ...sources) => {
+      call(
+        arrayForEach,
+        sources,
+        [(source) => defineProperties(O, source)],
+      );
+      return O;
+    },
+  };
+})();
 
-/** Returns whether the provided value is an object. */
-export const isObject = ($) => {
-  return $ !== null &&
-    (typeof $ == "function" || typeof $ == "object");
-};
+export const {
+  /**
+   * Defines an own property on the provided object on the provided
+   * property key using the provided property descriptor.
+   *
+   * ※ This is an alias for Object.defineProperty.
+   */
+  defineProperty: defineOwnProperty,
 
-/**
- * Returns the primitive value of the provided object per its
- * `toString` and `valueOf` methods.
- *
- * If the provided hint is "string", then `toString` takes precedence;
- * otherwise, `valueOf` does.
- *
- * Throws an error if both of these methods are not callable or do not
- * return a primitive.
- */
-export const ordinaryToPrimitive = (O, hint) => {
-  for (
-    const name of hint == "string"
-      ? ["toString", "valueOf"]
-      : ["valueOf", "toString"]
-  ) {
-    const method = O[name];
-    if (typeof method == "function") {
-      // Method is callable.
-      const result = call(method, O, []);
-      if (!isObject(result)) {
-        // Method returns a primitive.
-        return result;
+  /**
+   * Marks the provided object as non·extensible and marks all its
+   * properties as nonconfigurable and (if data properties)
+   * nonwritable, and returns the object.
+   *
+   * ※ This is an alias for Object.freeze.
+   */
+  freeze,
+
+  /**
+   * Returns the property descriptor for the own property with the
+   * provided property key on the provided object, or null if none
+   * exists.
+   *
+   * ※ This is an alias for Object.getOwnPropertyDescriptor.
+   */
+  getOwnPropertyDescriptor,
+
+  /**
+   * Returns the property descriptors for the own properties on the
+   * provided object.
+   *
+   * ※ This is an alias for Object.getOwnPropertyDescriptors.
+   */
+  getOwnPropertyDescriptors,
+
+  /**
+   * Returns an array of string‐valued own property keys on the
+   * provided object.
+   *
+   * ☡ This includes both enumerable and non·enumerable properties.
+   *
+   * ※ This is an alias for Object.getOwnPropertyNames.
+   */
+  getOwnPropertyNames: getOwnPropertyStrings,
+
+  /**
+   * Returns an array of symbol‐valued own property keys on the
+   * provided object.
+   *
+   * ☡ This includes both enumerable and non·enumerable properties.
+   *
+   * ※ This is an alias for Object.getOwnPropertySymbols.
+   */
+  getOwnPropertySymbols,
+
+  /**
+   * Returns the prototype of the provided object.
+   *
+   * ※ This is an alias for Object.getPrototypeOf.
+   */
+  getPrototypeOf: getPrototype,
+
+  /**
+   * Returns whether the provided object has an own property with the
+   * provided property key.
+   *
+   * ※ This is an alias for Object.hasOwn.
+   */
+  hasOwn: hasOwnProperty,
+
+  /**
+   * Returns whether the provided object is extensible.
+   *
+   * ※ This is an alias for Object.isExtensible.
+   */
+  isExtensible,
+
+  /**
+   * Returns whether the provided object is frozen.
+   *
+   * ※ This is an alias for Object.isFrozen.
+   */
+  isFrozen,
+
+  /**
+   * Returns whether the provided object is sealed.
+   *
+   * ※ This is an alias for Object.isSealed.
+   */
+  isSealed,
+
+  /**
+   * Returns an array of key~value pairs for the enumerable,
+   * string‐valued property keys on the provided object.
+   *
+   * ※ This is an alias for Object.entries.
+   */
+  entries: namedEntries,
+
+  /**
+   * Returns an array of the enumerable, string‐valued property keys on
+   * the provided object.
+   *
+   * ※ This is an alias for Object.keys.
+   */
+  keys: namedKeys,
+
+  /**
+   * Returns an array of property values for the enumerable,
+   * string‐valued property keys on the provided object.
+   *
+   * ※ This is an alias for Object.values.
+   */
+  values: namedValues,
+
+  /**
+   * Returns a new object with the provided prototype and property
+   * descriptors.
+   *
+   * ※ This is an alias for Object.create.
+   */
+  create: objectCreate,
+
+  /**
+   * Returns a new object with the provided property keys and values.
+   *
+   * ※ This is an alias for Object.fromEntries.
+   */
+  fromEntries: objectFromEntries,
+
+  /**
+   * Marks the provided object as non·extensible, and returns the
+   * object.
+   *
+   * ※ This is an alias for Object.preventExtensions.
+   */
+  preventExtensions,
+
+  /**
+   * Marks the provided object as non·extensible and marks all its
+   * properties as nonconfigurable, and returns the object.
+   *
+   * ※ This is an alias for Object.seal.
+   */
+  seal,
+
+  /**
+   * Sets the values of the enumerable own properties of the provided
+   * additional objects on the provided object.
+   *
+   * ※ This is an alias for Object.assign.
+   */
+  assign: setPropertyValues,
+
+  /**
+   * Sets the prototype of the provided object to the provided value
+   * and returns the object.
+   *
+   * ※ This is an alias for Object.setPrototypeOf.
+   */
+  setPrototypeOf: setPrototype,
+} = Object;
+
+export const {
+  /**
+   * Removes the provided property key from the provided object and
+   * returns the object.
+   *
+   * ☡ This function differs from Reflect.deleteProperty and the
+   * `delete` operator in that it throws if the deletion is
+   * unsuccessful.
+   */
+  deleteOwnProperty,
+
+  /**
+   * Sets the provided property key to the provided value on the
+   * provided object and returns the object.
+   *
+   * ※ This function differs from Reflect.set in that it throws if the
+   * setting is unsuccessful.
+   */
+  setPropertyValue,
+} = (() => {
+  const { deleteProperty, set } = Reflect;
+
+  return {
+    deleteOwnProperty: (O, P) => {
+      if (!deleteProperty(O, P)) {
+        throw new TypeError(
+          `Piscēs: Tried to delete property from object but [[Delete]] returned false: ${P}`,
+        );
       } else {
-        // Method returns an object.
-        continue;
+        return O;
       }
-    } else {
-      // Method is not callable.
-      continue;
-    }
-  }
-  throw new TypeError("Piscēs: Unable to convert object to primitive");
-};
+    },
+    setPropertyValue: (O, P, V, Receiver = O) => {
+      if (!set(O, P, V, Receiver)) {
+        throw new TypeError(
+          `Piscēs: Tried to set property on object but [[Set]] returned false: ${P}`,
+        );
+      } else {
+        return O;
+      }
+    },
+  };
+})();
 
-/**
- * Returns the provided value converted to a primitive, or throws if
- * no such conversion is possible.
- *
- * The provided preferred type, if specified, should be "string",
- * "number", or "default". If the provided input has a
- * `Symbol.toPrimitive` method, this function will throw rather than
- * calling that method with a preferred type other than one of the
- * above.
- */
-export const toPrimitive = ($, preferredType) => {
-  if (isObject($)) {
-    // The provided value is an object.
-    const exoticToPrim = $[Symbol.toPrimitive] ?? undefined;
-    if (exoticToPrim !== undefined) {
-      // The provided value has an exotic primitive conversion method.
-      if (typeof exoticToPrim != "function") {
-        // The method is not callable.
+export const {
+  /**
+   * Returns a new frozen shallow copy of the enumerable own properties
+   * of the provided object, according to the following rules :—
+   *
+   * - For data properties, create a nonconfigurable, nonwritable
+   *   property with the same value.
+   *
+   * - For accessor properties, create a nonconfigurable accessor
+   *   property with the same getter *and* setter.
+   *
+   * The prototype for the resulting object will be taken from the
+   * `prototype` property of the provided constructor, or the
+   * `prototype` of the `constructor` of the provided object if the
+   * provided constructor is undefined. If the used constructor has a
+   * nonnullish `Symbol.species`, that will be used instead. If the
+   * used constructor or species is nullish or does not have a
+   * `prototype` property, the prototype is set to null.
+   */
+  frozenCopy,
+} = (() => {
+  const {
+    iterator: iteratorSymbol,
+    species: speciesSymbol,
+  } = Symbol;
+  const {
+    next: generatorIteratorNext,
+  } = getPrototype(function* () {}.prototype);
+  const propertyDescriptorEntryIterablePrototype = {
+    [iteratorSymbol]() {
+      return {
+        next: bind(generatorIteratorNext, this.generator(), []),
+      };
+    },
+  };
+  return {
+    frozenCopy: (O, constructor = O?.constructor) => {
+      if (O == null) {
+        // O is null or undefined.
         throw new TypeError(
-          "Piscēs: Symbol.toPrimitive was neither nullish nor callable.",
+          "Piscēs: Cannot copy properties of null or undefined.",
         );
       } else {
-        // The method is callable.
-        const hint = `${preferredType ?? "default"}`;
-        if (!["default", "string", "number"].includes(hint)) {
-          // An invalid preferred type was specified.
-          throw new TypeError(
-            `Piscēs: Invalid preferred type: ${preferredType}.`,
-          );
-        } else {
-          // The resulting hint is either default, string, or number.
-          return call(exoticToPrim, $, [hint]);
-        }
+        // O is not null or undefined.
+        //
+        // (If not provided, the constructor will be the value of
+        // getting the `constructor` property of O.)
+        const species = constructor?.[speciesSymbol] ?? constructor;
+        return preventExtensions(
+          objectCreate(
+            species == null || !("prototype" in species)
+              ? null
+              : species.prototype,
+            objectFromEntries(
+              objectCreate(
+                propertyDescriptorEntryIterablePrototype,
+                {
+                  generator: {
+                    value: function* () {
+                      const ownPropertyKeys = getOwnPropertyKeys(O);
+                      for (
+                        let i = 0;
+                        i < ownPropertyKeys.length;
+                        ++i
+                      ) {
+                        const P = ownPropertyKeys[i];
+                        const Desc = getOwnPropertyDescriptor(O, P);
+                        if (Desc.enumerable) {
+                          // P is an enumerable property.
+                          yield [
+                            P,
+                            "get" in Desc || "set" in Desc
+                              ? {
+                                configurable: false,
+                                enumerable: true,
+                                get: Desc.get,
+                                set: Desc.set,
+                              }
+                              : {
+                                configurable: false,
+                                enumerable: true,
+                                value: Desc.value,
+                                writable: false,
+                              },
+                          ];
+                        } else {
+                          // P is not an enumerable property.
+                          /* do nothing */
+                        }
+                      }
+                    },
+                  },
+                },
+              ),
+            ),
+          ),
+        );
       }
-    } else {
-      // Use the ordinary primitive conversion function.
-      ordinaryToPrimitive($, hint);
-    }
-  } else {
-    // The provided value is already a primitive.
-    return $;
-  }
-};
+    },
+  };
+})();
+
+export const {
+  /**
+   * Returns an array of property keys on the provided object.
+   *
+   * ※ This is an alias for Reflect.ownKeys.
+   */
+  ownKeys: getOwnPropertyKeys,
+
+  /**
+   * Returns the value of the provided property key on the provided
+   * object.
+   *
+   * ※ This is an alias for Reflect.get.
+   */
+  get: getPropertyValue,
+
+  /**
+   * Returns whether the provided property key exists on the provided
+   * object.
+   *
+   * ※ This is an alias for Reflect.has.
+   *
+   * ※ This includes properties present on the prototype chain.
+   */
+  has: hasProperty,
+} = Reflect;
+
+/**
+ * Returns the provided value converted to an object.
+ *
+ * Null and undefined are converted to a new, empty object. Other
+ * primitives are wrapped. Existing objects are returned with no
+ * modification.
+ *
+ * ※ This is effectively a nonconstructible version of the Object
+ * constructor.
+ */
+export const { toObject } = (() => {
+  const makeObject = Object;
+  return { toObject: ($) => makeObject($) };
+})();
 
 /**
  * Returns the property key (symbol or string) corresponding to the
diff --git a/object.test.js b/object.test.js
new file mode 100644 (file)
index 0000000..da890d3
--- /dev/null
@@ -0,0 +1,734 @@
+// ♓🌟 Piscēs ∷ object.test.js
+// ====================================================================
+//
+// Copyright © 2022 Lady [@ Lady’s Computer].
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at <https://mozilla.org/MPL/2.0/>.
+
+import {
+  assert,
+  assertEquals,
+  assertSpyCall,
+  assertSpyCalls,
+  assertStrictEquals,
+  assertThrows,
+  describe,
+  it,
+  spy,
+} from "./dev-deps.js";
+import {
+  defineOwnProperties,
+  deleteOwnProperty,
+  frozenCopy,
+  PropertyDescriptor,
+  setPropertyValue,
+  toObject,
+  toPropertyKey,
+} from "./object.js";
+
+describe("PropertyDescriptor", () => {
+  it("[[Construct]] creates a new PropertyDescriptor", () => {
+    assertStrictEquals(
+      Object.getPrototypeOf(new PropertyDescriptor({})),
+      PropertyDescriptor.prototype,
+    );
+  });
+
+  it("[[Construct]] throws for primitives", () => {
+    assertThrows(() => new PropertyDescriptor("failure"));
+  });
+
+  describe("::complete", () => {
+    it("[[Call]] completes a generic descriptor", () => {
+      const desc = {};
+      PropertyDescriptor.prototype.complete.call(desc);
+      assertEquals(desc, {
+        configurable: false,
+        enumerable: false,
+        value: undefined,
+        writable: false,
+      });
+    });
+
+    it("[[Call]] completes a data descriptor", () => {
+      const desc = { value: undefined };
+      PropertyDescriptor.prototype.complete.call(desc);
+      assertEquals(desc, {
+        configurable: false,
+        enumerable: false,
+        value: undefined,
+        writable: false,
+      });
+    });
+
+    it("[[Call]] completes an accessor descriptor", () => {
+      const desc = { get: undefined };
+      PropertyDescriptor.prototype.complete.call(desc);
+      assertEquals(desc, {
+        configurable: false,
+        enumerable: false,
+        get: undefined,
+        set: undefined,
+      });
+    });
+  });
+
+  describe("::isAccessorDescriptor", () => {
+    it("[[Get]] returns false for a generic descriptor", () => {
+      assertStrictEquals(
+        Reflect.get(
+          PropertyDescriptor.prototype,
+          "isAccessorDescriptor",
+          {},
+        ),
+        false,
+      );
+    });
+
+    it("[[Get]] returns false for a data descriptor", () => {
+      assertStrictEquals(
+        Reflect.get(
+          PropertyDescriptor.prototype,
+          "isAccessorDescriptor",
+          { value: undefined },
+        ),
+        false,
+      );
+    });
+
+    it("[[Get]] returns true for an accessor descriptor", () => {
+      assertStrictEquals(
+        Reflect.get(
+          PropertyDescriptor.prototype,
+          "isAccessorDescriptor",
+          { get: undefined },
+        ),
+        true,
+      );
+    });
+  });
+
+  describe("::isDataDescriptor", () => {
+    it("[[Get]] returns false for a generic descriptor", () => {
+      assertStrictEquals(
+        Reflect.get(
+          PropertyDescriptor.prototype,
+          "isDataDescriptor",
+          {},
+        ),
+        false,
+      );
+    });
+
+    it("[[Get]] returns true for a data descriptor", () => {
+      assertStrictEquals(
+        Reflect.get(
+          PropertyDescriptor.prototype,
+          "isDataDescriptor",
+          { value: undefined },
+        ),
+        true,
+      );
+    });
+
+    it("[[Get]] returns false for an accessor descriptor", () => {
+      assertStrictEquals(
+        Reflect.get(
+          PropertyDescriptor.prototype,
+          "isDataDescriptor",
+          { get: undefined },
+        ),
+        false,
+      );
+    });
+  });
+
+  describe("::isFullyPopulated", () => {
+    it("[[Get]] returns false for a generic descriptor", () => {
+      assertStrictEquals(
+        Reflect.get(
+          PropertyDescriptor.prototype,
+          "isFullyPopulated",
+          {},
+        ),
+        false,
+      );
+    });
+
+    it("[[Get]] returns false for a non‐fully‐populated data descriptor", () => {
+      assertStrictEquals(
+        Reflect.get(
+          PropertyDescriptor.prototype,
+          "isFullyPopulated",
+          { value: undefined },
+        ),
+        false,
+      );
+    });
+
+    it("[[Get]] returns true for a fully‐populated data descriptor", () => {
+      assertStrictEquals(
+        Reflect.get(PropertyDescriptor.prototype, "isFullyPopulated", {
+          configurable: true,
+          enumerable: true,
+          value: undefined,
+          writable: true,
+        }),
+        true,
+      );
+    });
+
+    it("[[Get]] returns false for a non‐fully‐populated accessor descriptor", () => {
+      assertStrictEquals(
+        Reflect.get(
+          PropertyDescriptor.prototype,
+          "isFullyPopulated",
+          { get: undefined },
+        ),
+        false,
+      );
+    });
+
+    it("[[Get]] returns true for a fully‐populated accessor descriptor", () => {
+      assertStrictEquals(
+        Reflect.get(PropertyDescriptor.prototype, "isFullyPopulated", {
+          configurable: true,
+          enumerable: true,
+          get: undefined,
+          set: undefined,
+        }),
+        true,
+      );
+    });
+  });
+
+  describe("::isGenericDescriptor", () => {
+    it("[[Get]] returns true for a generic descriptor", () => {
+      assertStrictEquals(
+        Reflect.get(
+          PropertyDescriptor.prototype,
+          "isGenericDescriptor",
+          {},
+        ),
+        true,
+      );
+    });
+
+    it("[[Get]] returns true for a data descriptor", () => {
+      assertStrictEquals(
+        Reflect.get(
+          PropertyDescriptor.prototype,
+          "isGenericDescriptor",
+          { value: undefined },
+        ),
+        false,
+      );
+    });
+
+    it("[[Get]] returns false for an accessor descriptor", () => {
+      assertStrictEquals(
+        Reflect.get(
+          PropertyDescriptor.prototype,
+          "isGenericDescriptor",
+          { get: undefined },
+        ),
+        false,
+      );
+    });
+  });
+
+  describe("~configurable", () => {
+    it("[[DefineOwnProperty]] coerces to a boolean", () => {
+      const desc = new PropertyDescriptor({});
+      Object.defineProperty(desc, "configurable", {});
+      assertStrictEquals(desc.configurable, false);
+    });
+
+    it("[[DefineOwnProperty]] throws for accessor properties", () => {
+      const desc = new PropertyDescriptor({});
+      assertThrows(() =>
+        Object.defineProperty(desc, "configurable", { get: undefined })
+      );
+    });
+
+    it("[[Set]] coerces to a boolean", () => {
+      const desc = new PropertyDescriptor({});
+      desc.configurable = undefined;
+      assertStrictEquals(desc.configurable, false);
+    });
+
+    it("[[Delete]] works", () => {
+      const desc = new PropertyDescriptor({ configurable: false });
+      delete desc.configurable;
+      assert(!("configurable" in desc));
+    });
+  });
+
+  describe("~enumerable", () => {
+    it("[[DefineOwnProperty]] coerces to a boolean", () => {
+      const desc = new PropertyDescriptor({});
+      Object.defineProperty(desc, "enumerable", {});
+      assertStrictEquals(desc.enumerable, false);
+    });
+
+    it("[[DefineOwnProperty]] throws for accessor properties", () => {
+      const desc = new PropertyDescriptor({});
+      assertThrows(() =>
+        Object.defineProperty(desc, "enumerable", { get: undefined })
+      );
+    });
+
+    it("[[Set]] coerces to a boolean", () => {
+      const desc = new PropertyDescriptor({});
+      desc.enumerable = undefined;
+      assertStrictEquals(desc.enumerable, false);
+    });
+
+    it("[[Delete]] works", () => {
+      const desc = new PropertyDescriptor({ enumerable: false });
+      delete desc.enumerable;
+      assert(!("enumerable" in desc));
+    });
+  });
+
+  describe("~get", () => {
+    it("[[DefineOwnProperty]] works", () => {
+      const desc = new PropertyDescriptor({});
+      Object.defineProperty(desc, "get", {});
+      assertStrictEquals(desc.get, undefined);
+    });
+
+    it("[[DefineOwnProperty]] throws for accessor properties", () => {
+      const desc = new PropertyDescriptor({});
+      assertThrows(() =>
+        Object.defineProperty(desc, "get", { get: undefined })
+      );
+    });
+
+    it("[[DefineOwnProperty]] throws if not callable or undefined", () => {
+      const desc = new PropertyDescriptor({});
+      assertThrows(
+        () => Object.defineProperty(desc, "get", { value: null }),
+      );
+    });
+
+    it("[[DefineOwnProperty]] throws if a data property is defined", () => {
+      const desc = new PropertyDescriptor({ value: undefined });
+      assertThrows(() => Object.defineProperty(desc, "get", {}));
+    });
+
+    it("[[Set]] works", () => {
+      const desc = new PropertyDescriptor({});
+      const fn = () => {};
+      desc.get = fn;
+      assertStrictEquals(desc.get, fn);
+    });
+
+    it("[[Set]] throws if not callable or undefined", () => {
+      const desc = new PropertyDescriptor({});
+      assertThrows(() => desc.get = null);
+    });
+
+    it("[[Set]] throws if a data property is defined", () => {
+      const desc = new PropertyDescriptor({ value: undefined });
+      assertThrows(() => desc.get = undefined);
+    });
+
+    it("[[Delete]] works", () => {
+      const desc = new PropertyDescriptor({ get: undefined });
+      delete desc.get;
+      assert(!("get" in desc));
+    });
+  });
+
+  describe("~set", () => {
+    it("[[DefineOwnProperty]] works", () => {
+      const desc = new PropertyDescriptor({});
+      Object.defineProperty(desc, "set", {});
+      assertStrictEquals(desc.set, undefined);
+    });
+
+    it("[[DefineOwnProperty]] throws for accessor properties", () => {
+      const desc = new PropertyDescriptor({});
+      assertThrows(() =>
+        Object.defineProperty(desc, "set", { get: undefined })
+      );
+    });
+
+    it("[[DefineOwnProperty]] throws if not callable or undefined", () => {
+      const desc = new PropertyDescriptor({});
+      assertThrows(
+        () => Object.defineProperty(desc, "set", { value: null }),
+      );
+    });
+
+    it("[[DefineOwnProperty]] throws if a data property is defined", () => {
+      const desc = new PropertyDescriptor({ value: undefined });
+      assertThrows(() => Object.defineProperty(desc, "set", {}));
+    });
+
+    it("[[Set]] works", () => {
+      const desc = new PropertyDescriptor({});
+      const fn = (_) => {};
+      desc.set = fn;
+      assertStrictEquals(desc.set, fn);
+    });
+
+    it("[[Set]] throws if not callable or undefined", () => {
+      const desc = new PropertyDescriptor({});
+      assertThrows(() => desc.set = null);
+    });
+
+    it("[[Set]] throws if a data property is defined", () => {
+      const desc = new PropertyDescriptor({ value: undefined });
+      assertThrows(() => desc.set = undefined);
+    });
+
+    it("[[Delete]] works", () => {
+      const desc = new PropertyDescriptor({ set: undefined });
+      delete desc.set;
+      assert(!("set" in desc));
+    });
+  });
+
+  describe("~value", () => {
+    it("[[DefineOwnProperty]] works", () => {
+      const desc = new PropertyDescriptor({});
+      Object.defineProperty(desc, "value", {});
+      assertStrictEquals(desc.value, undefined);
+    });
+
+    it("[[DefineOwnProperty]] throws for accessor properties", () => {
+      const desc = new PropertyDescriptor({});
+      assertThrows(() =>
+        Object.defineProperty(desc, "value", { get: undefined })
+      );
+    });
+
+    it("[[DefineOwnProperty]] throws if an accessor property is defined", () => {
+      const desc = new PropertyDescriptor({ get: undefined });
+      assertThrows(() => Object.defineProperty(desc, "value", {}));
+    });
+
+    it("[[Set]] works", () => {
+      const desc = new PropertyDescriptor({});
+      desc.value = "success";
+      assertStrictEquals(desc.value, "success");
+    });
+
+    it("[[Set]] throws if an accessor property is defined", () => {
+      const desc = new PropertyDescriptor({ get: undefined });
+      assertThrows(() => desc.value = null);
+    });
+
+    it("[[Delete]] works", () => {
+      const desc = new PropertyDescriptor({ value: undefined });
+      delete desc.value;
+      assert(!("value" in desc));
+    });
+  });
+
+  describe("~writable", () => {
+    it("[[DefineOwnProperty]] coerces to a boolean", () => {
+      const desc = new PropertyDescriptor({});
+      Object.defineProperty(desc, "writable", {});
+      assertStrictEquals(desc.writable, false);
+    });
+
+    it("[[DefineOwnProperty]] throws for accessor properties", () => {
+      const desc = new PropertyDescriptor({});
+      assertThrows(() =>
+        Object.defineProperty(desc, "writable", { get: undefined })
+      );
+    });
+
+    it("[[DefineOwnProperty]] throws if an accessor property is defined", () => {
+      const desc = new PropertyDescriptor({ get: undefined });
+      assertThrows(() => Object.defineProperty(desc, "writable", {}));
+    });
+
+    it("[[Set]] coerces to a boolean", () => {
+      const desc = new PropertyDescriptor({});
+      desc.writable = undefined;
+      assertStrictEquals(desc.writable, false);
+    });
+
+    it("[[Set]] throws if an accessor property is defined", () => {
+      const desc = new PropertyDescriptor({ get: undefined });
+      assertThrows(() => desc.writable = false);
+    });
+
+    it("[[Delete]] works", () => {
+      const desc = new PropertyDescriptor({ writable: false });
+      delete desc.writable;
+      assert(!("writable" in desc));
+    });
+  });
+});
+
+describe("defineOwnProperties", () => {
+  it("[[Call]] defines properties from the provided objects", () => {
+    const obj = {};
+    defineOwnProperties(obj, {
+      etaoin: {},
+      shrdlu: {},
+    }, { cmfwyp: {} });
+    assert("etaoin" in obj);
+    assert("shrdlu" in obj);
+    assert("cmfwyp" in obj);
+  });
+
+  it("[[Call]] overrides earlier declarations with later ones", () => {
+    const obj = { etaoin: undefined };
+    defineOwnProperties(obj, {
+      etaoin: { value: "failure" },
+    }, {
+      etaoin: { value: "success" },
+    });
+    assertStrictEquals(obj.etaoin, "success");
+  });
+
+  it("[[Call]] returns the provided object", () => {
+    const obj = {};
+    assertStrictEquals(defineOwnProperties(obj), obj);
+  });
+});
+
+describe("deleteOwnProperty", () => {
+  it("[[Call]] deletes the provided property on the provided object", () => {
+    const obj = { failure: undefined };
+    deleteOwnProperty(obj, "failure");
+    assert(!("failure" in obj));
+  });
+
+  it("[[Call]] does nothing if the property doesn’t exist", () => {
+    const obj = Object.freeze({});
+    deleteOwnProperty(obj, "failure");
+    assert(!("failure" in obj));
+  });
+
+  it("[[Call]] throws if the property can’t be deleted", () => {
+    const obj = Object.seal({ failure: undefined });
+    assertThrows(() => deleteOwnProperty(obj, "failure"));
+  });
+
+  it("[[Call]] returns the provided object", () => {
+    const obj = {};
+    assertStrictEquals(deleteOwnProperty(obj, ""), obj);
+  });
+});
+
+describe("frozenCopy", () => {
+  it("[[Call]] returns a frozen object", () => {
+    assert(
+      Object.isFrozen(
+        frozenCopy(Object.create(null), {
+          data: {
+            configurable: true,
+            enumerable: true,
+            value: undefined,
+            writable: true,
+          },
+          accessor: {
+            configurable: true,
+            enumerable: true,
+            get: undefined,
+          },
+        }),
+      ),
+    );
+  });
+
+  it("[[Call]] ignores non·enumerable properties", () => {
+    assertEquals(
+      frozenCopy(
+        Object.create(null, {
+          data: { value: undefined },
+          accessor: { get: undefined },
+        }),
+      ),
+      {},
+    );
+  });
+
+  it("[[Call]] preserves accessor properties", () => {
+    const properties = {
+      both: {
+        configurable: false,
+        enumerable: true,
+        get: () => {},
+        set: (_) => {},
+      },
+      empty: {
+        configurable: false,
+        enumerable: true,
+        get: undefined,
+        set: undefined,
+      },
+      getter: {
+        configurable: false,
+        enumerable: true,
+        get: () => {},
+        set: undefined,
+      },
+      setter: {
+        configurable: false,
+        enumerable: true,
+        get: undefined,
+        set: (_) => {},
+      },
+    };
+    assertEquals(
+      Object.getOwnPropertyDescriptors(
+        frozenCopy(Object.create(null, properties)),
+      ),
+      properties,
+    );
+  });
+
+  it("[[Call]] does not copy properties on the prototype", () => {
+    assert(
+      !("failure" in
+        frozenCopy(Object.create({ failure: undefined }), {
+          data: {
+            configurable: true,
+            value: undefined,
+            writable: true,
+          },
+          accessor: { configurable: true, get: undefined },
+        })),
+    );
+  });
+
+  it("[[Call]] uses the species of the constructor", () => {
+    const species = { prototype: {} };
+    assertStrictEquals(
+      Object.getPrototypeOf(
+        frozenCopy({}, { [Symbol.species]: species }),
+      ),
+      species.prototype,
+    );
+  });
+
+  it("[[Call]] uses constructor if no species is defined", () => {
+    const constructor = { [Symbol.species]: null, prototype: {} };
+    assertStrictEquals(
+      Object.getPrototypeOf(frozenCopy({}, constructor)),
+      constructor.prototype,
+    );
+  });
+
+  it("[[Call]] uses the constructor on the object if none is provided", () => {
+    const constructor = { [Symbol.species]: null, prototype: {} };
+    assertStrictEquals(
+      Object.getPrototypeOf(frozenCopy({ constructor })),
+      constructor.prototype,
+    );
+  });
+
+  it("[[Call]] allows a null constructor", () => {
+    assertStrictEquals(
+      Object.getPrototypeOf(frozenCopy({}, null)),
+      null,
+    );
+  });
+});
+
+describe("setPropertyValue", () => {
+  it("[[Call]] sets the provided property on the provided object", () => {
+    const obj = {};
+    setPropertyValue(obj, "success", true);
+    assertStrictEquals(obj.success, true);
+  });
+
+  it("[[Call]] calls setters", () => {
+    const setter = spy((_) => {});
+    const obj = Object.create(null, { success: { set: setter } });
+    setPropertyValue(obj, "success", true);
+    assertSpyCalls(setter, 1);
+    assertSpyCall(setter, 0, {
+      args: [true],
+      self: obj,
+    });
+  });
+
+  it("[[Call]] walks the prototype chain", () => {
+    const setter = spy((_) => {});
+    const obj = Object.create(
+      Object.create(null, { success: { set: setter } }),
+    );
+    setPropertyValue(obj, "success", true);
+    assertSpyCalls(setter, 1);
+    assertSpyCall(setter, 0, {
+      args: [true],
+      self: obj,
+    });
+  });
+
+  it("[[Call]] uses the provided receiver", () => {
+    const setter = spy((_) => {});
+    const obj = Object.create(null, { success: { set: setter } });
+    const receiver = {};
+    setPropertyValue(obj, "success", true, receiver);
+    assertSpyCalls(setter, 1);
+    assertSpyCall(setter, 0, {
+      args: [true],
+      self: receiver,
+    });
+  });
+
+  it("[[Call]] throws if the property can’t be set", () => {
+    const obj = Object.freeze({ failure: undefined });
+    assertThrows(() => setPropertyValue(obj, "failure", true));
+  });
+
+  it("[[Call]] returns the provided object", () => {
+    const obj = {};
+    assertStrictEquals(setPropertyValue(obj, "", undefined), obj);
+  });
+});
+
+describe("toObject", () => {
+  it("returns the input for objects", () => {
+    const obj = {};
+    assertStrictEquals(toObject(obj), obj);
+  });
+
+  it("returns a new object for nullish values", () => {
+    assertEquals(toObject(null), {});
+    assertEquals(toObject(void {}), {});
+  });
+
+  it("returns a wrapper object for other primitives", () => {
+    const sym = Symbol();
+    assertStrictEquals(typeof toObject(sym), "object");
+    assertStrictEquals(toObject(sym).valueOf(), sym);
+  });
+});
+
+describe("toPropertyKey", () => {
+  it("returns a string or symbol", () => {
+    const sym = Symbol();
+    assertStrictEquals(toPropertyKey(sym), sym);
+    assertStrictEquals(
+      toPropertyKey(new String("success")),
+      "success",
+    );
+  });
+
+  it("favours the `toString` representation", () => {
+    assertStrictEquals(
+      toPropertyKey({
+        toString() {
+          return "success";
+        },
+        valueOf() {
+          return "failure";
+        },
+      }),
+      "success",
+    );
+  });
+});
diff --git a/value.js b/value.js
new file mode 100644 (file)
index 0000000..77cb6b4
--- /dev/null
+++ b/value.js
@@ -0,0 +1,160 @@
+// ♓🌟 Piscēs ∷ value.js
+// ====================================================================
+//
+// Copyright © 2022 Lady [@ Lady’s Computer].
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at <https://mozilla.org/MPL/2.0/>.
+
+import { call } from "./function.js";
+
+/** The null primitive. */
+export const NULL = null;
+
+/** The undefined primitive. */
+export const UNDEFINED = undefined;
+
+export const {
+  /**
+   * Returns the primitive value of the provided object per its
+   * `toString` and `valueOf` methods.
+   *
+   * If the provided hint is "string", then `toString` takes
+   * precedence; otherwise, `valueOf` does.
+   *
+   * Throws an error if both of these methods are not callable or do
+   * not return a primitive.
+   */
+  ordinaryToPrimitive,
+
+  /**
+   * Returns the provided value converted to a primitive, or throws if
+   * no such conversion is possible.
+   *
+   * The provided preferred type, if specified, should be "string",
+   * "number", or "default". If the provided input has a
+   * `[Symbol.toPrimitive]` method, this function will throw rather
+   * than calling that method with a preferred type other than one of
+   * the above.
+   */
+  toPrimitive,
+} = (() => {
+  const { toPrimitive: toPrimitiveSymbol } = Symbol;
+
+  return {
+    ordinaryToPrimitive: (O, hint) => {
+      const methodNames = hint == "string"
+        ? ["toString", "valueOf"]
+        : ["valueOf", "toString"];
+      for (let index = 0; index < methodNames.length; ++index) {
+        const method = O[methodNames[index]];
+        if (typeof method === "function") {
+          // Method is callable.
+          const result = call(method, O, []);
+          if (type(result) !== "object") {
+            // Method returns a primitive.
+            return result;
+          } else {
+            // Method returns an object.
+            continue;
+          }
+        } else {
+          // Method is not callable.
+          continue;
+        }
+      }
+      throw new TypeError(
+        "Piscēs: Unable to convert object to primitive",
+      );
+    },
+    toPrimitive: ($, preferredType = "default") => {
+      const hint = `${preferredType}`;
+      if (
+        "default" !== hint && "string" !== hint &&
+        "number" !== hint
+      ) {
+        // An invalid preferred type was specified.
+        throw new TypeError(
+          `Piscēs: Invalid preferred type: ${preferredType}.`,
+        );
+      } else if (type($) === "object") {
+        // The provided value is an object.
+        const exoticToPrim = $[toPrimitiveSymbol] ?? undefined;
+        if (exoticToPrim !== undefined) {
+          // The provided value has an exotic primitive conversion
+          // method.
+          if (typeof exoticToPrim !== "function") {
+            // The method is not callable.
+            throw new TypeError(
+              "Piscēs: `[Symbol.toPrimitive]` was neither nullish nor callable.",
+            );
+          } else {
+            // The method is callable.
+            return call(exoticToPrim, $, [hint]);
+          }
+        } else {
+          // Use the ordinary primitive conversion function.
+          return ordinaryToPrimitive($, hint);
+        }
+      } else {
+        // The provided value is already a primitive.
+        return $;
+      }
+    },
+  };
+})();
+
+/**
+ * Returns whether the provided values are the same value.
+ *
+ * ※ This differs from `===` in the cases of nan and zero.
+ */
+export const sameValue = Object.is;
+
+export const {
+  /**
+   * Returns whether the provided values are either the same value or
+   * both zero (either positive or negative).
+   *
+   * ※ This differs from `===` in the case of nan.
+   */
+  sameValueZero,
+} = (() => {
+  const { isNaN: isNan } = Number;
+  return {
+    sameValueZero: ($1, $2) => {
+      const type1 = type($1);
+      const type2 = type($2);
+      if (type1 !== type2) {
+        // The provided values are not of the same type.
+        return false;
+      } else if (type1 === "number") {
+        // The provided values are numbers; check if they are nan and
+        // use strict equality otherwise.
+        return isNan($1) && isNan($2) || $1 === $2;
+      } else {
+        // The provided values are not numbers; use strict equality.
+        return $1 === $2;
+      }
+    },
+  };
+})();
+
+/**
+ * Returns a lowercase string identifying the type of the provided
+ * value.
+ *
+ * This differs from the value of the `typeof` operator only in the
+ * cases of objects and null.
+ */
+export const type = ($) => {
+  if ($ === null) {
+    // The provided value is null.
+    return "null";
+  } else {
+    // The provided value is not null.
+    const type·of = typeof $;
+    return type·of === "function" ? "object" : type·of;
+  }
+};
diff --git a/value.test.js b/value.test.js
new file mode 100644 (file)
index 0000000..6729b85
--- /dev/null
@@ -0,0 +1,315 @@
+// ♓🌟 Piscēs ∷ value.test.js
+// ====================================================================
+//
+// Copyright © 2022 Lady [@ Lady’s Computer].
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at <https://mozilla.org/MPL/2.0/>.
+
+import {
+  assert,
+  assertStrictEquals,
+  assertThrows,
+  describe,
+  it,
+} from "./dev-deps.js";
+import {
+  NULL,
+  ordinaryToPrimitive,
+  sameValue,
+  sameValueZero,
+  toPrimitive,
+  type,
+  UNDEFINED,
+} from "./value.js";
+
+describe("NULL", () => {
+  it("[[Get]] is null", () => {
+    assertStrictEquals(NULL, null);
+  });
+});
+
+describe("UNDEFINED", () => {
+  it("[[Get]] is undefined", () => {
+    assertStrictEquals(UNDEFINED, void {});
+  });
+});
+
+describe("ordinaryToPrimitive", () => {
+  it("[[Call]] prefers `valueOf` by default", () => {
+    const obj = {
+      toString() {
+        return "failure";
+      },
+      valueOf() {
+        return "success";
+      },
+    };
+    assertStrictEquals(ordinaryToPrimitive(obj), "success");
+    assertStrictEquals(ordinaryToPrimitive(obj, "default"), "success");
+  });
+
+  it('[[Call]] prefers `valueOf` for a "number" hint', () => {
+    const obj = {
+      toString() {
+        return "failure";
+      },
+      valueOf() {
+        return "success";
+      },
+    };
+    assertStrictEquals(ordinaryToPrimitive(obj, "number"), "success");
+  });
+
+  it('[[Call]] prefers `toString` for a "string" hint', () => {
+    const obj = {
+      toString() {
+        return "success";
+      },
+      valueOf() {
+        return "failure";
+      },
+    };
+    assertStrictEquals(ordinaryToPrimitive(obj, "string"), "success");
+  });
+
+  it("[[Call]] falls back to the other method if the first isn’t callable", () => {
+    const obj = {
+      toString() {
+        return "success";
+      },
+      valueOf: "failure",
+    };
+    assertStrictEquals(ordinaryToPrimitive(obj), "success");
+  });
+
+  it("[[Call]] falls back to the other method if the first returns an object", () => {
+    const obj = {
+      toString() {
+        return "success";
+      },
+      valueOf() {
+        return new String("failure");
+      },
+    };
+    assertStrictEquals(ordinaryToPrimitive(obj), "success");
+  });
+
+  it("[[Call]] throws an error if neither method is callable", () => {
+    const obj = {
+      toString: "failure",
+      valueOf: "failure",
+    };
+    assertThrows(() => ordinaryToPrimitive(obj));
+  });
+
+  it("[[Call]] throws an error if neither method returns an object", () => {
+    const obj = {
+      toString() {
+        return new String("failure");
+      },
+      valueOf() {
+        return new String("failure");
+      },
+    };
+    assertThrows(() => ordinaryToPrimitive(obj));
+  });
+});
+
+describe("sameValue", () => {
+  it("[[Call]] returns false for null 🆚 undefined", () => {
+    assert(!sameValue(null, void {}));
+  });
+
+  it("[[Call]] returns false for null 🆚 an object", () => {
+    assert(!sameValue(null, {}));
+  });
+
+  it("[[Call]] returns true for null 🆚 null", () => {
+    assert(sameValue(null, null));
+  });
+
+  it("[[Call]] returns false for two different objects", () => {
+    assert(!sameValue({}, {}));
+  });
+
+  it("[[Call]] returns true for the same object", () => {
+    const obj = {};
+    assert(sameValue(obj, obj));
+  });
+
+  it("[[Call]] returns false for ±0", () => {
+    assert(!sameValue(0, -0));
+  });
+
+  it("[[Call]] returns true for -0", () => {
+    assert(sameValue(-0, -0));
+  });
+
+  it("[[Call]] returns true for nan", () => {
+    assert(sameValue(0 / 0, 0 / 0));
+  });
+
+  it("[[Call]] returns false for a primitive and its wrapped object", () => {
+    assert(!sameValue(false, new Boolean(false)));
+  });
+});
+
+describe("sameValueZero", () => {
+  it("[[Call]] returns false for null 🆚 undefined", () => {
+    assert(!sameValueZero(null, void {}));
+  });
+
+  it("[[Call]] returns false for null 🆚 an object", () => {
+    assert(!sameValueZero(null, {}));
+  });
+
+  it("[[Call]] returns true for null 🆚 null", () => {
+    assert(sameValueZero(null, null));
+  });
+
+  it("[[Call]] returns false for two different objects", () => {
+    assert(!sameValueZero({}, {}));
+  });
+
+  it("[[Call]] returns true for the same object", () => {
+    const obj = {};
+    assert(sameValueZero(obj, obj));
+  });
+
+  it("[[Call]] returns true for ±0", () => {
+    assert(sameValueZero(0, -0));
+  });
+
+  it("[[Call]] returns true for -0", () => {
+    assert(sameValueZero(-0, -0));
+  });
+
+  it("[[Call]] returns true for nan", () => {
+    assert(sameValueZero(0 / 0, 0 / 0));
+  });
+
+  it("[[Call]] returns false for a primitive and its wrapped object", () => {
+    assert(!sameValueZero(false, new Boolean(false)));
+  });
+});
+
+describe("toPrimitive", () => {
+  it("[[Call]] returns the argument when passed a primitive", () => {
+    const value = Symbol();
+    assertStrictEquals(toPrimitive(value), value);
+  });
+
+  it("[[Call]] works with nullish values", () => {
+    assertStrictEquals(toPrimitive(null), null);
+    assertStrictEquals(toPrimitive(), void {});
+  });
+
+  it("[[Call]] calls ordinaryToPrimitive by default", () => {
+    const value = Object.assign(
+      Object.create(null),
+      {
+        valueOf() {
+          return "success";
+        },
+      },
+    );
+    assertStrictEquals(toPrimitive(value), "success");
+  });
+
+  it("[[Call]] accepts a hint", () => {
+    const value = Object.assign(
+      Object.create(null),
+      {
+        toString() {
+          return "success";
+        },
+        valueOf() {
+          return "failure";
+        },
+      },
+    );
+    assertStrictEquals(toPrimitive(value, "string"), "success");
+  });
+
+  it("[[Call]] uses the exotic toPrimitive method if available", () => {
+    const value = Object.assign(
+      Object.create(null),
+      {
+        [Symbol.toPrimitive]() {
+          return "success";
+        },
+      },
+    );
+    assertStrictEquals(toPrimitive(value), "success");
+  });
+
+  it("[[Call]] passes the hint to the exotic toPrimitive", () => {
+    const value = Object.assign(
+      Object.create(null),
+      {
+        [Symbol.toPrimitive](hint) {
+          return hint === "string" ? "success" : "failure";
+        },
+      },
+    );
+    assertStrictEquals(toPrimitive(value, "string"), "success");
+  });
+
+  it('[[Call]] passes a "default" hint by default', () => {
+    const value = Object.assign(
+      Object.create(null),
+      {
+        [Symbol.toPrimitive](hint) {
+          return hint === "default" ? "success" : "failure";
+        },
+      },
+    );
+    assertStrictEquals(toPrimitive(value, "default"), "success");
+  });
+
+  it("[[Call]] throws for an invalid hint", () => {
+    const value1 = Object.assign(
+      Object.create(null),
+      {
+        [Symbol.toPrimitive]() {
+          return "success";
+        },
+      },
+    );
+    const value2 = Object.assign(
+      Object.create(null),
+      {
+        valueOf() {
+          return true;
+        },
+      },
+    );
+    assertThrows(() => toPrimitive(value1, "badhint"));
+    assertThrows(() => toPrimitive(value2, "badhint"));
+    assertThrows(() => toPrimitive(true, "badhint"));
+  });
+});
+
+describe("type", () => {
+  it('[[Call]] returns "null" for null', () => {
+    assertStrictEquals(type(null), "null");
+  });
+
+  it('[[Call]] returns "undefined" for undefined', () => {
+    assertStrictEquals(type(void {}), "undefined");
+  });
+
+  it('[[Call]] returns "object" for non‐callable objects', () => {
+    assertStrictEquals(type(Object.create(null)), "object");
+  });
+
+  it('[[Call]] returns "object" for callable objects', () => {
+    assertStrictEquals(type(() => {}), "object");
+  });
+
+  it('[[Call]] returns "object" for constructable objects', () => {
+    assertStrictEquals(type(class {}), "object");
+  });
+});
This page took 0.098241 seconds and 4 git commands to generate.