X-Git-Url: https://git.ladys.computer/Pisces/blobdiff_plain/393f4d07436fec754c9863fddf98b8503afdb2c9..refs/heads/current:/function.js?ds=sidebyside diff --git a/function.js b/function.js index efb3871..2f242fe 100644 --- a/function.js +++ b/function.js @@ -7,7 +7,23 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at . -import { ITERATOR } from "./value.js"; +import { + ITERATOR, + toFunctionName, + toLength, + type, + UNDEFINED, +} from "./value.js"; +import { + defineOwnDataProperty, + defineOwnProperties, + defineOwnProperty, + getOwnPropertyDescriptor, + getPrototype, + objectCreate, + setPropertyValues, + setPrototype, +} from "./object.js"; export const { /** @@ -24,6 +40,21 @@ export const { * first argument as the `this` value and the remaining arguments * passed through. * + * 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`. + */ + createArrowFunction, + + /** + * Returns a new function which calls the provided function with its + * first argument as the `this` value and the remaining arguments + * passed through. + * + * 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, @@ -32,34 +63,54 @@ export const { * Returns a constructor which throws whenever it is called but has * the same `.name` and `.prototype` as the provided value. * - * If a second argument is provided, the returned constructor will - * use that as its prototype; otherwise, it will use the prototype of - * 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 string function name generated from the provided value - * and optional prefix. + * Returns a constructor which produces a new constructor which 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. */ - toFunctionName, + 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, - defineProperties: defineOwnProperties, - getOwnPropertyDescriptor, - getPrototypeOf: getPrototype, - setPrototypeOf: setPrototype, - } = Object; + const { revocable } = Proxy; const { [ITERATOR]: arrayIterator } = Array.prototype; const { next: arrayIteratorNext, @@ -67,6 +118,9 @@ export const { const argumentIterablePrototype = { [ITERATOR]() { return { + [ITERATOR]() { + return this; + }, next: callBind( arrayIteratorNext, call(arrayIterator, this.args, []), @@ -74,111 +128,301 @@ export const { }; }, }; - const getSymbolDescription = getOwnPropertyDescriptor( - Symbol.prototype, - "description", - ).get; + 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) { + return existing; + } else { + 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 { length, name, prototype } = base; + if (getPrototype($) === functionPrototype) { + setPrototype($, getPrototype(base)); + } else { + /* do nothing */ + } + return applyProperties($, { + length: +length + lengthDelta, + name, + prototype, + }); + } + }; + const applyProperties = ($, override) => { + if (override === UNDEFINED) { + // No properties were provided to apply. + return $; + } else { + // Properties were provided; apply them. + const { length, name, prototype } = override; + if ( + prototype === UNDEFINED || + !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. + // + // Change the prototype property of the provided function to + // match. + defineOwnProperty( + $, + "prototype", + defineOwnDataProperty( + objectCreate(null), + "value", + 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, ), ), - createCallableFunction: ($, name = undefined) => - defineOwnProperties(callBind(functionCall, $), { - length: { value: $.length + 1 }, - name: { - value: toFunctionName( - name === undefined ? $.name ?? "" : name, + 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( + applyProperties( + applyBaseFunction( + function () { + throw new TypeError("Illegal constructor"); + }, + $, ), - }, - }), - createIllegalConstructor: ($, proto = undefined) => { - const constructor = function () { - throw new TypeError("Illegal constructor"); - }; - if ($ == null && proto === undefined) { - // The provided argument is nullish and no explicit prototype - // was provided. - // - // Do not modify the prototype of the generated constructor. - /* do nothing */ + 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 provided argument is not nullish or an explicit - // prototype was provided. - // - // Set the prototype of the generated constructor to match. - setPrototype( - constructor, - proto === undefined ? getPrototype($) : proto, + // 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, + }), + }); } - return defineOwnProperties(constructor, { - name: { value: $?.name ?? "" }, - prototype: { value: $?.prototype ?? {}, writable: false }, - }); - }, - toFunctionName: ($, prefix = undefined) => { - const name = (() => { - if (typeof $ === "symbol") { - // The provided value is a symbol; format its description. - const description = call(getSymbolDescription, $, []); - return description === undefined ? "" : `[${description}]`; - } else { - // The provided value not a symbol; convert it to a string - // property key. - return `${$}`; - } - })(); - return prefix !== undefined ? `${prefix} ${name}` : name; }, }; })(); -export const { - /** - * Calls the provided function with the provided this value and - * arguments list. - * - * ☡ This is effectively an alias for `Reflect.apply`—the arguments - * must be passed as an arraylike. - */ - call, - - /** - * Constructs the provided function with the provided arguments list - * and new target. - * - * ☡ This is effectively an alias for `Reflect.construct`—the - * arguments must be passed as an arraylike. - */ - construct, - - /** Returns whether the provided value is a constructor. */ - isConstructor, -} = (() => { - const { apply, construct } = Reflect; - return { - call: (target, thisArgument, argumentsList) => - apply(target, thisArgument, argumentsList), - construct: (target, argumentsList, ...args) => - args.length > 0 - ? construct(target, argumentsList, args[0]) - : construct(target, argumentsList), - 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. - construct(function () {}, [], $) - ), - }; -})(); +/** + * Calls the provided function with the provided this value and + * arguments list. + * + * ☡ This is effectively an alias for `Reflect.apply`—the arguments + * must be passed as an arraylike. + */ +export const call = createArrowFunction(Reflect.apply, { + name: "call", +}); /** * Returns whether calling the provided function with no `this` value @@ -206,6 +450,15 @@ export const completesNormally = ($) => { } }; +/** + * Constructs the provided function with the provided arguments list + * and new target. + * + * ☡ This is effectively an alias for `Reflect.construct`—the + * arguments must be passed as an arraylike. + */ +export const construct = createArrowFunction(Reflect.construct); + /** * Returns the provided value. * @@ -221,11 +474,27 @@ export const identity = function ($) { /** Returns whether the provided value is callable. */ 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. + 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 = createCallableFunction( Function.prototype[Symbol.hasInstance], - "ordinaryHasInstance", + { name: "ordinaryHasInstance" }, );