From ac34b4c4c61393987d99e7043fb99be401cf6313 Mon Sep 17 00:00:00 2001 From: Mathieu Hofman Date: Thu, 28 Dec 2023 20:47:31 +0000 Subject: [PATCH 1/3] WIP: ergonomic exo classes --- packages/exo/src/exo-makers.js | 2 +- packages/exo/test/test-exo-wobbly-point.js | 180 ++++++++++++++++++--- 2 files changed, 160 insertions(+), 22 deletions(-) diff --git a/packages/exo/src/exo-makers.js b/packages/exo/src/exo-makers.js index 6f51916358..629f74ed03 100644 --- a/packages/exo/src/exo-makers.js +++ b/packages/exo/src/exo-makers.js @@ -19,7 +19,7 @@ const LABEL_INSTANCES = DEBUG.split(',').includes('label-instances'); * @param {number} instanceCount * @returns {T} */ -const makeSelf = (proto, instanceCount) => { +export const makeSelf = (proto, instanceCount) => { const self = create(proto); if (LABEL_INSTANCES) { defineProperty(self, Symbol.toStringTag, { diff --git a/packages/exo/test/test-exo-wobbly-point.js b/packages/exo/test/test-exo-wobbly-point.js index 59ecc6e380..b8018283f4 100644 --- a/packages/exo/test/test-exo-wobbly-point.js +++ b/packages/exo/test/test-exo-wobbly-point.js @@ -14,32 +14,162 @@ import { test } from './prepare-test-env-ava.js'; import { getMethodNames } from '@endo/eventual-send/utils.js'; import { passStyleOf, Far, GET_METHOD_NAMES } from '@endo/pass-style'; import { M } from '@endo/patterns'; -import { defineExoClass } from '../src/exo-makers.js'; +import { defendPrototype } from '../src/exo-tools.js'; +import { makeSelf } from '../src/exo-makers.js'; import { GET_INTERFACE_GUARD } from '../src/get-interface.js'; const { Fail, quote: q } = assert; const { apply } = Reflect; +const { create, seal, freeze, defineProperty } = Object; + +/** + * @typedef {import('../src/exo-tools.js').FacetName} FacetName + * @typedef {import('../src/exo-tools.js').Methods} Methods + */ + +/** + * @template [S = any] + * @template {Methods} [M = any] + * @typedef {import('../src/exo-tools.js').ClassContext} ClassContext + */ + +/** + * @template {Methods} M + * @typedef {Farable>} Guarded + */ const ExoEmptyI = M.interface('ExoEmpty', {}); -class ExoBaseClass { - constructor() { - Fail`Turn Exo JS classes into Exo classes with defineExoClassFromJSClass: ${q( - new.target.name, - )}`; - } +const makeHardenedState = state => + harden( + create( + null, + Object.fromEntries( + Reflect.ownKeys(state).map(key => [ + key, + { + get() { + return state[key]; + }, + set(value) { + state[key] = value; + }, + }, + ]), + ), + ), + ); +const dummyStateTarget = freeze({}); + +const makeState = () => { + const state = {}; + const stateProxy = new Proxy(dummyStateTarget, { + get(target, prop, receiver) { + return Reflect.get(state, prop); + }, + set(target, prop, value, receiver) { + return Reflect.set(state, prop, value); + }, + deleteProperty(target, prop) { + return Reflect.deleteProperty(state, prop); + }, + }); + const sealer = () => { + seal(state); + return makeHardenedState(state); + }; + return { state: stateProxy, sealer }; +}; + +const pendingConstruct = new WeakMap(); + +const makeTarget = () => + function target() { + Fail`Should not call the target`; + }; +/** + * @template {new (...args: any[]) => Methods} C constructor + * @param {C} constructor + * @returns {(...args: Parameters) => Guarded>} + */ +export const defineExoClassFromJSClass = constructor => { + harden(constructor); + /** @type {WeakMap, M>>} */ + const contextMap = new WeakMap(); + const tag = constructor.name; + const proto = defendPrototype( + tag, + self => /** @type {any} */ (contextMap.get(self)), + constructor.prototype, + true, + constructor.implements, + ); + let instanceCount = 0; + + const makeContext = () => { + instanceCount += 1; + const { state, sealer } = makeState(); + const self = makeSelf(proto, instanceCount); + // It's safe to harden the state record + /** @type {ClassContext,M>} */ + const context = harden({ state, self }); + contextMap.set(self, context); + return { context, sealer }; + }; + + const makeInstance = { + /** + * @param {Parameters} args + */ + // eslint-disable-next-line object-shorthand, func-names + [tag]: function (...args) { + const target = makeTarget(); + defineProperty(target, 'prototype', { value: constructor.prototype }); + const pending = { makeContext, sealer: null }; + pendingConstruct.set(target, pending); + + try { + const context = Reflect.construct(constructor, args, target); + pending.sealer || + Fail`Exo ${q(tag)} constructor did not call ExoBaseClass`; + const state = pending.sealer(); + context || Fail`Exo ${q(tag)} constructor did not return a context`; + const { self } = context; + contextMap.get(self) === context || + Fail`Exo ${q(tag)} constructor did not return a valid context`; + const newContext = harden({ state, self }); + contextMap.set(self, newContext); + return self; + } finally { + pendingConstruct.delete(target); + } + }, + }[tag]; + defineProperty(makeInstance, 'prototype', { value: proto }); + + return harden(makeInstance); +}; +harden(defineExoClassFromJSClass); + +class ExoBaseClass { static implements = ExoEmptyI; - static init() { - return harden({}); + constructor() { + const pending = pendingConstruct.get(new.target); + pending || + Fail`Not constructed through Exo maker: ${q( + (new.target || this.constructor || { name: 'Unknown' }).name, + )}`; + !pending.sealer || Fail`Already constructed`; + + const { context, sealer } = pending.makeContext(); + pending.sealer = sealer; + // eslint-disable-next-line no-constructor-return + return context; } } -const defineExoClassFromJSClass = klass => - defineExoClass(klass.name, klass.implements, klass.init, klass.prototype); -harden(defineExoClassFromJSClass); - const ExoPointI = M.interface('ExoPoint', { toString: M.call().returns(M.string()), getX: M.call().returns(M.gte(0)), @@ -64,12 +194,10 @@ test('cannot make abstract class concrete', t => { }); class ExoPoint extends ExoAbstractPoint { - static init(x, y) { - // Heap exos currently use the returned record directly, so - // needs to not be frozen for `state.y` to be assignable. - // TODO not true for other zones. May make heap zone more like - // the others in treatment of `state`. - return { x, y }; + constructor(x, y) { + super(); + this.state.x = x; + this.state.y = y; } getX() { @@ -100,10 +228,19 @@ const assertMethodNames = (t, obj, names) => { t.deepEqual(obj[GET_METHOD_NAMES](), names); }; +test('ExoPoint constructor', t => { + t.throws(() => ExoPoint(1, 2)); + t.throws(() => new ExoPoint(1, 2)); + + const makeFoo = defineExoClassFromJSClass(class Foo {}); + t.throws(() => makeFoo()); +}); + test('ExoPoint instances', t => { const pt = makeExoPoint(3, 5); t.is(passStyleOf(pt), 'remotable'); t.false(pt instanceof ExoPoint); + t.true(pt instanceof makeExoPoint); assertMethodNames(t, pt, [ GET_INTERFACE_GUARD, GET_METHOD_NAMES, @@ -132,8 +269,9 @@ test('ExoPoint instances', t => { }); class ExoWobblyPoint extends ExoPoint { - static init(x, y, getWobble) { - return { ...super.init(x, y), getWobble }; + constructor(x, y, getWobble) { + super(x, y); + this.state.getWobble = getWobble; } getX() { From 8d1b37983db75dc07343955aaa5a97b57e290401 Mon Sep 17 00:00:00 2001 From: Mathieu Hofman Date: Sat, 14 Dec 2024 14:18:08 +0000 Subject: [PATCH 2/3] WIP: more class-like --- packages/exo/test/test-exo-wobbly-point.js | 448 ++++++++++++++------- 1 file changed, 294 insertions(+), 154 deletions(-) diff --git a/packages/exo/test/test-exo-wobbly-point.js b/packages/exo/test/test-exo-wobbly-point.js index b8018283f4..428c9f9d6d 100644 --- a/packages/exo/test/test-exo-wobbly-point.js +++ b/packages/exo/test/test-exo-wobbly-point.js @@ -1,3 +1,4 @@ +// @ts-check /** * Based on the WobblyPoint inheritance examples in * https://storage.googleapis.com/google-code-archive-downloads/v2/code.google.com/google-caja/caja-spec-2007-12-21.pdf @@ -12,56 +13,69 @@ import { test } from './prepare-test-env-ava.js'; // eslint-disable-next-line import/order import { getMethodNames } from '@endo/eventual-send/utils.js'; -import { passStyleOf, Far, GET_METHOD_NAMES } from '@endo/pass-style'; +import { + passStyleOf, + Far, + GET_METHOD_NAMES, + PASS_STYLE, +} from '@endo/pass-style'; import { M } from '@endo/patterns'; import { defendPrototype } from '../src/exo-tools.js'; import { makeSelf } from '../src/exo-makers.js'; import { GET_INTERFACE_GUARD } from '../src/get-interface.js'; const { Fail, quote: q } = assert; -const { apply } = Reflect; -const { create, seal, freeze, defineProperty } = Object; +const { apply, getPrototypeOf, setPrototypeOf, defineProperty } = Reflect; +const { + create, + seal, + freeze, + defineProperties, + getOwnPropertyDescriptors, + prototype: objectPrototype, +} = Object; /** - * @typedef {import('../src/exo-tools.js').FacetName} FacetName - * @typedef {import('../src/exo-tools.js').Methods} Methods + * @import { ContextProvider, ClassContext } from '../src/exo-tools.js'; + * @import { Guarded } from '../src/exo-makers.js'; */ -/** - * @template [S = any] - * @template {Methods} [M = any] - * @typedef {import('../src/exo-tools.js').ClassContext} ClassContext - */ +const ExoEmptyI = M.interface('ExoEmpty', {}); /** - * @template {Methods} M - * @typedef {Farable>} Guarded + * @template {Record} S + * @param {S} state + * @returns {S} */ - -const ExoEmptyI = M.interface('ExoEmpty', {}); - const makeHardenedState = state => harden( create( null, Object.fromEntries( - Reflect.ownKeys(state).map(key => [ - key, - { - get() { - return state[key]; - }, - set(value) { - state[key] = value; + Reflect.ownKeys(state).map( + /** @param {keyof S} key */ + // @ts-expect-error ownKeys has bad typing + key => [ + key, + { + get() { + return state[key]; + }, + set(value) { + state[key] = value; + }, }, - }, - ]), + ], + ), ), ), ); + +/** @type {Record} */ const dummyStateTarget = freeze({}); const makeState = () => { + /** @type {Record} */ const state = {}; const stateProxy = new Proxy(dummyStateTarget, { get(target, prop, receiver) { @@ -89,72 +103,167 @@ const makeTarget = () => }; /** - * @template {new (...args: any[]) => Methods} C constructor + * @template {[] | any[]} A + * @template {object} C + * @typedef {{(...args: A): C, new(...args: A): C}} CallableConstructor + */ + +/** + * @template {{new (...args: any[]): Record; implements?: any}} T + * @param {string} tag + * @param {ContextProvider} contextProvider + * @param {T} constructor + */ +const defendClassProto = (tag, contextProvider, constructor) => { + const protoProto = defendPrototype( + tag, + contextProvider, + create(constructor.prototype, { + // Hide inherited GET_INTERFACE_GUARD + // Not sure why defendPrototype uses it if it exists + [GET_INTERFACE_GUARD]: { value: undefined }, + // Hide inherited GET_METHOD_NAMES + // so it gets redefined + [GET_METHOD_NAMES]: { value: undefined }, + }), + true, + constructor.implements, + ); + + const proto = create( + getPrototypeOf(constructor.prototype), + getOwnPropertyDescriptors(protoProto), + ); + defineProperty(proto, Symbol.toStringTag, { + value: protoProto[Symbol.toStringTag], + }); + + // do not harden here to let the caller define constructor + return proto; +}; + +/** + * @template {new (...args: any[]) => any} C constructor * @param {C} constructor - * @returns {(...args: Parameters) => Guarded>} + * @returns {CallableConstructor, Guarded>>} */ export const defineExoClassFromJSClass = constructor => { harden(constructor); - /** @type {WeakMap, M>>} */ + /** @typedef {InstanceType} M */ + /** @typedef {ClassContext, M>} Context */ + /** @type {WeakMap} */ const contextMap = new WeakMap(); const tag = constructor.name; - const proto = defendPrototype( + const proto = defendClassProto( tag, self => /** @type {any} */ (contextMap.get(self)), - constructor.prototype, - true, - constructor.implements, + constructor, ); + let instanceCount = 0; - const makeContext = () => { + const makeContext = registerHooks => { instanceCount += 1; const { state, sealer } = makeState(); const self = makeSelf(proto, instanceCount); // It's safe to harden the state record - /** @type {ClassContext,M>} */ + /** @type {Context} */ const context = harden({ state, self }); contextMap.set(self, context); + for (const hook of registerHooks) { + hook(null, context); + } return { context, sealer }; }; - const makeInstance = { - /** - * @param {Parameters} args - */ - // eslint-disable-next-line object-shorthand, func-names - [tag]: function (...args) { - const target = makeTarget(); - defineProperty(target, 'prototype', { value: constructor.prototype }); - const pending = { makeContext, sealer: null }; - pendingConstruct.set(target, pending); - - try { - const context = Reflect.construct(constructor, args, target); - pending.sealer || - Fail`Exo ${q(tag)} constructor did not call ExoBaseClass`; - const state = pending.sealer(); - context || Fail`Exo ${q(tag)} constructor did not return a context`; - const { self } = context; - contextMap.get(self) === context || - Fail`Exo ${q(tag)} constructor did not return a valid context`; - const newContext = harden({ state, self }); - contextMap.set(self, newContext); - return self; - } finally { - pendingConstruct.delete(target); + const registerContext = (finalContext, initContext) => { + if (finalContext) { + contextMap.delete(initContext) || Fail`initContext didn't exist`; + } + const context = finalContext || initContext; + !contextMap.has(context) || Fail`context already registered`; + contextMap.set(context, context); + }; + + /** + * @param {ConstructorParameters} args + */ + const makeInstance = function (...args) { + if (new.target && new.target !== makeInstance) { + const pending = pendingConstruct.get(new.target); + + pending || Fail`Not constructing an Exo class`; + pending.registerHooks.push(registerContext); + + return Reflect.construct(constructor, args, new.target); + } + const target = makeTarget(); + defineProperty(target, 'prototype', { value: constructor.prototype }); + const pending = { + makeContext, + registerHooks: [registerContext], + sealer: /** @type {ReturnType['sealer'] | null} */ ( + null + ), + }; + pendingConstruct.set(target, pending); + + try { + /** @type {Context} */ + const context = Reflect.construct(constructor, args, target); + if (!pending.sealer) { + throw Fail`Exo ${q(tag)} constructor did not call ExoBaseClass`; } - }, - }[tag]; - defineProperty(makeInstance, 'prototype', { value: proto }); + const state = pending.sealer(); + context || Fail`Exo ${q(tag)} constructor did not return a context`; + const { self } = context; + (contextMap.get(self) === context && self !== context) || + Fail`Exo ${q(tag)} constructor did not return a valid context`; + const newContext = harden({ state, self }); + contextMap.set(self, newContext); + for (const hook of pending.registerHooks) { + hook(newContext, context); + } + return self; + } finally { + pendingConstruct.delete(target); + } + }; + setPrototypeOf(makeInstance, getPrototypeOf(constructor)); + defineProperty(makeInstance, 'prototype', { + value: proto, + configurable: false, + writable: false, + }); + const originalConstructorProps = getOwnPropertyDescriptors(constructor); + delete originalConstructorProps.prototype; + defineProperties(makeInstance, originalConstructorProps); + + // TODO: fidelity wants a constructor, but real constructor + // is a capability leak + // defineProperty(proto, 'constructor', { value: makeInstance }); + // @ts-expect-error CallableConstructor return harden(makeInstance); }; harden(defineExoClassFromJSClass); +/** + * @template {any} [M=any] + * @template {Record} [S=Record] + */ class ExoBaseClass { static implements = ExoEmptyI; + // To partially help TypeScript for now + // Has no impact on actual runtime + + /** @type {S} */ + state; + + /** @type {M} */ + self; + constructor() { const pending = pendingConstruct.get(new.target); pending || @@ -163,13 +272,22 @@ class ExoBaseClass { )}`; !pending.sealer || Fail`Already constructed`; - const { context, sealer } = pending.makeContext(); + const { context, sealer } = pending.makeContext(pending.registerHooks); pending.sealer = sealer; // eslint-disable-next-line no-constructor-return return context; } } +setPrototypeOf( + ExoBaseClass.prototype, + create(objectPrototype, { + [PASS_STYLE]: { value: 'remotable' }, + [Symbol.toStringTag]: { value: 'Remotable' }, + }), +); +harden(ExoBaseClass); + const ExoPointI = M.interface('ExoPoint', { toString: M.call().returns(M.string()), getX: M.call().returns(M.gte(0)), @@ -193,72 +311,84 @@ test('cannot make abstract class concrete', t => { }); }); -class ExoPoint extends ExoAbstractPoint { - constructor(x, y) { - super(); - this.state.x = x; - this.state.y = y; - } - - getX() { - const { - state: { x }, - } = this; - return x; - } - - getY() { - const { - state: { y }, - } = this; - return y; - } - - setY(newY) { - const { state } = this; - state.y = newY; - } -} -harden(ExoPoint); - -const makeExoPoint = defineExoClassFromJSClass(ExoPoint); +// Would look a lot nicer with a class decorator +const ExoPoint = defineExoClassFromJSClass( + class ExoPoint extends ExoAbstractPoint { + constructor(x, y) { + super(); + this.state.x = x; + this.state.y = y; + } + + getX() { + const { + state: { x }, + } = this; + return x; + } + + getY() { + const { + state: { y }, + } = this; + return y; + } + + setY(newY) { + const { state } = this; + state.y = newY; + } + }, +); const assertMethodNames = (t, obj, names) => { t.deepEqual(getMethodNames(obj), names); t.deepEqual(obj[GET_METHOD_NAMES](), names); }; -test('ExoPoint constructor', t => { - t.throws(() => ExoPoint(1, 2)); - t.throws(() => new ExoPoint(1, 2)); - - const makeFoo = defineExoClassFromJSClass(class Foo {}); - t.throws(() => makeFoo()); +test('defineExoClassFromJSClass requires ExoBaseClass', t => { + const Foo = defineExoClassFromJSClass(class Foo {}); + t.throws(() => Foo()); + t.throws(() => new Foo()); }); -test('ExoPoint instances', t => { - const pt = makeExoPoint(3, 5); +/** + * + * @param {import('ava').ExecutionContext} t + * @param {InstanceType} pt + * @param {{x: number, y:number}} values + */ +const assertPoint = (t, pt, values) => { t.is(passStyleOf(pt), 'remotable'); - t.false(pt instanceof ExoPoint); - t.true(pt instanceof makeExoPoint); + t.true(pt instanceof ExoPoint); + t.true(pt instanceof ExoAbstractPoint); + t.true(pt instanceof ExoBaseClass); assertMethodNames(t, pt, [ GET_INTERFACE_GUARD, GET_METHOD_NAMES, + 'constructor', 'getX', 'getY', 'setY', 'toString', ]); - t.is(pt.getX(), 3); - t.is(pt.getY(), 5); - t.is(`${pt}`, '<3,5>'); - pt.setY(6); - t.is(`${pt}`, '<3,6>'); + t.is(pt.getX(), values.x); + t.is(pt.getY(), values.y); + t.is(`${pt}`, `<${values.x},${values.y}>`); + pt.setY(values.y + 1); + t.is(`${pt}`, `<${values.x},${values.y + 1}>`); +}; - const otherPt = makeExoPoint(1, 2); - t.is(apply(pt.getX, otherPt, []), 1); +test('ExoPoint instances', t => { + const pt = ExoPoint(3, 5); + assertPoint(t, pt, { x: 3, y: 5 }); + + const newPt = new ExoPoint(1, 2); + assertPoint(t, newPt, { x: 1, y: 2 }); + + t.is(apply(pt.getX, newPt, []), 1); - const negPt = makeExoPoint(-3, 5); + const negPt = ExoPoint(-3, 5); t.throws(() => negPt.getX(), { message: 'In "getX" method of (ExoPoint): result: -3 - Must be >= 0', }); @@ -268,22 +398,37 @@ test('ExoPoint instances', t => { }); }); -class ExoWobblyPoint extends ExoPoint { - constructor(x, y, getWobble) { - super(x, y); - this.state.getWobble = getWobble; - } - - getX() { - const { - state: { getWobble }, - } = this; - return super.getX() + getWobble(); +class ExoAbstractWobblyPoint extends ExoPoint { + // TODO: "abstract" methods end up on the prototype chain + // as-is, non-guarded and without brand checks. + // An attacker could define an exo that extends from this + // prototype and call it through super. + toString() { + return super.toString(); } } -harden(ExoWobblyPoint); -const makeExoWobblyPoint = defineExoClassFromJSClass(ExoWobblyPoint); +const ExoWobblyPoint = defineExoClassFromJSClass( + class ExoWobblyPoint extends ExoAbstractWobblyPoint { + constructor(x, y, getWobble) { + super(x, y); + this.state.getWobble = getWobble; + } + + getX() { + const { + state: { getWobble }, + } = this; + return super.getX() + getWobble(); + } + }, +); + +test('ExoAbstractWobblyPoint constructor', t => { + // @ts-expect-error + t.throws(() => ExoAbstractWobblyPoint(1, 2)); + t.throws(() => new ExoAbstractWobblyPoint(1, 2)); +}); test('FarWobblyPoint inheritance', t => { let wobble = 0; @@ -291,38 +436,33 @@ test('FarWobblyPoint inheritance', t => { // But other zones insist on at least passability, and TODO we may eventually // make the heap zone act like this as well. const getWobble = Far('getW', () => (wobble += 1)); - const wpt = makeExoWobblyPoint(3, 5, getWobble); - t.false(wpt instanceof ExoWobblyPoint); - t.false(wpt instanceof ExoPoint); - t.is(passStyleOf(wpt), 'remotable'); - assertMethodNames(t, wpt, [ - GET_INTERFACE_GUARD, - GET_METHOD_NAMES, - 'getX', - 'getY', - 'setY', - 'toString', - ]); - t.is(`${wpt}`, '<4,5>'); - t.is(`${wpt}`, '<5,5>'); - t.is(`${wpt}`, '<6,5>'); - wpt.setY(6); - t.is(`${wpt}`, '<7,6>'); - - const otherPt = makeExoPoint(1, 2); - t.false(otherPt instanceof ExoWobblyPoint); - t.throws(() => apply(wpt.getX, otherPt, []), { + const wpt = ExoWobblyPoint(3, 5, getWobble); + assertPoint(t, wpt, { + get x() { + return 3 + wobble; + }, + y: 5, + }); + t.true(wpt instanceof ExoWobblyPoint); + t.true(wpt instanceof ExoAbstractWobblyPoint); + + const newWpt = new ExoWobblyPoint(3, 5, () => 1); + assertPoint(t, newWpt, { x: 4, y: 5 }); + t.true(newWpt instanceof ExoWobblyPoint); + t.true(wpt instanceof ExoAbstractWobblyPoint); + + const pt = ExoPoint(1, 2); + t.false(pt instanceof ExoWobblyPoint); + t.throws(() => apply(wpt.getX, pt, []), { message: '"In \\"getX\\" method of (ExoWobblyPoint)" may only be applied to a valid instance: "[Alleged: ExoPoint]"', }); - t.throws(() => apply(wpt.getY, otherPt, []), { + t.throws(() => apply(wpt.getY, pt, []), { message: '"In \\"getY\\" method of (ExoWobblyPoint)" may only be applied to a valid instance: "[Alleged: ExoPoint]"', }); - const otherWpt = makeExoWobblyPoint(3, 5, () => 1); - t.is(`${otherWpt}`, '<4,5>'); - t.is(apply(wpt.getX, otherWpt, []), 4); + t.is(apply(wpt.getX, newWpt, []), 4); // This error behavior shows the absence of the security vulnerability // explained at the corresponding example in `test-far-wobbly-point.js` @@ -331,17 +471,17 @@ test('FarWobblyPoint inheritance', t => { // method denies that method to clients of instances of the subclass. // At the same time, this overridden method remains available within // the overriding subclass via unguarded `super` calls. - t.throws(() => apply(otherPt.getX, otherWpt, []), { + t.throws(() => apply(pt.getX, newWpt, []), { message: '"In \\"getX\\" method of (ExoPoint)" may only be applied to a valid instance: "[Alleged: ExoWobblyPoint]"', }); - const negWpt1 = makeExoWobblyPoint(-3, 5, () => 4); - t.is(negWpt1.getX(), 1); - // `super` calls are direct, bypassing guards and sharing context - t.is(`${negWpt1}`, '<1,5>'); + const negWpt1 = ExoWobblyPoint(-3, 5, () => 4); + // t.is(negWpt1.getX(), 1); + // TODO?: `super` calls are direct, bypassing guards and sharing context + // t.is(`${negWpt1}`, '<1,5>'); - const negWpt2 = makeExoWobblyPoint(1, 5, () => -4); + const negWpt2 = ExoWobblyPoint(1, 5, () => -4); t.throws(() => `${negWpt2}`, { // `self` calls are guarded message: 'In "getX" method of (ExoWobblyPoint): result: -3 - Must be >= 0', From 800d20d139880dee1286ac9a7291d2333635afd7 Mon Sep 17 00:00:00 2001 From: Mathieu Hofman Date: Thu, 2 Jan 2025 20:26:28 +0000 Subject: [PATCH 3/3] WIP: remove unneeded target prototype change --- packages/exo/test/test-exo-wobbly-point.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/exo/test/test-exo-wobbly-point.js b/packages/exo/test/test-exo-wobbly-point.js index 428c9f9d6d..d2a5a75b8f 100644 --- a/packages/exo/test/test-exo-wobbly-point.js +++ b/packages/exo/test/test-exo-wobbly-point.js @@ -198,7 +198,6 @@ export const defineExoClassFromJSClass = constructor => { return Reflect.construct(constructor, args, new.target); } const target = makeTarget(); - defineProperty(target, 'prototype', { value: constructor.prototype }); const pending = { makeContext, registerHooks: [registerContext],