X-Git-Url: https://git.ladys.computer/Pisces/blobdiff_plain/aa1ae4c089cec40d79c3087ed7d6007dade77815..2e6fac40384b2e17dd3a6b8700abc787b0b57479:/function.js?ds=sidebyside diff --git a/function.js b/function.js index d02fe6e..5f9c2b5 100644 --- a/function.js +++ b/function.js @@ -1,75 +1,122 @@ -// ♓🌟 Piscēs ∷ function.js -// ==================================================================== -// -// 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 . +// SPDX-FileCopyrightText: 2022, 2023, 2025 Lady +// SPDX-License-Identifier: MPL-2.0 +/** + * ⁌ ♓🧩 Piscēs ∷ function.js + * + * Copyright © 2022–2023, 2025 Lady [@ Ladys 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 . + */ + +import { + ITERATOR, + toFunctionName, + toLength, + type, + UNDEFINED, +} from "./value.js"; +import { + defineOwnDataProperty, + defineOwnProperties, + defineOwnProperty, + getOwnPropertyDescriptor, + getPrototype, + hasOwnProperty, + objectCreate, + setPropertyValues, + setPrototype, +} from "./object.js"; -import { ITERATOR, toFunctionName, toLength } from "./value.js"; +const PISCĒS = "♓🧩 Piscēs"; export const { /** * Creates a bound function from the provided function using the * provided this value and arguments list. * - * ☡ As with `call` and `construct`, the arguments must be passed as + * ☡ As with `call´ and `construct´, the arguments must be passed as * an array. */ bind, /** - * Returns a new function which calls the provided function with its - * first argument as the `this` value and the remaining arguments - * passed through. + * Returns a new arrow function function which wraps the provided + * function and passes its arguments thru. * - * The `length`, `name`, and prototype of the provided function will + * The `length´, `name´, and prototype of the provided function will * be preserved in the new one. A second argument may be used to - * override `length` and `name`. + * override `length´ and `name´. */ createArrowFunction, /** - * Returns a new function which calls the provided function with its - * first argument as the `this` value and the remaining arguments - * passed through. + * Returns a new arrow function which wraps the provided function, + * using its first argument as the this value when calling the + * provided function and passing the remainder thru. * - * The `length`, `name`, and prototype of the provided function will + * The `length´, `name´, and prototype of the provided function will * be preserved in the new one. A second argument may be used to - * override `length` and `name`. + * override `length´ and `name´. * - * ※ This is effectively an alias for `Function::call.bind`. + * ※ This is effectively an alias for `Function::call.bind´. */ createCallableFunction, /** * Returns a constructor which throws whenever it is called but has - * the same `.name` and `.prototype` as the provided value. + * the same `.length´, `.name´, and `.prototype´ as the provided + * value. * - * The `length`, `name`, `prototype`, and prototype of the provided + * The `length´, `name´, `prototype´, and prototype of the provided * function will be preserved in the new one. A second argument may - * be used to override `length`, `name`, and `prototype`. + * be used to override `length´, `name´, and `prototype´. */ createIllegalConstructor, + + /** + * Returns a constructor which produces a new constructor that wraps + * the provided constructor, but returns a proxy of the result using + * the provided handler. + * + * The resulting constructor inherits from, and has the same basic + * shape as, `Proxy´. + * + * If a base constructor is not provided, `Object´ will be used. + * + * If a third argument is provided, it is used as the target for the + * provided constructor when it is constructed. This can be used to + * prevent leakage of the provided constructor to superclasses + * through `new.target´. + * + * The `length´ of the provided function will be preserved in the new + * one. A fourth argument may be used to override `length´ and + * `name´. + * + * ※ `.prototype´ will be present, but undefined, on the resulting + * constructor. This differs from the behaviour of `Proxy´, for which + * `.prototype´ is not present at all. It is not presently possible + * to create a constructor with no `.prototype´ property in + * Ecmascript code. + */ + createProxyConstructor, } = (() => { const { prototype: functionPrototype } = Function; const { bind: functionBind, call: functionCall, } = functionPrototype; - const { apply: reflectApply } = Reflect; + const objectConstructor = Object; + const proxyConstructor = Proxy; const { - create: objectCreate, - defineProperty: defineOwnProperty, - defineProperties: defineOwnProperties, - hasOwn: hasOwnProperty, - getPrototypeOf: getPrototype, - setPrototypeOf: setPrototype, - } = Object; + apply: reflectApply, + construct: reflectConstruct, + } = Reflect; const callBind = reflectApply(functionBind, functionCall, [ functionBind, ]); + const { revocable } = Proxy; const { [ITERATOR]: arrayIterator } = Array.prototype; const { next: arrayIteratorNext, @@ -82,61 +129,127 @@ export const { }, next: callBind( arrayIteratorNext, - call(arrayIterator, this.args, []), + reflectApply(arrayIterator, this.args, []), ), }; }, }; + const { get: wmGet, set: wmSet } = WeakMap.prototype; + const wsConstructor = WeakSet; + const { add: wsAdd, has: wsHas } = WeakSet.prototype; + const proxyConstructorValuesMap = new WeakMap(); + const registerConstructedProxy = (constructor, proxy) => { + const values = (() => { + const existing = reflectApply(wmGet, proxyConstructorValuesMap, [ + constructor, + ]); + if (existing) { + // There is an existing values set for this constructor. + // + // It is returned. + return existing; + } else { + // There is no existing values set for this constructor so one + // must be created. + const result = new wsConstructor(); + reflectApply(wmSet, proxyConstructorValuesMap, [ + constructor, + result, + ]); + return result; + } + })(); + reflectApply(wsAdd, values, [proxy]); + return proxy; + }; const applyBaseFunction = ($, base, lengthDelta = 0) => { - if (base === undefined) { + if (base === UNDEFINED) { // No base function was provided to apply. return $; } else { - // A base function was provided; apply it. - const { length, name, prototype } = base; + // A base function was provided. + // + // Apply it. + const overrides = objectCreate(null); + overrides.length = +base.length + lengthDelta; + overrides.name = base.name; if (getPrototype($) === functionPrototype) { + // The provided function has the function prototype. + // + // Change it to match the base function. setPrototype($, getPrototype(base)); } else { + // The provided function already does not have the function + // prototype, so its prototype is not changed. /* do nothing */ } - return applyProperties($, { - length: +length + lengthDelta, - name, - prototype, - }); + if (hasOwnProperty($, "prototype") && "prototype" in base) { + // The provided function has a `.prototype´ property, and one + // was provided in the base function as well. + try { + // If this does not throw, the provided function is a + // constructor. Its `.prototype´ property is set alongside + // the others. + reflectConstruct(function () {}, [], $); + overrides.prototype = base.prototype; + } catch { + // The provided function is not a constructor. + /* do nothing */ + } + } else { + // The provided function does not have a `.prototype´ property, + // or the base function did not have one. + /* do nothing */ + } + return applyProperties($, overrides); } }; const applyProperties = ($, override) => { - if (override === undefined) { + if (override === UNDEFINED) { // No properties were provided to apply. return $; } else { - // Properties were provided; apply them. - const { length, name, prototype } = override; - if (!hasOwnProperty($, "prototype") || prototype === undefined) { - // The provided function has no `.prototype` or no prototype - // value was provided. + // Properties were provided. + // + // Apply them. + const { length, name } = override; + if ( + !("prototype" in override) + || !getOwnPropertyDescriptor($, "prototype")?.writable + ) { + // The provided function has no `.prototype´, its prototype is + // not writable, or no prototype value was provided. // // Do not modify the prototype property of the provided // function. /* do nothing */ } else { - // The provided function is a constructor and a prototype value - // was provided. + // The provided function has a writable `.prototype´ and a + // prototype value was provided. // // Change the prototype property of the provided function to // match. - defineOwnProperty($, "prototype", { value: prototype }); + defineOwnProperty( + $, + "prototype", + defineOwnDataProperty( + objectCreate(null), + "value", + override.prototype, + ), + ); } return defineOwnProperties($, { - length: { - value: toLength(length === undefined ? $.length : length), - }, - name: { - value: toFunctionName( - name === undefined ? $.name ?? "" : name, - ), - }, + length: defineOwnDataProperty( + objectCreate(null), + "value", + toLength(length === UNDEFINED ? $.length : length), + ), + name: defineOwnDataProperty( + objectCreate(null), + "value", + toFunctionName(name === UNDEFINED ? $.name ?? "" : name), + ), }); } }; @@ -146,26 +259,28 @@ export const { callBind( $, boundThis, - ...objectCreate( - argumentIterablePrototype, - { args: { value: boundArgs } }, + ...defineOwnDataProperty( + objectCreate(argumentIterablePrototype), + "args", + boundArgs, ), ), - createArrowFunction: ($, propertyOverride = undefined) => + createArrowFunction: ($, propertyOverride = UNDEFINED) => applyProperties( applyBaseFunction( - (...$s) => reflectApply($, undefined, $s), + (...$s) => reflectApply($, UNDEFINED, $s), $, ), propertyOverride, ), - createCallableFunction: ($, propertyOverride = undefined) => + createCallableFunction: ($, propertyOverride = UNDEFINED) => applyProperties( applyBaseFunction( (...$s) => { - const iterator = objectCreate( - argumentIterablePrototype, - { args: { value: $s } }, + const iterator = defineOwnDataProperty( + objectCreate(argumentIterablePrototype), + "args", + $s, )[ITERATOR](); const { value: thisValue } = iterator.next(); return reflectApply($, thisValue, [...iterator]); @@ -175,7 +290,7 @@ export const { ), propertyOverride, ), - createIllegalConstructor: ($, propertyOverride = undefined) => + createIllegalConstructor: ($, propertyOverride = UNDEFINED) => defineOwnProperty( applyProperties( applyBaseFunction( @@ -189,6 +304,150 @@ export const { "prototype", { writable: false }, ), + createProxyConstructor: ( + handler, + $, + newTarget = UNDEFINED, + propertyOverride = UNDEFINED, + ) => { + const constructor = $ === UNDEFINED + ? function ($) { + return new objectConstructor($); + } + : $; + const target = newTarget === UNDEFINED ? constructor : newTarget; + const len = toLength(constructor.length); + if (!(type(handler) === "object")) { + // The provided handler is not an object; this is an error. + throw new TypeError( + `${PISCĒS}: Proxy handler must be an object, but got: ${handler}.`, + ); + } else if (!isConstructor(constructor)) { + // The provided constructor is not a constructor; this is an + // error. + throw new TypeError( + `${PISCĒS}: Cannot create proxy constructor from nonconstructible value.`, + ); + } else if (!isConstructor(target)) { + // The provided new target is not a constructor; this is an + // error. + throw new TypeError( + `${PISCĒS}: New target must be a constructor.`, + ); + } else { + // The arguments are acceptable. + const C = applyProperties( + defineOwnProperties( + setPrototype( + function (...$s) { + if (new.target === UNDEFINED) { + // The constructor was not called with new; this is + // an error. + throw new TypeError( + `${PISCĒS}: ${ + C.name ?? "Proxy" + } must be called with new.`, + ); + } else { + // The constructor was called with new. + // + // Return the appropriate proxy. + const O = reflectConstruct( + constructor, + $s, + target, + ); + const proxy = new proxyConstructor(O, handler); + return registerConstructedProxy(C, proxy); + } + }, + proxyConstructor, + ), + { + length: defineOwnDataProperty( + objectCreate(null), + "value", + len, + ), + name: defineOwnDataProperty( + objectCreate(null), + "value", + `${toFunctionName(constructor.name ?? "")}Proxy`, + ), + prototype: setPropertyValues(objectCreate(null), { + configurable: false, + enumerable: false, + value: UNDEFINED, + writable: false, + }), + }, + ), + propertyOverride, + ); + const { name } = C; + return defineOwnProperties(C, { + revocable: setPropertyValues(objectCreate(null), { + configurable: true, + enumerable: false, + value: defineOwnProperties( + (...$s) => { + const O = reflectConstruct( + constructor, + $s, + target, + ); + const proxy = revocable(O, handler); + return registerConstructedProxy(C, proxy); + }, + { + length: defineOwnDataProperty( + objectCreate(null), + "value", + len, + ), + name: defineOwnDataProperty( + objectCreate(null), + "value", + "revocable", + ), + }, + ), + writable: true, + }), + [`is${name}`]: setPropertyValues(objectCreate(null), { + configurable: true, + enumerable: false, + value: defineOwnProperty( + ($) => { + const values = reflectApply( + wmGet, + proxyConstructorValuesMap, + [C], + ); + if (values === UNDEFINED) { + // No values have been registered for the current + // constructor. + return false; + } else { + // One or more values has been registered for the + // current constructor. + // + // Return whether the provided argument is one. + return reflectApply(wsHas, values, [$]); + } + }, + "name", + defineOwnDataProperty( + objectCreate(null), + "value", + `is${name}`, + ), + ), + writable: true, + }), + }); + } + }, }; })(); @@ -196,7 +455,7 @@ export const { * Calls the provided function with the provided this value and * arguments list. * - * ☡ This is effectively an alias for `Reflect.apply`—the arguments + * ☡ This is effectively an alias for `Reflect.apply´—the arguments * must be passed as an arraylike. */ export const call = createArrowFunction(Reflect.apply, { @@ -204,7 +463,7 @@ export const call = createArrowFunction(Reflect.apply, { }); /** - * Returns whether calling the provided function with no `this` value + * Returns whether calling the provided function with no this value * or arguments completes normally; that is, does not throw an error. * * ☡ This function will throw an error if the provided argument is not @@ -214,16 +473,20 @@ export const completesNormally = ($) => { if (!isCallable($)) { // The provided value is not callable; this is an error. throw new TypeError( - `Piscēs: Cannot determine completion of noncallable value: ${$}`, + `${PISCĒS}: Cannot determine completion of noncallable value: ${$}.`, ); } else { // The provided value is callable. try { - // Attempt to call the function and return true if this succeeds. + // This will throw if calling the function throws. + // + // Otherwise, return true. $(); return true; } catch { - // Calling the function did not succeed; return false. + // Calling the function did not succeed. + // + // Return false. return false; } } @@ -233,7 +496,7 @@ export const completesNormally = ($) => { * Constructs the provided function with the provided arguments list * and new target. * - * ☡ This is effectively an alias for `Reflect.construct`—the + * ☡ This is effectively an alias for `Reflect.construct´—the * arguments must be passed as an arraylike. */ export const construct = createArrowFunction(Reflect.construct); @@ -242,8 +505,8 @@ export const construct = createArrowFunction(Reflect.construct); * Returns the provided value. * * ※ This function can be called as a constructor. When used in an - * `extends` clause and called via `super`, it will set the value of - * `this` to the provided value, enabling it to be extended with + * `extends´ clause and called via `super´, it will set the value of + * `this´ to the provided value, enabling it to be extended with * private class features. */ export const identity = function ($) { @@ -256,12 +519,20 @@ export const isCallable = ($) => typeof $ === "function"; /** Returns whether the provided value is a constructor. */ export const isConstructor = ($) => completesNormally(() => - // Try constructing a new object with the provided value as its - // `new.target`. This will throw if the provided value is not a - // constructor. + // Try constructing a new object using the provided value as + // `new.target´. + // + // ☡ This will throw if the provided value is not a constructor. construct(function () {}, [], $) ); +/** + * Calls the provided callback with the provided argument if the + * provided argument is not nullish; otherwise, returns the provided + * argument unmodified. + */ +export const maybe = ($, callback) => $ == null ? $ : callback($); + /** * Returns whether the provided object inherits from the prototype of * the provided function.