X-Git-Url: https://git.ladys.computer/Pisces/blobdiff_plain/29307967cb084aceaa9265fe22af4348c9e4376a..e1cb83c479df2a3e4a5e918867a135ff9dde8121:/function.js?ds=sidebyside
diff --git a/function.js b/function.js
index 46cab86..5f9c2b5 100644
--- a/function.js
+++ b/function.js
@@ -1,76 +1,453 @@
-// ♓🌟 Piscēs ∷ function.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 .
+// 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";
+
+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 an
- * array.
+ * ☡ 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.
*
- * ※ This is effectively an alias for Function.prototype.call.bind.
+ * 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´.
*/
- makeCallable,
+ createArrowFunction,
+
+ /**
+ * 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
+ * be preserved in the new one. A second argument may be used to
+ * override `length´ and `name´.
+ *
+ * ※ This is effectively an alias for `Function::call.bind´.
+ */
+ createCallableFunction,
+
+ /**
+ * Returns a constructor which throws whenever it is called but has
+ * the same `.length´, `.name´, and `.prototype´ as the provided
+ * value.
+ *
+ * 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´.
+ */
+ 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,
} = (() => {
- // ☡ Because these functions are used to initialize module constants,
- // they can’t depend on imports from elsewhere.
+ const { prototype: functionPrototype } = Function;
const {
bind: functionBind,
call: functionCall,
- } = Function.prototype;
- const callBind = Reflect.apply(functionBind, functionCall, [
+ } = functionPrototype;
+ const objectConstructor = Object;
+ const proxyConstructor = Proxy;
+ const {
+ apply: reflectApply,
+ construct: reflectConstruct,
+ } = Reflect;
+ const callBind = reflectApply(functionBind, functionCall, [
functionBind,
]);
- const {
- create: objectCreate,
- defineProperty: defineOwnProperty,
- getPrototypeOf: getPrototype,
- } = Object;
- const { iterator: iteratorSymbol } = Symbol;
- const { [iteratorSymbol]: arrayIterator } = Array.prototype;
+ const { revocable } = Proxy;
+ const { [ITERATOR]: arrayIterator } = Array.prototype;
const {
next: arrayIteratorNext,
- } = getPrototype([][iteratorSymbol]());
+ } = getPrototype([][ITERATOR]());
const argumentIterablePrototype = {
- [iteratorSymbol]() {
+ [ITERATOR]() {
return {
+ [ITERATOR]() {
+ return this;
+ },
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) {
+ // No base function was provided to apply.
+ return $;
+ } else {
+ // 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 */
+ }
+ 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) {
+ // No properties were provided to apply.
+ return $;
+ } else {
+ // 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 has a writable `.prototype´ and a
+ // prototype value was provided.
+ //
+ // Change the prototype property of the provided function to
+ // match.
+ defineOwnProperty(
+ $,
+ "prototype",
+ defineOwnDataProperty(
+ objectCreate(null),
+ "value",
+ override.prototype,
+ ),
+ );
+ }
+ return defineOwnProperties($, {
+ length: defineOwnDataProperty(
+ objectCreate(null),
+ "value",
+ toLength(length === UNDEFINED ? $.length : length),
+ ),
+ name: defineOwnDataProperty(
+ objectCreate(null),
+ "value",
+ toFunctionName(name === UNDEFINED ? $.name ?? "" : name),
+ ),
+ });
+ }
+ };
+
return {
bind: ($, boundThis, boundArgs) =>
callBind(
$,
boundThis,
- ...objectCreate(
- argumentIterablePrototype,
- { args: { value: boundArgs } },
+ ...defineOwnDataProperty(
+ objectCreate(argumentIterablePrototype),
+ "args",
+ boundArgs,
),
),
- makeCallable: ($) =>
+ createArrowFunction: ($, propertyOverride = UNDEFINED) =>
+ applyProperties(
+ applyBaseFunction(
+ (...$s) => reflectApply($, UNDEFINED, $s),
+ $,
+ ),
+ propertyOverride,
+ ),
+ createCallableFunction: ($, propertyOverride = UNDEFINED) =>
+ applyProperties(
+ applyBaseFunction(
+ (...$s) => {
+ const iterator = defineOwnDataProperty(
+ objectCreate(argumentIterablePrototype),
+ "args",
+ $s,
+ )[ITERATOR]();
+ const { value: thisValue } = iterator.next();
+ return reflectApply($, thisValue, [...iterator]);
+ },
+ $,
+ 1,
+ ),
+ propertyOverride,
+ ),
+ createIllegalConstructor: ($, propertyOverride = UNDEFINED) =>
defineOwnProperty(
- callBind(functionCall, $),
- "length",
- { value: $.length + 1 },
+ applyProperties(
+ applyBaseFunction(
+ function () {
+ throw new TypeError("Illegal constructor");
+ },
+ $,
+ ),
+ propertyOverride,
+ ),
+ "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,
+ }),
+ });
+ }
+ },
};
})();
@@ -78,26 +455,58 @@ export const {
* Calls the provided function with the provided this value and
* arguments list.
*
- * ☡ This is an alias for Reflect.apply—the arguments must be passed
- * as an array.
+ * ☡ This is effectively an alias for `Reflect.apply´—the arguments
+ * must be passed as an arraylike.
*/
-export const call = Reflect.apply;
+export const call = createArrowFunction(Reflect.apply, {
+ name: "call",
+});
+
+/**
+ * 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
+ * callable.
+ */
+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: ${$}.`,
+ );
+ } else {
+ // The provided value is callable.
+ try {
+ // This will throw if calling the function throws.
+ //
+ // Otherwise, return true.
+ $();
+ return true;
+ } catch {
+ // Calling the function did not succeed.
+ //
+ // Return false.
+ return false;
+ }
+ }
+};
/**
* Constructs the provided function with the provided arguments list
* and new target.
*
- * ☡ This is an alias for Reflect.construct—the arguments must be
- * passed as an array.
+ * ☡ This is effectively an alias for `Reflect.construct´—the
+ * arguments must be passed as an arraylike.
*/
-export const construct = Reflect.construct;
+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 ($) {
@@ -108,24 +517,27 @@ export const identity = function ($) {
export const isCallable = ($) => typeof $ === "function";
/** Returns whether the provided value is a constructor. */
-export const isConstructor = ($) => {
- // The provided value is an object.
- try {
- construct(
- function () {},
- [],
- $,
- ); // will throw if $ is not a constructor
- return true;
- } catch {
- return false;
- }
-};
+export const isConstructor = ($) =>
+ completesNormally(() =>
+ // 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.
*/
-export const ordinaryHasInstance = makeCallable(
+export const ordinaryHasInstance = createCallableFunction(
Function.prototype[Symbol.hasInstance],
+ { name: "ordinaryHasInstance" },
);