X-Git-Url: https://git.ladys.computer/Pisces/blobdiff_plain/32e1e0ba06627f2ed6c3f99c415ef6b9c0f7de4d..6e6d4e3261c1c943fe44fa9e381bcf8bf1441fd6:/object.js?ds=sidebyside diff --git a/object.js b/object.js index 8f5c490..9986291 100644 --- a/object.js +++ b/object.js @@ -7,6 +7,77 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at . +import { bind, call } from "./function.js"; +import { 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. * @@ -18,7 +89,7 @@ * * 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 @@ -28,48 +99,47 @@ export const PropertyDescriptor = (() => { * its `enumerable` property, if defined, will always be a * 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.", + `Piscēs: Cannot convert primitive to property descriptor: ${O}.`, ); } else { // The provided value is an object. - const desc = Object.create(propertyDescriptorPrototype); - if ("enumerable" in Obj) { + const desc = objectCreate(propertyDescriptorPrototype); + 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 { @@ -80,10 +150,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 { @@ -165,20 +235,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 || @@ -187,7 +255,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() { @@ -197,7 +265,7 @@ export const PropertyDescriptor = (() => { } } - const coercePropretyDescriptorValue = (P, V) => { + const coercePropertyDescriptorValue = (P, V) => { switch (P) { case "configurable": case "enumerable": @@ -206,7 +274,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.", ); @@ -214,7 +282,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.", ); @@ -226,7 +294,9 @@ export const PropertyDescriptor = (() => { } }; - const propertyDescriptorPrototype = PropertyDescriptor.prototype; + const { + prototype: propertyDescriptorPrototype, + } = PropertyDescriptor; const propertyDescriptorProxyHandler = Object.assign( Object.create(null), @@ -244,9 +314,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. @@ -264,28 +334,44 @@ export const PropertyDescriptor = (() => { ); } else { // P can be safely defined on O. - return Reflect.defineProperty(O, P, desc); + return defineOwnProperty(O, P, desc); } } else { // P is not a property descriptor attribute. - return Reflect.defineProperty(O, P, Desc); + return defineOwnProperty(O, P, Desc); } }, 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 Reflect.set(O, prop, newValue, Receiver); + return setPropertyValue(O, P, V, Receiver); } }, setPrototypeOf(O, V) { @@ -294,194 +380,434 @@ export const PropertyDescriptor = (() => { return false; } else { // V is the property descriptor prototype. - return Reflect.setPrototypeOf(O, V); + return setPrototype(O, V); } }, }, ); - 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 Object.preventExtensions( - Object.create( - species == null || !("prototype" in species) - ? null - : species.prototype, - Object.fromEntries( - function* () { - for (const P of Reflect.ownKeys(O)) { - const Desc = Object.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 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; - } - } -}; +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 whether the provided value is an object. */ -export const isObject = ($) => { - return $ !== null && - (typeof $ == "function" || typeof $ == "object"); -}; + /** + * 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 whether the provided object inherits from the prototype of - * the provided function. - */ -export const ordinaryHasInstance = Function.prototype.call.bind( - Function.prototype[Symbol.hasInstance], -); + /** + * 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. + * + * ☡ 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 { + 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: 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?.[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 */ + } + } + }, + }, + }, + ), + ), + ), + ); + } + }, + }; +})(); /** - * Returns the primitive value of the provided object per its - * `toString` and `valueOf` methods. + * Returns the function on the provided value at the provided property + * key. * - * 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. + * ☡ This function throws if the provided property key does not have an + * associated value which is callable. */ -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 = method.call(O); - if (!isObject(result)) { - // Method returns a primitive. - return result; - } else { - // Method returns an object. - continue; - } - } else { - // Method is not callable. - continue; - } +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; } - throw new TypeError("Piscēs: Unable to convert object to primitive"); }; /** - * Returns the provided value converted to a primitive, or throws if - * no such conversion is possible. + * Returns the provided value converted to an object. * - * 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. + * Existing objects are returned with no modification. + * + * ☡ This function throws if its argument is null or undefined. */ -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 { toObject } = (() => { + const makeObject = Object; + return { + toObject: ($) => { + if ($ == null) { throw new TypeError( - "Piscēs: Symbol.toPrimitive was neither nullish nor callable.", + `Piscēs: Cannot convert ${$} into an object.`, ); } 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 exoticToPrim.call($, hint); - } + return makeObject($); } - } else { - // Use the ordinary primitive conversion function. - ordinaryToPrimitive($, hint); - } - } else { - // The provided value is already a primitive. - return $; - } -}; + }, + }; +})(); /** * Returns the property key (symbol or string) corresponding to the