From: Lady Date: Sun, 5 Nov 2023 20:41:30 +0000 (-0500) Subject: Redo create⸺Function A·P·I in function.js X-Git-Url: https://git.ladys.computer/Pisces/commitdiff_plain/37ab7f76d34b76493528f20e7427ef9534a8edf7?hp=00a99e780024dae2f15b5def2064f9d29d738e9f Redo create⸺Function A·P·I in function.js `createCallableFunction` and `createIllegalConstructor` are now joined by a `createArrowFunction`, and all three have extremely similar interfaces. Further refactoring is probably necessary elsewhere in the codebase to make use of these changes. --- diff --git a/function.js b/function.js index 7da0a3a..d13be72 100644 --- a/function.js +++ b/function.js @@ -7,7 +7,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at . -import { ITERATOR, toPrimitive } from "./value.js"; +import { ITERATOR, toLength, toPrimitive } from "./value.js"; export const { /** @@ -24,6 +24,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,9 +47,9 @@ 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, @@ -55,6 +70,7 @@ export const { ]); const { create: objectCreate, + defineProperty: defineOwnProperty, defineProperties: defineOwnProperties, getOwnPropertyDescriptor, getPrototypeOf: getPrototype, @@ -67,6 +83,9 @@ export const { const argumentIterablePrototype = { [ITERATOR]() { return { + [ITERATOR]() { + return this; + }, next: callBind( arrayIteratorNext, call(arrayIterator, this.args, []), @@ -78,6 +97,55 @@ export const { Symbol.prototype, "description", ).get; + 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; + setPrototype($, getPrototype(base)); + 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 (!isConstructor($) || prototype === undefined) { + // The provided function is not a constructor 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", { value: prototype }); + } + return defineOwnProperties($, { + length: { + value: toLength(length === undefined ? $.length : length), + }, + name: { + value: toFunctionName( + name === undefined ? $.name ?? "" : name, + ), + }, + }); + } + }; return { bind: ($, boundThis, boundArgs) => @@ -89,38 +157,46 @@ export const { { args: { value: boundArgs } }, ), ), - createCallableFunction: ($, name = undefined) => - defineOwnProperties(callBind(functionCall, $), { - length: { value: $.length + 1 }, - name: { - value: toFunctionName( - name === undefined ? $.name ?? "" : name, - ), - }, - }), - 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 */ - } 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, - ); - } - return defineOwnProperties(constructor, { - name: { value: $?.name ?? "" }, - prototype: { value: $?.prototype ?? {}, writable: false }, + createArrowFunction: ($, propertyOverride = undefined) => + applyProperties( + applyBaseFunction( + (...$s) => + $(...objectCreate( + argumentIterablePrototype, + { args: { value: $s } }, + )), + $, + ), + propertyOverride, + ), + createCallableFunction: ($, propertyOverride = undefined) => + applyProperties( + applyBaseFunction( + (...$s) => { + const iterator = objectCreate( + argumentIterablePrototype, + { args: { value: $s } }, + )[ITERATOR](); + const { value: thisValue } = iterator.next(); + return call($, thisValue, [...iterator]); + }, + $, + 1, + ), + propertyOverride, + ), + createIllegalConstructor: ($, propertyOverride = undefined) => { + const constructor = applyProperties( + applyBaseFunction( + function () { + throw new TypeError("Illegal constructor"); + }, + $, + ), + propertyOverride, + ); + return defineOwnProperty(constructor, "prototype", { + writable: false, }); }, toFunctionName: ($, prefix = undefined) => { @@ -228,5 +304,5 @@ export const isCallable = ($) => typeof $ === "function"; */ export const ordinaryHasInstance = createCallableFunction( Function.prototype[Symbol.hasInstance], - "ordinaryHasInstance", + { name: "ordinaryHasInstance" }, ); diff --git a/function.test.js b/function.test.js index dd9b642..9be4db6 100644 --- a/function.test.js +++ b/function.test.js @@ -20,6 +20,7 @@ import { call, completesNormally, construct, + createArrowFunction, createCallableFunction, createIllegalConstructor, identity, @@ -265,6 +266,98 @@ describe("construct", () => { }); }); +describe("createArrowFunction", () => { + it("[[Call]] sets this to undefined", () => { + assertStrictEquals( + createArrowFunction( + function () { + return this; + }, + ).call("fail"), + undefined, + ); + }); + + it("[[Call]] transfers the provided arguments", () => { + assertEquals( + createArrowFunction( + function (...args) { + return [this, ...args]; + }, + ).call("failure", "etaoin", "shrdlu", "cmfwyp"), + [undefined, "etaoin", "shrdlu", "cmfwyp"], + ); + }); + + it("[[Call]] correctly sets the length", () => { + assertStrictEquals( + createArrowFunction( + function (_a, _b, _c) {}, + ).length, + 3, + ); + }); + + it("[[Call]] correctly sets the name", () => { + assertStrictEquals( + createArrowFunction( + function etaoin() {}, + ).name, + "etaoin", + ); + }); + + it("[[Call]] allows the length to be overridden", () => { + assertStrictEquals( + createArrowFunction( + function etaoin() {}, + { length: 100 }, + ).length, + 100, + ); + }); + + it("[[Call]] allows the name to be overridden", () => { + assertStrictEquals( + createArrowFunction( + function etaoin() {}, + { name: "shrdlu" }, + ).name, + "shrdlu", + ); + }); + + it("[[Call]] returns an arrow function", () => { + assertThrows(() => new (createArrowFunction(function () {}))()); + }); + + it("[[Call]] returns a function with no prototype property", () => { + assert( + !("prototype" in + createArrowFunction(function () {}, { prototype: {} })), + ); + }); + + it("[[Construct]] throws an error", () => { + assertThrows(() => new createArrowFunction(function () {})); + }); + + describe(".length", () => { + it("[[Get]] returns the correct length", () => { + assertStrictEquals(createArrowFunction.length, 1); + }); + }); + + describe(".name", () => { + it("[[Get]] returns the correct name", () => { + assertStrictEquals( + createArrowFunction.name, + "createArrowFunction", + ); + }); + }); +}); + describe("createCallableFunction", () => { it("[[Call]] transfers the first argument to this", () => { assertStrictEquals( @@ -306,16 +399,37 @@ describe("createCallableFunction", () => { ); }); + it("[[Call]] allows the length to be overridden", () => { + assertStrictEquals( + createCallableFunction( + function etaoin() {}, + { length: 100 }, + ).length, + 100, + ); + }); + it("[[Call]] allows the name to be overridden", () => { assertStrictEquals( createCallableFunction( function etaoin() {}, - "shrdlu", + { name: "shrdlu" }, ).name, "shrdlu", ); }); + it("[[Call]] returns an arrow function", () => { + assertThrows(() => new (createCallableFunction(function () {}))()); + }); + + it("[[Call]] returns a function with no prototype property", () => { + assert( + !("prototype" in + createCallableFunction(function () {}, { prototype: {} })), + ); + }); + it("[[Construct]] throws an error", () => { assertThrows(() => new createCallableFunction(function () {})); }); @@ -351,56 +465,39 @@ describe("createIllegalConstructor", () => { Object.getPrototypeOf(constructor.prototype), Object.prototype, ); - assertEquals(constructor.prototype, {}); - assert( - !Object.getOwnPropertyDescriptor(constructor, "prototype") - .writable, - ); - assertStrictEquals(constructor.name, ""); - }); - - it("[[Call]] returns a correctly‐formed constructor when provided one argument", () => { - const constructorPrototype = Object.create(null, { - name: { value: "etaoin" }, - prototype: { value: {} }, - }); - const constructor = createIllegalConstructor( - Object.create(constructorPrototype), + console.dir( + Object.getOwnPropertyDescriptors(constructor.prototype), ); - assert(isConstructor(constructor)); - assertStrictEquals( - Object.getPrototypeOf(constructor), - constructorPrototype, - ); - assert(Object.hasOwn(constructor, "prototype")); assertEquals( constructor.prototype, - constructorPrototype.prototype, + Object.create(Object.prototype, { + constructor: { + configurable: true, + enumerable: false, + value: constructor, + writable: true, + }, + }), ); assert( !Object.getOwnPropertyDescriptor(constructor, "prototype") .writable, ); - assertStrictEquals(constructor.name, "etaoin"); + assertStrictEquals(constructor.name, ""); }); - it("[[Call]] allows the second argument to override the prototype of the constructor", () => { + it("[[Call]] returns a correctly‐formed constructor when provided one argument", () => { const constructorPrototype = Object.create(null, { name: { value: "etaoin" }, prototype: { value: {} }, }); - const expectedPrototype = Object.create(null, { - name: { value: "shrdlu" }, - prototype: { value: {} }, - }); const constructor = createIllegalConstructor( Object.create(constructorPrototype), - expectedPrototype, ); assert(isConstructor(constructor)); assertStrictEquals( Object.getPrototypeOf(constructor), - expectedPrototype, + constructorPrototype, ); assert(Object.hasOwn(constructor, "prototype")); assertEquals( diff --git a/symbol.js b/symbol.js index a96cb81..e1183ef 100644 --- a/symbol.js +++ b/symbol.js @@ -20,7 +20,7 @@ import { getOwnPropertyDescriptor } from "./object.js"; */ export const getSymbolDescription = createCallableFunction( getOwnPropertyDescriptor(Symbol.prototype, "description").get, - "getSymbolDescription", + { name: "getSymbolDescription" }, ); /** @@ -35,7 +35,7 @@ export const getSymbolDescription = createCallableFunction( */ export const symbolToString = createCallableFunction( Symbol.prototype.toString, - "symbolToString", + { name: "symbolToString" }, ); /** @@ -48,5 +48,5 @@ export const symbolToString = createCallableFunction( */ export const symbolValue = createCallableFunction( Symbol.prototype.valueOf, - "symbolValue", + { name: "symbolValue" }, );