]> Lady’s Gitweb - Pisces/blobdiff - object.js
Add buffer getters and setters to binary.js
[Pisces] / object.js
index 502207e3b6ed1f8496300af183e968c9a72e3f9e..a4037951fcd77fa746e8aabe23a974948e29ed9a 100644 (file)
--- a/object.js
+++ b/object.js
 // ♓🌟 Piscēs ∷ object.js
 // ====================================================================
 //
-// Copyright © 2022 Lady [@ Lady’s Computer].
+// Copyright © 2022–2023 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/>.
 
-/** Returns whether the provided value is a constructor. */
-export const isConstructor = ($) => {
-  if (!isObject($)) {
-    // The provided value is not an object.
-    return false;
-  } else {
-    // The provided value is an object.
-    try {
-      Reflect.construct(
-        function () {},
-        [],
-        $,
-      ); // will throw if $ is not a constructor
-      return true;
-    } catch {
-      return false;
+import { bind, call } from "./function.js";
+import { ITERATOR, SPECIES, toPrimitive, type } from "./value.js";
+
+/**
+ * An object whose properties are lazy‐loaded from the methods on the
+ * own properties of the provided object.
+ *
+ * This is useful when you are looking to reference properties on
+ * objects which, due to module dependency graphs, cannot be guaranteed
+ * to have been initialized yet.
+ *
+ * The resulting properties will have the same attributes (regarding
+ * configurability, enumerability, and writability) as the
+ * corresponding properties on the methods object. If a property is
+ * marked as writable, the method will never be called if it is set
+ * before it is gotten. By necessity, the resulting properties are all
+ * configurable before they are accessed for the first time.
+ *
+ * Methods will be called with the resulting object as their this
+ * value.
+ *
+ * `LazyLoader` objects have the same prototype as the passed methods
+ * object.
+ */
+export class LazyLoader extends null {
+  /**
+   * Constructs a new `LazyLoader` object.
+   *
+   * ☡ This function throws if the provided value is not an object.
+   */
+  constructor(loadMethods) {
+    if (type(loadMethods) !== "object") {
+      throw new TypeError(
+        `Piscēs: Cannot construct LazyLoader: Provided argument is not an object: ${loadMethods}.`,
+      );
+    } else {
+      const result = objectCreate(getPrototype(loadMethods));
+      const methodKeys = getOwnPropertyKeys(loadMethods);
+      for (let index = 0; index < methodKeys.length; ++index) {
+        const methodKey = methodKeys[index];
+        const { configurable, enumerable, writable } =
+          getOwnPropertyDescriptor(loadMethods, methodKey);
+        defineOwnProperty(result, methodKey, {
+          configurable: true,
+          enumerable,
+          get: () => {
+            const value = call(loadMethods[methodKey], result, []);
+            defineOwnProperty(result, methodKey, {
+              configurable,
+              enumerable,
+              value,
+              writable,
+            });
+            return value;
+          },
+          set: writable
+            ? ($) =>
+              defineOwnProperty(result, methodKey, {
+                configurable,
+                enumerable,
+                value: $,
+                writable,
+              })
+            : void {},
+        });
+      }
+      return result;
     }
   }
-};
+}
+
+/**
+ * A property descriptor object.
+ *
+ * Actually constructing a property descriptor object using this class
+ * is only necessary if you need strict guarantees about the types of
+ * its properties; the resulting object is proxied to ensure the types
+ * match what one would expect from composing FromPropertyDescriptor
+ * and ToPropertyDescriptor in the Ecmascript specification.
+ *
+ * Otherwise, the instance properties and methods are generic.
+ */
+export const { PropertyDescriptor } = (() => {
+  class PropertyDescriptor extends null {
+    /**
+     * Constructs a new property descriptor object from the provided
+     * object.
+     *
+     * The resulting object is proxied to enforce types (for example,
+     * its `.enumerable` property, if defined, will always be a
+     * boolean).
+     */
+    constructor(O) {
+      if (type(O) !== "object") {
+        // The provided value is not an object.
+        throw new TypeError(
+          `Piscēs: Cannot convert primitive to property descriptor: ${O}.`,
+        );
+      } else {
+        // The provided value is an object.
+        const desc = objectCreate(propertyDescriptorPrototype);
+        if ("enumerable" in O) {
+          // An enumerable property is specified.
+          desc.enumerable = !!O.enumerable;
+        } else {
+          // An enumerable property is not specified.
+          /* do nothing */
+        }
+        if ("configurable" in O) {
+          // A configurable property is specified.
+          desc.configurable = !!O.configurable;
+        } else {
+          // A configurable property is not specified.
+          /* do nothing */
+        }
+        if ("value" in O) {
+          // A value property is specified.
+          desc.value = O.value;
+        } else {
+          // A value property is not specified.
+          /* do nothing */
+        }
+        if ("writable" in O) {
+          // A writable property is specified.
+          desc.writable = !!O.writable;
+        } else {
+          // A writable property is not specified.
+          /* do nothing */
+        }
+        if ("get" in O) {
+          // A get property is specified.
+          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 {
+            // The getter is callable.
+            desc.get = getter;
+          }
+        } else {
+          // A get property is not specified.
+          /* do nothing */
+        }
+        if ("set" in O) {
+          // A set property is specified.
+          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 {
+            // The setter is callable.
+            desc.set = setter;
+          }
+        } else {
+          // A set property is not specified.
+          /* do nothing */
+        }
+        if (
+          ("get" in desc || "set" in desc) &&
+          ("value" in desc || "writable" in desc)
+        ) {
+          // Both accessor and data attributes have been defined.
+          throw new TypeError(
+            "Piscēs: Property descriptors cannot specify both accessor and data attributes.",
+          );
+        } else {
+          // The property descriptor is valid.
+          return new Proxy(desc, propertyDescriptorProxyHandler);
+        }
+      }
+    }
+
+    /**
+     * Completes this property descriptor by setting missing values to
+     * their defaults.
+     *
+     * This method modifies this object and returns undefined.
+     */
+    complete() {
+      if (this !== undefined && !("get" in this || "set" in this)) {
+        // This is a generic or data descriptor.
+        if (!("value" in this)) {
+          // `value` is not defined on this.
+          this.value = undefined;
+        } else {
+          // `value` is already defined on this.
+          /* do nothing */
+        }
+        if (!("writable" in this)) {
+          // `writable` is not defined on this.
+          this.writable = false;
+        } else {
+          // `writable` is already defined on this.
+          /* do nothing */
+        }
+      } else {
+        // This is not a generic or data descriptor.
+        if (!("get" in this)) {
+          // `get` is not defined on this.
+          this.get = undefined;
+        } else {
+          // `get` is already defined on this.
+          /* do nothing */
+        }
+        if (!("set" in this)) {
+          // `set` is not defined on this.
+          this.set = undefined;
+        } else {
+          // `set` is already defined on this.
+          /* do nothing */
+        }
+      }
+      if (!("enumerable" in this)) {
+        // `enumerable` is not defined on this.
+        this.enumerable = false;
+      } else {
+        // `enumerable` is already defined on this.
+        /* do nothing */
+      }
+      if (!("configurable" in this)) {
+        // `configurable` is not defined on this.
+        this.configurable = false;
+      } else {
+        // `configurable` is already defined on this.
+        /* do nothing */
+      }
+    }
+
+    /** Gets whether this is an accessor descrtiptor. */
+    get isAccessorDescriptor() {
+      return this !== undefined && ("get" in this || "set" in this);
+    }
+
+    /** Gets whether this is a data descrtiptor. */
+    get isDataDescriptor() {
+      return this !== undefined &&
+        ("value" in this || "writable" in this);
+    }
+
+    /** Gets whether this is a fully‐populated property descriptor. */
+    get isFullyPopulated() {
+      return this !== undefined &&
+        ("value" in this && "writable" in this ||
+          "get" in this && "set" in this) &&
+        "enumerable" in this && "configurable" in this;
+    }
+
+    /**
+     * Gets whether this is a generic (not accessor or data)
+     * descrtiptor.
+     */
+    get isGenericDescriptor() {
+      return this !== undefined &&
+        !("get" in this || "set" in this || "value" in this ||
+          "writable" in this);
+    }
+  }
+
+  const coercePropertyDescriptorValue = (P, V) => {
+    switch (P) {
+      case "configurable":
+      case "enumerable":
+      case "writable":
+        return !!V;
+      case "value":
+        return V;
+      case "get":
+        if (V !== undefined && typeof V !== "function") {
+          throw new TypeError(
+            "Piscēs: Getters must be callable.",
+          );
+        } else {
+          return V;
+        }
+      case "set":
+        if (V !== undefined && typeof V !== "function") {
+          throw new TypeError(
+            "Piscēs: Setters must be callable.",
+          );
+        } else {
+          return V;
+        }
+      default:
+        return V;
+    }
+  };
+
+  const {
+    prototype: propertyDescriptorPrototype,
+  } = PropertyDescriptor;
+
+  const propertyDescriptorProxyHandler = Object.assign(
+    Object.create(null),
+    {
+      defineProperty(O, P, Desc) {
+        if (
+          P === "configurable" || P === "enumerable" ||
+          P === "writable" || P === "value" ||
+          P === "get" || P === "set"
+        ) {
+          // P is a property descriptor attribute.
+          const desc = new PropertyDescriptor(Desc);
+          if ("get" in desc || "set" in desc) {
+            // Desc is an accessor property descriptor.
+            throw new TypeError(
+              "Piscēs: Property descriptor attributes must be data properties.",
+            );
+          } 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.
+            /* do nothing */
+          }
+          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.
+            return defineOwnProperty(O, P, desc);
+          }
+        } else {
+          // P is not a property descriptor attribute.
+          return defineOwnProperty(O, P, Desc);
+        }
+      },
+      set(O, P, V, Receiver) {
+        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 {
+          return setPropertyValue(O, P, V, Receiver);
+        }
+      },
+      setPrototypeOf(O, V) {
+        if (V !== propertyDescriptorPrototype) {
+          // V is not the property descriptor prototype.
+          return false;
+        } else {
+          // V is the property descriptor prototype.
+          return setPrototype(O, V);
+        }
+      },
+    },
+  );
+
+  return { PropertyDescriptor };
+})();
+
+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;
+    },
+  };
+})();
+
+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,
+
+  /**
+   * 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 whether the provided value is an object. */
-export const isObject = ($) => {
-  return $ !== null &&
-    (typeof $ == "function" || typeof $ == "object");
+  /**
+   * 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: isExtensibleObject,
+
+  /**
+   * Returns whether the provided object is frozen.
+   *
+   * ※ This is an alias for `Object.isFrozen`.
+   */
+  isFrozen: isFrozenObject,
+
+  /**
+   * Returns whether the provided object is sealed.
+   *
+   * ※ This is an alias for `Object.isSealed`.
+   */
+  isSealed: isSealedObject,
+
+  /**
+   * 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.
+   *
+   * ☡ This function throws if the first argument is not an object.
+   */
+  deleteOwnProperty,
+
+  /**
+   * Returns an array of property keys on the provided object.
+   *
+   * ※ This is effectively an alias for `Reflect.ownKeys`, except that
+   * it does not require that the argument be an object.
+   */
+  getOwnPropertyKeys,
+
+  /**
+   * Returns the value of the provided property key on the provided
+   * object.
+   *
+   * ※ This is effectively an alias for `Reflect.get`, except that it
+   * does not require that the argument be an object.
+   */
+  getPropertyValue,
+
+  /**
+   * Returns whether the provided property key exists on the provided
+   * object.
+   *
+   * ※ This is effectively an alias for `Reflect.has`, except that it
+   * does not require that the argument be an object.
+   *
+   * ※ This includes properties present on the prototype chain.
+   */
+  hasProperty,
+
+  /**
+   * 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.
+   *
+   * ☡ This function throws if the first argument is not an object.
+   */
+  setPropertyValue,
+} = (() => {
+  const { deleteProperty, get, has, ownKeys, set } = Reflect;
+
+  return {
+    deleteOwnProperty: (O, P) => {
+      if (type(O) !== "object") {
+        throw new TypeError(
+          `Piscēs: Tried to set property but provided value was not an object: ${V}`,
+        );
+      } else if (!deleteProperty(O, P)) {
+        throw new TypeError(
+          `Piscēs: Tried to delete property from object but [[Delete]] returned false: ${P}`,
+        );
+      } else {
+        return O;
+      }
+    },
+    getOwnPropertyKeys: (O) => ownKeys(toObject(O)),
+    getPropertyValue: (O, P, Receiver = O) =>
+      get(toObject(O), P, Receiver),
+    hasProperty: (O, P) => has(toObject(O), P),
+    setPropertyValue: (O, P, V, Receiver = O) => {
+      if (type(O) !== "object") {
+        throw new TypeError(
+          `Piscēs: Tried to set property but provided value was not an object: ${V}`,
+        );
+      } else 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;
+      }
+    },
+  };
+})();
+
+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.
+   *
+   * ※ The prototype of the provided object itself is ignored.
+   */
+  frozenCopy,
+} = (() => {
+  const {
+    next: generatorIteratorNext,
+  } = getPrototype(function* () {}.prototype);
+  const propertyDescriptorEntryIterablePrototype = {
+    [ITERATOR]() {
+      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: 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?.[SPECIES] ?? 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 */
+                        }
+                      }
+                    },
+                  },
+                },
+              ),
+            ),
+          ),
+        );
+      }
+    },
+  };
+})();
+
+/**
+ * Returns the function on the provided value at the provided property
+ * key.
+ *
+ * ☡ This function throws if the provided property key does not have an
+ * associated value which is callable.
+ */
+export const getMethod = (V, P) => {
+  const func = getPropertyValue(V, P);
+  if (func == null) {
+    return undefined;
+  } else if (typeof func !== "function") {
+    throw new TypeError(`Piscēs: Method not callable: ${P}`);
+  } else {
+    return func;
+  }
 };
 
 /**
- * Returns whether the provided object inherits from the prototype of
- * the provided function.
+ * Returns the provided value converted to an object.
+ *
+ * Existing objects are returned with no modification.
+ *
+ * ☡ This function throws if its argument is null or undefined.
  */
-export const ordinaryHasInstance = Function.prototype.call.bind(
-  Function.prototype[Symbol.hasInstance],
-);
+export const { toObject } = (() => {
+  const makeObject = Object;
+  return {
+    toObject: ($) => {
+      if ($ == null) {
+        throw new TypeError(
+          `Piscēs: Cannot convert ${$} into an object.`,
+        );
+      } else {
+        return makeObject($);
+      }
+    },
+  };
+})();
+
+/**
+ * Returns the property key (symbol or string) corresponding to the
+ * provided value.
+ */
+export const toPropertyKey = ($) => {
+  const key = toPrimitive($, "string");
+  return typeof key == "symbol" ? key : `${key}`;
+};
This page took 0.03674 seconds and 4 git commands to generate.