diff --git a/packages/exo/package.json b/packages/exo/package.json index c842a839b0..0fb816f3f5 100644 --- a/packages/exo/package.json +++ b/packages/exo/package.json @@ -33,6 +33,7 @@ }, "dependencies": { "@endo/env-options": "^0.1.4", + "@endo/eventual-send": "^0.17.6", "@endo/far": "^0.2.22", "@endo/pass-style": "^0.1.7", "@endo/patterns": "^0.2.6" diff --git a/packages/exo/src/exo-tools.js b/packages/exo/src/exo-tools.js index e17d7ba2e7..a516b1a487 100644 --- a/packages/exo/src/exo-tools.js +++ b/packages/exo/src/exo-tools.js @@ -1,5 +1,5 @@ +import { getMethodNames } from '@endo/eventual-send/src/local.js'; import { E, Far } from '@endo/far'; -import { hasOwnPropertyOf } from '@endo/pass-style'; import { listDifference, objectMap, @@ -253,21 +253,6 @@ const bindMethod = ( */ export const GET_INTERFACE_GUARD = Symbol.for('getInterfaceGuard'); -/** - * - * @template {Record} T - * @param {T} behaviorMethods - * @param {InterfaceGuard<{ [M in keyof T]: MethodGuard }>} interfaceGuard - * @returns {T} - */ -const withGetInterfaceGuardMethod = (behaviorMethods, interfaceGuard) => - harden({ - [GET_INTERFACE_GUARD]() { - return interfaceGuard; - }, - ...behaviorMethods, - }); - /** * @template {Record} T * @param {string} tag @@ -285,13 +270,20 @@ export const defendPrototype = ( interfaceGuard = undefined, ) => { const prototype = {}; - if (hasOwnPropertyOf(behaviorMethods, 'constructor')) { - // By ignoring any method named "constructor", we can use a + const methodNames = getMethodNames(behaviorMethods).filter( + // By ignoring any method that seems to be a constructor, we can use a // class.prototype as a behaviorMethods. - const { constructor: _, ...methods } = behaviorMethods; - // @ts-expect-error TS misses that hasOwn check makes this safe - behaviorMethods = harden(methods); - } + key => { + if (key !== 'constructor') { + return true; + } + const constructor = behaviorMethods.constructor; + return !( + constructor.prototype && + constructor.prototype.constructor === constructor + ); + }, + ); /** @type {Record | undefined} */ let methodGuards; if (interfaceGuard) { @@ -307,7 +299,6 @@ export const defendPrototype = ( fromEntries(getCopyMapEntries(symbolMethodGuards))), }); { - const methodNames = ownKeys(behaviorMethods); const methodGuardNames = ownKeys(methodGuards); const unimplemented = listDifference(methodGuardNames, methodNames); unimplemented.length === 0 || @@ -318,13 +309,9 @@ export const defendPrototype = ( Fail`methods ${q(unguarded)} not guarded by ${q(interfaceName)}`; } } - behaviorMethods = withGetInterfaceGuardMethod( - behaviorMethods, - interfaceGuard, - ); } - for (const prop of ownKeys(behaviorMethods)) { + for (const prop of methodNames) { prototype[prop] = bindMethod( `In ${q(prop)} method of (${tag})`, contextProvider, @@ -335,6 +322,21 @@ export const defendPrototype = ( ); } + if (interfaceGuard) { + const getInterfaceGuardMethod = { + [GET_INTERFACE_GUARD]() { + return interfaceGuard; + }, + }[GET_INTERFACE_GUARD]; + prototype[GET_INTERFACE_GUARD] = bindMethod( + `In ${q(GET_INTERFACE_GUARD)} method of (${tag})`, + contextProvider, + getInterfaceGuardMethod, + thisfulMethods, + undefined, + ); + } + return Far(tag, /** @type {T} */ (prototype)); }; harden(defendPrototype); diff --git a/packages/exo/test/test-exo-class-js-class.js b/packages/exo/test/test-exo-class-js-class.js new file mode 100644 index 0000000000..697ece1922 --- /dev/null +++ b/packages/exo/test/test-exo-class-js-class.js @@ -0,0 +1,79 @@ +/* eslint-disable max-classes-per-file */ +/* eslint-disable class-methods-use-this */ +// eslint-disable-next-line import/order +import { test } from './prepare-test-env-ava.js'; + +import { passStyleOf } from '@endo/pass-style'; +// import { M, getInterfaceGuardPayload } from '@endo/patterns'; +// import { defineExoClass, makeExo } from '../src/exo-makers.js'; +import { M, getInterfaceGuardPayload } from '@endo/patterns'; +import { makeExo, defineExoClass } from '../src/exo-makers.js'; + +// Based on FarSubclass1 in test-far-class-instances.js +class DoublerBehaviorClass { + double(x) { + return x + x; + } +} + +const DoublerI = M.interface('Doubler', { + double: M.call(M.lte(10)).returns(M.number()), +}); + +const doubler = makeExo('doubler', DoublerI, DoublerBehaviorClass.prototype); + +test('exo doubler using js classes', t => { + t.is(passStyleOf(doubler), 'remotable'); + t.is(doubler.double(3), 6); + t.throws(() => doubler.double('x'), { + message: 'In "double" method of (doubler): arg 0: "x" - Must be <= 10', + }); + t.throws(() => doubler.double(), { + message: + 'In "double" method of (doubler): Expected at least 1 arguments: []', + }); + t.throws(() => doubler.double(12), { + message: 'In "double" method of (doubler): arg 0: 12 - Must be <= 10', + }); +}); + +// Based on FarSubclass2 in test-far-class-instances.js +class DoubleAdderBehaviorClass extends DoublerBehaviorClass { + doubleAddSelfCall(x) { + const { + state: { y }, + self, + } = this; + return self.double(x) + y; + } + + doubleAddSuperCall(x) { + const { + state: { y }, + } = this; + return super.double(x) + y; + } +} + +const DoubleAdderI = M.interface('DoubleAdder', { + ...getInterfaceGuardPayload(DoublerI).methodGuards, + doubleAddSelfCall: M.call(M.number()).returns(M.number()), + doubleAddSuperCall: M.call(M.number()).returns(M.number()), +}); + +const makeDoubleAdder = defineExoClass( + 'doubleAdderClass', + DoubleAdderI, + y => ({ y }), + DoubleAdderBehaviorClass.prototype, +); + +test('exo inheritance self vs super call', t => { + const da = makeDoubleAdder(5); + t.is(da.doubleAddSelfCall(3), 11); + t.throws(() => da.doubleAddSelfCall(12), { + message: + 'In "double" method of (doubleAdderClass): arg 0: 12 - Must be <= 10', + }); + t.is(da.doubleAddSuperCall(12), 29); +}); diff --git a/packages/exo/test/test-exo-wobbly-point.js b/packages/exo/test/test-exo-wobbly-point.js new file mode 100644 index 0000000000..8773141806 --- /dev/null +++ b/packages/exo/test/test-exo-wobbly-point.js @@ -0,0 +1,197 @@ +/** + * 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 + * and + * test-far-wobbly-point.js + */ + +/* eslint-disable class-methods-use-this */ +/* eslint-disable max-classes-per-file */ +/* eslint-disable-next-line import/order */ +import { test } from './prepare-test-env-ava.js'; + +// TODO enable import of getMethodNames without deep import +// eslint-disable-next-line import/order +import { getMethodNames } from '@endo/eventual-send/src/local.js'; +import { passStyleOf, Far } from '@endo/pass-style'; +import { M } from '@endo/patterns'; +import { defineExoClass } from '../src/exo-makers.js'; +import { GET_INTERFACE_GUARD } from '../src/exo-tools.js'; + +const { Fail, quote: q } = assert; +const { apply } = Reflect; + +const ExoEmptyI = M.interface('ExoEmpty', {}); + +class ExoBaseClass { + constructor() { + Fail`Turn Exo JS classes into Exo classes with defineExoClassFromJSClass: ${q( + new.target.name, + )}`; + } + + static implements = ExoEmptyI; + + static init() { + return harden({}); + } +} + +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)), + getY: M.call().returns(M.number()), + setY: M.call(M.number()).returns(), +}); + +class ExoAbstractPoint extends ExoBaseClass { + static implements = ExoPointI; + + toString() { + const { self } = this; + return `<${self.getX()},${self.getY()}>`; + } +} + +test('cannot make abstract class concrete', t => { + t.throws(() => defineExoClassFromJSClass(ExoAbstractPoint), { + message: + 'methods ["getX","getY","setY"] not implemented by "ExoAbstractPoint"', + }); +}); + +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 }; + } + + 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); + +test('ExoPoint instances', t => { + const pt = makeExoPoint(3, 5); + t.is(passStyleOf(pt), 'remotable'); + t.false(pt instanceof ExoPoint); + t.deepEqual(getMethodNames(pt), [ + GET_INTERFACE_GUARD, + '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>'); + + const otherPt = makeExoPoint(1, 2); + t.is(apply(pt.getX, otherPt, []), 1); + + const negPt = makeExoPoint(-3, 5); + t.throws(() => negPt.getX(), { + message: 'In "getX" method of (ExoPoint): result: -3 - Must be >= 0', + }); + // `self` calls are guarded + t.throws(() => `${negPt}`, { + message: 'In "getX" method of (ExoPoint): result: -3 - Must be >= 0', + }); +}); + +class ExoWobblyPoint extends ExoPoint { + static init(x, y, getWobble) { + return { ...super.init(x, y), getWobble }; + } + + getX() { + const { + state: { getWobble }, + } = this; + return super.getX() + getWobble(); + } +} +harden(ExoWobblyPoint); + +const makeExoWobblyPoint = defineExoClassFromJSClass(ExoWobblyPoint); + +test('FarWobblyPoint inheritance', t => { + let wobble = 0; + // For heap classes currently, there is no reason to make `getWobble` passable. + // 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'); + t.deepEqual(getMethodNames(wpt), [ + GET_INTERFACE_GUARD, + '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, []), { + message: + '"In \\"getX\\" method of (ExoWobblyPoint)" may only be applied to a valid instance: "[Alleged: ExoPoint]"', + }); + t.throws(() => apply(wpt.getY, otherPt, []), { + 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.throws(() => apply(otherPt.getX, otherWpt, []), { + 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 negWpt2 = makeExoWobblyPoint(1, 5, () => -4); + t.throws(() => `${negWpt2}`, { + // `self` calls are guarded + message: 'In "getX" method of (ExoWobblyPoint): result: -3 - Must be >= 0', + }); +}); diff --git a/packages/exo/test/test-non-enumerable-methods.js b/packages/exo/test/test-non-enumerable-methods.js new file mode 100644 index 0000000000..46a489562f --- /dev/null +++ b/packages/exo/test/test-non-enumerable-methods.js @@ -0,0 +1,130 @@ +// eslint-disable-next-line import/order +import { test } from './prepare-test-env-ava.js'; + +// eslint-disable-next-line import/order +import { getInterfaceMethodKeys, M, fromUniqueEntries } from '@endo/patterns'; +import { defineExoClass } from '../src/exo-makers.js'; +import { GET_INTERFACE_GUARD } from '../src/exo-tools.js'; + +const { getPrototypeOf, getOwnPropertyDescriptors, create } = Object; + +const { ownKeys } = Reflect; + +/** + * Borrowed from https://github.com/endojs/endo/pull/1815 to avoid + * depending on it being merged. TODO If it is merged, then delete this + * copy and import `objectMetaMap` instead. + * + * Like `objectMap`, but at the reflective level of property descriptors + * rather than property values. + * + * Except for hardening, the edge case behavior is mostly the opposite of + * the `objectMap` edge cases. + * * No matter how mutable the original object, the returned object is + * hardened. + * * All own properties of the original are mapped, even if symbol-named + * or non-enumerable. + * * If any of the original properties were accessors, the descriptor + * containing the getter and setter are given to `metaMapFn`. + * * The own properties of the returned are according to the descriptors + * returned by `metaMapFn`. + * * The returned object will always be a plain object whose state is + * only these mapped own properties. It will inherit from the third + * argument if provided, defaulting to `Object.prototype` if omitted. + * + * Because a property descriptor is distinct from `undefined`, we bundle + * mapping and filtering together. When the `metaMapFn` returns `undefined`, + * that property is omitted from the result. + * + * @template {Record} O + * @param {O} original + * @param {( + * desc: TypedPropertyDescriptor, + * key: keyof O + * ) => (PropertyDescriptor | undefined)} metaMapFn + * @param {any} [proto] + * @returns {any} + */ +export const objectMetaMap = ( + original, + metaMapFn, + proto = Object.prototype, +) => { + const descs = getOwnPropertyDescriptors(original); + const keys = ownKeys(original); + + const descEntries = /** @type {[PropertyKey,PropertyDescriptor][]} */ ( + keys + .map(key => [key, metaMapFn(descs[key], key)]) + .filter(([_key, optDesc]) => optDesc !== undefined) + ); + return harden(create(proto, fromUniqueEntries(descEntries))); +}; +harden(objectMetaMap); + + + +const UpCounterI = M.interface('UpCounter', { + incr: M.call() + // TODO M.number() should not be needed to get a better error message + .optional(M.and(M.number(), M.gte(0))) + .returns(M.number()), +}); + +const denumerate = obj => + objectMetaMap( + obj, + desc => ({ ...desc, enumerable: false }), + getPrototypeOf(obj), + ); + +test('test defineExoClass', t => { + const makeUpCounter = defineExoClass( + 'UpCounter', + UpCounterI, + /** @param {number} x */ + (x = 0) => ({ x }), + denumerate({ + incr(y = 1) { + const { state } = this; + state.x += y; + return state.x; + }, + }), + ); + const upCounter = makeUpCounter(3); + t.is(upCounter.incr(5), 8); + t.is(upCounter.incr(1), 9); + t.throws(() => upCounter.incr(-3), { + message: 'In "incr" method of (UpCounter): arg 0?: -3 - Must be >= 0', + }); + // @ts-expect-error bad arg + t.throws(() => upCounter.incr('foo'), { + message: + 'In "incr" method of (UpCounter): arg 0?: string "foo" - Must be a number', + }); + t.deepEqual(upCounter[GET_INTERFACE_GUARD](), UpCounterI); + t.deepEqual(getInterfaceMethodKeys(UpCounterI), ['incr']); + + const symbolic = Symbol.for('symbolic'); + const FooI = M.interface('Foo', { + m: M.call().returns(), + [symbolic]: M.call(M.boolean()).returns(), + }); + t.deepEqual(getInterfaceMethodKeys(FooI), ['m', Symbol.for('symbolic')]); + const makeFoo = defineExoClass( + 'Foo', + FooI, + () => ({}), + denumerate({ + m() {}, + [symbolic]() {}, + }), + ); + const foo = makeFoo(); + t.deepEqual(foo[GET_INTERFACE_GUARD](), FooI); + t.throws(() => foo[symbolic]('invalid arg'), { + message: + 'In "[Symbol(symbolic)]" method of (Foo): arg 0: string "invalid arg" - Must be a boolean', + }); +}); diff --git a/packages/pass-style/package.json b/packages/pass-style/package.json index 8759556307..a0d923481a 100644 --- a/packages/pass-style/package.json +++ b/packages/pass-style/package.json @@ -37,6 +37,7 @@ "@fast-check/ava": "^1.1.5" }, "devDependencies": { + "@endo/eventual-send": "^0.17.6", "@endo/init": "^0.5.60", "@endo/ses-ava": "^0.2.44", "ava": "^5.3.0", diff --git a/packages/pass-style/test/test-far-class-instances.js b/packages/pass-style/test/test-far-class-instances.js new file mode 100644 index 0000000000..7b98427ce4 --- /dev/null +++ b/packages/pass-style/test/test-far-class-instances.js @@ -0,0 +1,98 @@ +/* eslint-disable class-methods-use-this */ +/* eslint-disable max-classes-per-file */ +import { test } from './prepare-test-env-ava.js'; + +// TODO enable import of getMethodNames without deep import +// eslint-disable-next-line import/order +import { getMethodNames } from '@endo/eventual-send/src/local.js'; +import { passStyleOf } from '../src/passStyleOf.js'; +import { Far } from '../src/make-far.js'; + +/** + * Classes whose instances should be Far objects may find it convenient to + * inherit from this base class. Note that the constructor of this base class + * freezes the instance in an empty state, so all is interesting attributes + * can only depend on its identity and what it inherits from. + * This includes private fields, as those are keyed only on + * this object's identity. However, we discourage (but cannot prevent) such + * use of private fields, as they cannot easily be refactored into Exo state. + */ +export const FarBaseClass = class FarBaseClass { + constructor() { + harden(this); + } +}; + +Far('FarBaseClass', FarBaseClass.prototype); +harden(FarBaseClass); + +class FarSubclass1 extends FarBaseClass { + double(x) { + return x + x; + } +} + +class FarSubclass2 extends FarSubclass1 { + #y = 0; + + constructor(y) { + super(); + this.#y = y; + } + + doubleAdd(x) { + return this.double(x) + this.#y; + } +} + +test('far class instances', t => { + const fb = new FarBaseClass(); + t.is(passStyleOf(fb), 'remotable'); + t.deepEqual(getMethodNames(fb), ['constructor']); + + t.assert(new fb.constructor() instanceof FarBaseClass); + t.throws(() => fb.constructor(), { + // TODO message depends on JS engine, and so is a fragile golden test + message: "Class constructor FarBaseClass cannot be invoked without 'new'", + }); + + const fs1 = new FarSubclass1(); + t.is(passStyleOf(fs1), 'remotable'); + t.is(fs1.double(4), 8); + t.assert(new fs1.constructor() instanceof FarSubclass1); + t.deepEqual(getMethodNames(fs1), ['constructor', 'double']); + + const fs2 = new FarSubclass2(3); + t.is(passStyleOf(fs2), 'remotable'); + t.is(fs2.double(4), 8); + t.is(fs2.doubleAdd(4), 11); + t.deepEqual(getMethodNames(fs2), ['constructor', 'double', 'doubleAdd']); + + const yField = new WeakMap(); + class FarSubclass3 extends FarSubclass1 { + constructor(y) { + super(); + yField.set(this, y); + } + + doubleAdd(x) { + return this.double(x) + yField.get(this); + } + } + + const fs3 = new FarSubclass3(3); + t.is(passStyleOf(fs3), 'remotable'); + t.is(fs3.double(4), 8); + t.is(fs3.doubleAdd(4), 11); + t.deepEqual(getMethodNames(fs3), ['constructor', 'double', 'doubleAdd']); +}); + +test('far class instance hardened empty', t => { + class FarClass4 extends FarBaseClass { + z = 0; + } + t.throws(() => new FarClass4(), { + // TODO message depends on JS engine, and so is a fragile golden test + message: 'Cannot define property z, object is not extensible', + }); +}); diff --git a/packages/pass-style/test/test-far-wobbly-point.js b/packages/pass-style/test/test-far-wobbly-point.js new file mode 100644 index 0000000000..8f67103a03 --- /dev/null +++ b/packages/pass-style/test/test-far-wobbly-point.js @@ -0,0 +1,132 @@ +/** + * 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 + */ + +/* eslint-disable class-methods-use-this */ +/* eslint-disable max-classes-per-file */ +import { test } from './prepare-test-env-ava.js'; + +// TODO enable import of getMethodNames without deep import +// eslint-disable-next-line import/order +import { getMethodNames } from '@endo/eventual-send/src/local.js'; +import { passStyleOf } from '../src/passStyleOf.js'; +import { Far } from '../src/make-far.js'; + +const { apply } = Reflect; + +/** + * Classes whose instances should be Far objects may find it convenient to + * inherit from this base class. Note that the constructor of this base class + * freezes the instance in an empty state, so all is interesting attributes + * can only depend on its identity and what it inherits from. + * This includes private fields, as those are keyed only on + * this object's identity. However, we discourage (but cannot prevent) such + * use of private fields, as they cannot easily be refactored into Exo state. + */ +class FarBaseClass { + constructor() { + harden(this); + } +} +Far('FarBaseClass', FarBaseClass.prototype); +harden(FarBaseClass); + +class FarPoint extends FarBaseClass { + #x; + + #y; + + constructor(x, y) { + super(); + this.#x = x; + this.#y = y; + } + + toString() { + return `<${this.getX()},${this.getY()}>`; + } + + getX() { + return this.#x; + } + + getY() { + return this.#y; + } + + setY(newY) { + this.#y = newY; + } +} +harden(FarPoint); + +test('FarPoint instances', t => { + const pt = new FarPoint(3, 5); + t.is(passStyleOf(pt), 'remotable'); + t.assert(pt instanceof FarPoint); + t.deepEqual(getMethodNames(pt), [ + '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>'); + + const otherPt = new FarPoint(1, 2); + t.is(apply(pt.getX, otherPt, []), 1); +}); + +class FarWobblyPoint extends FarPoint { + #getWobble; + + constructor(x, y, getWobble) { + super(x, y); + this.#getWobble = getWobble; + } + + getX() { + return super.getX() + this.#getWobble(); + } +} +harden(FarWobblyPoint); + +test('FarWobblyPoint inheritance', t => { + let wobble = 0; + const getWobble = () => (wobble += 1); + const wpt = new FarWobblyPoint(3, 5, getWobble); + t.assert(wpt instanceof FarWobblyPoint); + t.assert(wpt instanceof FarPoint); + t.is(passStyleOf(wpt), 'remotable'); + t.deepEqual(getMethodNames(wpt), [ + 'constructor', + '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 = new FarPoint(1, 2); + t.false(otherPt instanceof FarWobblyPoint); + t.throws(() => apply(wpt.getX, otherPt, []), { + // TODO great error message, but is a golden specific to v8 + message: + 'Cannot read private member #getWobble from an object whose class did not declare it', + }); + t.is(apply(wpt.getY, otherPt, []), 2); + + const otherWpt = new FarWobblyPoint(3, 5, () => 1); + t.is(`${otherWpt}`, '<4,5>'); + t.is(apply(wpt.getX, otherWpt, []), 4); + t.is(apply(otherPt.getX, otherWpt, []), 3); +});