From a4f6fe073e514f10ced082f4be6cd3e53532fec0 Mon Sep 17 00:00:00 2001 From: "Mark S. Miller" Date: Mon, 12 Sep 2022 13:16:17 -0700 Subject: [PATCH] fix: review suggestions --- packages/captp/src/captp.js | 12 +- packages/marshal/src/encodeToSmallcaps.js | 376 +++++++++--------- packages/marshal/src/marshal-stringify.js | 6 +- packages/marshal/src/marshal.js | 30 +- packages/marshal/test/test-marshal-far-obj.js | 16 +- packages/marshal/test/test-marshal-justin.js | 2 + .../marshal/test/test-marshal-smallcaps.js | 130 +++++- 7 files changed, 337 insertions(+), 235 deletions(-) diff --git a/packages/captp/src/captp.js b/packages/captp/src/captp.js index 0ed5983154..4ae8abd598 100644 --- a/packages/captp/src/captp.js +++ b/packages/captp/src/captp.js @@ -121,6 +121,9 @@ export const makeCapTP = ( // TODO Temporary hack. // See https://github.com/Agoric/agoric-sdk/issues/2780 errorIdNum: 20000, + // captp not yet compat with smallcaps + // TODO: fix captp and remove this flag. + useSmallcaps: false, }, ); @@ -611,10 +614,11 @@ export const makeCapTP = ( assert.fail(X`Trap(${target}) target cannot be a promise`); const slot = valToSlot.get(target); - (slot && slot[1] === '-') || + if (!(slot && slot[1] === '-')) { + // TypeScript confused about `||` control flow so use `if` instead + // https://github.com/microsoft/TypeScript/issues/50739 assert.fail(X`Trap(${target}) target was not imported`); - // @ts-expect-error TS apparently confused about `||` control flow - // https://github.com/microsoft/TypeScript/issues/50739 + } slot[0] === 't' || assert.fail( X`Trap(${target}) imported target was not created with makeTrapHandler`, @@ -651,8 +655,6 @@ export const makeCapTP = ( // messages over the current CapTP data channel. const [isException, serialized] = trapGuest({ trapMethod: implMethod, - // @ts-expect-error TS apparently confused about `||` control flow - // https://github.com/microsoft/TypeScript/issues/50739 slot, trapArgs: implArgs, startTrap: () => { diff --git a/packages/marshal/src/encodeToSmallcaps.js b/packages/marshal/src/encodeToSmallcaps.js index c74a2e81e9..065ab483d4 100644 --- a/packages/marshal/src/encodeToSmallcaps.js +++ b/packages/marshal/src/encodeToSmallcaps.js @@ -12,11 +12,7 @@ import { passStyleOf } from './passStyleOf.js'; import { ErrorHelper } from './helpers/error.js'; import { makeTagged } from './makeTagged.js'; -import { - isObject, - getTag, - hasOwnPropertyOf, -} from './helpers/passStyle-helpers.js'; +import { getTag, hasOwnPropertyOf } from './helpers/passStyle-helpers.js'; import { assertPassableSymbol, nameForPassableSymbol, @@ -32,37 +28,48 @@ import { const { ownKeys } = Reflect; const { isArray } = Array; -const { - getOwnPropertyDescriptors, - defineProperties, - is, - fromEntries, - freeze, -} = Object; +const { is, fromEntries } = Object; const { details: X, quote: q } = assert; /** - * Special property name that indicates an encoding that needs special - * decoding. - */ -export const SCLASS = '@qclass'; - -/** - * `'` - escaped string - * `+` - non-negative bigint - * `-` - negative bigint - * `#` - constant - * `@` - symbol - * `$` - remotable - * `?` - promise + * Smallcaps considers the characters between `!` (ascii code 33) + * and `-` (ascii code 45) to be special. These characters, in order, are + * `!"#$%&'()*+,-` Of these, smallcaps currently uses the following: + * + * * `'` - escaped string + * * `+` - non-negative bigint + * * `-` - negative bigint + * * `#` - manifest constant + * * `%` - symbol + * * `$` - remotable + * * `&` - promise + * + * All other special characters (`!"()*,`) are reserved for future use. + * + * The menifest constants that smallcaps currently uses for values: + * * `#undefined` + * * `#NaN` + * * `#Infinity` + * * `#-Infinity` + * + * and for property names: + * * `#tag` + * * `#error` + * + * All other encoded strings beginning with `#` are reserved for + * future use. + * + * @param {string} encodedStr + * @returns {boolean} */ -const SpecialChars = `'+-#@$?`; - -/** - * @param {SmallcapsEncoding} encoded - * @returns {encoded is SmallcapsEncodingUnion} - */ -const hasSClass = encoded => hasOwnPropertyOf(encoded, SCLASS); +const startsSpecial = encodedStr => { + if (encodedStr === '') { + return false; + } + const c = encodedStr[0]; + // eslint-disable-next-line yoda + return '!' <= c && c <= '-'; +}; /** * @typedef {object} EncodeToSmallcapsOptions @@ -125,23 +132,33 @@ export const makeEncodeToSmallcaps = ({ */ const encodeToSmallcapsRecur = passable => { // First we handle all primitives. Some can be represented directly as - // JSON, and some must be encoded as [SCLASS] composites. + // JSON, and some must be encoded into smallcaps strings. const passStyle = passStyleOf(passable); switch (passStyle) { case 'null': case 'boolean': { + // pass through to JSON return passable; } case 'string': { - if (passable !== '' && SpecialChars.includes(passable[0])) { + if (startsSpecial(passable)) { + // Strings that start with a special char are quoted with `'`. + // Since `'` is itself a special character, this trivially does + // the Hilbert hotel. Also, since the special characters are + // a continuous subrange of ascii, this quoting is sort-order + // preserving. return `'${passable}`; } + // All other strings pass through to JSON return passable; } case 'symbol': { + // At the smallcaps level, we prefix symbol names with `%`. + // By "symbol name", we mean according to `nameForPassableSymbol` + // which does some further escaping. See comment on that function. assertPassableSymbol(passable); const name = /** @type {string} */ (nameForPassableSymbol(passable)); - return `@${name}`; + return `%${name}`; } case 'undefined': { return '#undefined'; @@ -159,38 +176,23 @@ export const makeEncodeToSmallcaps = ({ if (passable === -Infinity) { return '#-Infinity'; } + // All other numbers pass through to JSON return passable; } case 'bigint': { const str = String(passable); - return passable < 0n ? `${str}n` : `+${str}n`; + return passable < 0n ? str : `+${str}`; } case 'copyRecord': { - if (hasOwnPropertyOf(passable, SCLASS)) { - // Hilbert hotel - const { [SCLASS]: sclassValue, ...rest } = passable; - /** @type {SmallcapsEncoding} */ - const result = { - [SCLASS]: 'hilbert', - original: encodeToSmallcapsRecur(sclassValue), - }; - if (ownKeys(rest).length >= 1) { - // We harden the entire smallcaps encoding before we return it. - // `encodeToSmallcaps` requires that its input be Passable, and - // therefore hardened. - // The `freeze` here is needed anyway, because the `rest` is - // freshly constructed by the `...` above, and we're using it - // as imput in another call to `encodeToSmallcaps`. - result.rest = encodeToSmallcapsRecur(freeze(rest)); - } - return result; - } // Currently copyRecord allows only string keys so this will // work. If we allow sortable symbol keys, this will need to // become more interesting. const names = ownKeys(passable).sort(); return fromEntries( - names.map(name => [name, encodeToSmallcapsRecur(passable[name])]), + names.map(name => [ + encodeToSmallcapsRecur(name), + encodeToSmallcapsRecur(passable[name]), + ]), ); } case 'copyArray': { @@ -198,8 +200,7 @@ export const makeEncodeToSmallcaps = ({ } case 'tagged': { return { - [SCLASS]: 'tagged', - tag: getTag(passable), + '#tag': getTag(passable), payload: encodeToSmallcapsRecur(passable.payload), }; } @@ -214,20 +215,23 @@ export const makeEncodeToSmallcaps = ({ } case 'promise': { const result = encodePromiseToSmallcaps(passable); - if (typeof result === 'string' && result.startsWith('?')) { + if (typeof result === 'string' && result.startsWith('&')) { return result; } assert.fail( - X`internal: Promise encoding must start with "?": ${result}`, + X`internal: Promise encoding must start with "&": ${result}`, ); } case 'error': { const result = encodeErrorToSmallcaps(passable); - if (typeof result === 'object' && result[SCLASS] === 'error') { + if ( + typeof result === 'object' && + typeof result['#error'] === 'string' + ) { return result; } assert.fail( - X`internal: Error encoding must use ${q(SCLASS)} "error": ${result}`, + X`internal: Error encoding must have "#error" property: ${result}`, ); } default: { @@ -292,169 +296,145 @@ export const makeDecodeFromSmallcaps = ({ * @param {SmallcapsEncoding} encoding must be hardened */ const decodeFromSmallcaps = encoding => { - if (typeof encoding === 'string') { - switch (encoding.charAt(0)) { - case "'": { - // un-hilbert-ify the string - return encoding.slice(1); - } - case '@': { - return passableSymbolForName(encoding.slice(1)); + switch (typeof encoding) { + case 'boolean': + case 'number': { + return encoding; + } + case 'string': { + if (encoding === '') { + return encoding; } - case '#': { - switch (encoding) { - case '#undefined': { - return undefined; - } - case '#NaN': { - return NaN; - } - case '#Infinity': { - return Infinity; + switch (encoding[0]) { + case "'": { + // un-hilbert-ify the string + return encoding.slice(1); + } + case '%': { + return passableSymbolForName(encoding.slice(1)); + } + case '#': { + switch (encoding) { + case '#undefined': { + return undefined; + } + case '#NaN': { + return NaN; + } + case '#Infinity': { + return Infinity; + } + case '#-Infinity': { + return -Infinity; + } + default: { + assert.fail(X`unknown constant "${q(encoding)}"`, TypeError); + } } - case '#-Infinity': { - return -Infinity; + } + case '+': + case '-': { + return BigInt(encoding); + } + case '$': { + const result = decodeRemotableFromSmallcaps(encoding); + if (passStyleOf(result) !== 'remotable') { + assert.fail( + X`internal: decodeRemotableFromSmallcaps option must return a remotable: ${result}`, + ); } - default: { - assert.fail(X`unknown constant "${q(encoding)}"`, TypeError); + return result; + } + case '&': { + const result = decodePromiseFromSmallcaps(encoding); + if (passStyleOf(result) !== 'promise') { + assert.fail( + X`internal: decodePromiseFromSmallcaps option must return a promise: ${result}`, + ); } + return result; } - } - case '+': - case '-': { - const last = encoding.length - 1; - assert( - encoding[last] === 'n', - X`Encoded bigint must start with "+" or "-" and end with "n": ${encoding}`, - ); - return BigInt(encoding.slice(0, last)); - } - case '$': { - const result = decodeRemotableFromSmallcaps(encoding); - if (passStyleOf(result) !== 'remotable') { + case '!': + case '"': + case '(': + case ')': + case '*': + case ',': { assert.fail( - X`internal: decodeRemotableFromSmallcaps option must return a remotable: ${result}`, + X`Special char ${q( + encoding[0], + )} reserved for future use: ${encoding}`, ); } - return result; - } - case '?': { - const result = decodePromiseFromSmallcaps(encoding); - if (passStyleOf(result) !== 'promise') { - assert.fail( - X`internal: decodePromiseFromSmallcaps option must return a promise: ${result}`, - ); + default: { + return encoding; } - return result; } - default: { + } + case 'object': { + if (encoding === null) { return encoding; } - } - } - if (!isObject(encoding)) { - // primitives pass through - return encoding; - } - if (isArray(encoding)) { - const result = []; - const { length } = encoding; - for (let i = 0; i < length; i += 1) { - result[i] = decodeFromSmallcaps(encoding[i]); - } - return result; - } - if (hasSClass(encoding)) { - /** @type {string} */ - const sclass = encoding[SCLASS]; - if (typeof sclass !== 'string') { - assert.fail(X`invalid sclass typeof ${q(typeof sclass)}`); - } - switch (sclass) { - // SmallcapsEncoding of primitives not handled by JSON - case 'tagged': { - // Using @ts-ignore rather than @ts-expect-error below because - // with @ts-expect-error I get a red underline in vscode, but - // without it I get errors from `yarn lint`. - // @ts-ignore inadequate type inference - // See https://github.com/endojs/endo/pull/1259#discussion_r954561901 - const { tag, payload } = encoding; + + if (isArray(encoding)) { + const result = []; + const { length } = encoding; + for (let i = 0; i < length; i += 1) { + result[i] = decodeFromSmallcaps(encoding[i]); + } + return result; + } + + if (hasOwnPropertyOf(encoding, '#tag')) { + const { '#tag': tag, payload, ...rest } = encoding; + typeof tag === 'string' || + assert.fail( + X`Value of "#tag", the tag, must be a string: ${encoding}`, + ); + ownKeys(rest).length === 0 || + assert.fail( + X`#tag record unexpected properties: ${q(ownKeys(rest))}`, + ); return makeTagged(tag, decodeFromSmallcaps(payload)); } - case 'error': { + if (hasOwnPropertyOf(encoding, '#error')) { const result = decodeErrorFromSmallcaps(encoding); - if (passStyleOf(result) !== 'error') { + passStyleOf(result) === 'error' || assert.fail( X`internal: decodeErrorFromSmallcaps option must return an error: ${result}`, ); - } return result; } - case 'hilbert': { - // Using @ts-ignore rather than @ts-expect-error below because - // with @ts-expect-error I get a red underline in vscode, but - // without it I get errors from `yarn lint`. - // @ts-ignore inadequate type inference - // See https://github.com/endojs/endo/pull/1259#discussion_r954561901 - const { original, rest } = encoding; - assert( - hasOwnPropertyOf(encoding, 'original'), - X`Invalid Hilbert Hotel encoding ${encoding}`, - ); - // Don't harden since we're not done mutating it - const result = { [SCLASS]: decodeFromSmallcaps(original) }; - if (hasOwnPropertyOf(encoding, 'rest')) { - assert( - typeof rest === 'object' && - rest !== null && - ownKeys(rest).length >= 1, - X`Rest encoding must be a non-empty object: ${rest}`, - ); - const restObj = decodeFromSmallcaps(rest); - // TODO really should assert that `passStyleOf(rest)` is - // `'copyRecord'` but we'd have to harden it and it is too - // early to do that. - assert( - !hasOwnPropertyOf(restObj, SCLASS), - X`Rest must not contain its own definition of ${q(SCLASS)}`, + const result = {}; + for (const encodedName of ownKeys(encoding)) { + if (typeof encodedName !== 'string') { + // TypeScript confused about `||` control flow so use `if` instead + // https://github.com/microsoft/TypeScript/issues/50739 + assert.fail( + X`Property name ${q( + encodedName, + )} of ${encoding} must be a string`, ); - defineProperties(result, getOwnPropertyDescriptors(restObj)); } - return result; - } - - case 'ibid': - case 'undefined': - case 'NaN': - case 'Infinity': - case '-Infinity': - case '@@asyncIterator': - case 'symbol': - case 'slot': { - assert.fail( - X`Unlike capData, the smallcaps protocol does not support [${q( - sclass, - )} encoding: ${encoding}.`, - ); - } - default: { - assert.fail(X`unrecognized ${q(SCLASS)}: ${q(sclass)}`, TypeError); + !encodedName.startsWith('#') || + assert.fail( + X`Unrecognized record type ${q(encodedName)}: ${encoding}`, + ); + const name = decodeFromSmallcaps(encodedName); + result[name] = decodeFromSmallcaps(encoding[encodedName]); } + return result; } - } else { - assert(typeof encoding === 'object' && encoding !== null); - const result = {}; - for (const name of ownKeys(encoding)) { - if (typeof name !== 'string') { - assert.fail( - X`Property name ${q(name)} of ${encoding} must be a string`, - ); - } - result[name] = decodeFromSmallcaps(encoding[name]); + default: { + assert.fail( + X`internal: unrecognized JSON typeof ${q( + typeof encoding, + )}: ${encoding}`, + TypeError, + ); } - return result; } }; return harden(decodeFromSmallcaps); diff --git a/packages/marshal/src/marshal-stringify.js b/packages/marshal/src/marshal-stringify.js index 2bcb35c939..652fcc1519 100644 --- a/packages/marshal/src/marshal-stringify.js +++ b/packages/marshal/src/marshal-stringify.js @@ -29,7 +29,11 @@ const badArray = harden(new Proxy(harden([]), badArrayHandler)); const { serialize, unserialize } = makeMarshal( doNotConvertValToSlot, doNotConvertSlotToVal, - { errorTagging: 'off', useSmallcaps: false }, + { + errorTagging: 'off', + // TODO fix tests to works with smallcaps and turn this option on. + useSmallcaps: false, + }, ); /** diff --git a/packages/marshal/src/marshal.js b/packages/marshal/src/marshal.js index 4c3b5a3833..84d5776054 100644 --- a/packages/marshal/src/marshal.js +++ b/packages/marshal/src/marshal.js @@ -15,7 +15,6 @@ import { import { makeDecodeFromSmallcaps, makeEncodeToSmallcaps, - SCLASS, } from './encodeToSmallcaps.js'; /** @typedef {import('./types.js').MakeMarshalOptions} MakeMarshalOptions */ @@ -186,13 +185,18 @@ export const makeMarshal = ( }; const encodePromiseToSmallcaps = promise => { - return serializeSlotToSmallcaps('?', promise); + return serializeSlotToSmallcaps('&', promise); }; - // Only under this assumption are the error encodings the same, so - // be sure. - assert(QCLASS === SCLASS); - const encodeErrorToSmallcaps = encodeErrorToCapData; + const encodeErrorToSmallcaps = err => { + // Not the most elegant way to reuse code. TODO refactor. + const capDataErr = encodeErrorToCapData(err); + const { [QCLASS]: _, message, ...rest } = capDataErr; + return harden({ + '#error': message, + ...rest, + }); + }; const encodeToSmallcaps = makeEncodeToSmallcaps({ encodeRemotableToSmallcaps, @@ -295,7 +299,7 @@ export const makeMarshal = ( }; const decodePromiseFromSmallcaps = stringEncoding => { - assert(stringEncoding.startsWith('?')); + assert(stringEncoding.startsWith('&')); // slots: $slotIndex.iface or $slotIndex const i = stringEncoding.indexOf('.'); const index = Number(stringEncoding.slice(1, i)); @@ -305,7 +309,17 @@ export const makeMarshal = ( return unserializeSlot(index, iface); }; - const decodeErrorFromSmallcaps = decodeErrorFromCapData; + const decodeErrorFromSmallcaps = encoding => { + const { '#error': message, name, ...rest } = encoding; + // Not the most elegant way to reuse code. TODO refactor + const rawTree = harden({ + [QCLASS]: 'error', + message, + name, + ...rest, + }); + return decodeErrorFromCapData(rawTree); + }; const reviveFromSmallcaps = makeDecodeFromSmallcaps({ decodeRemotableFromSmallcaps, diff --git a/packages/marshal/test/test-marshal-far-obj.js b/packages/marshal/test/test-marshal-far-obj.js index 58ae01e39d..a0463f71db 100644 --- a/packages/marshal/test/test-marshal-far-obj.js +++ b/packages/marshal/test/test-marshal-far-obj.js @@ -61,14 +61,10 @@ test('Remotable/getInterfaceOf', t => { return 'slot'; }; const m = makeMarshal(convertValToSlot, undefined, { - useSmallcaps: false, + useSmallcaps: true, }); t.deepEqual(m.serialize(p2), { - body: JSON.stringify({ - '@qclass': 'slot', - iface: 'Alleged: Thing', - index: 0, - }), + body: '#"$0.Alleged: Thing"', slots: ['slot'], }); }); @@ -172,15 +168,11 @@ test('transitional remotables', t => { return presence; } const { serialize: ser } = makeMarshal(convertValToSlot, convertSlotToVal, { - useSmallcaps: false, + useSmallcaps: true, }); const yesIface = { - body: JSON.stringify({ - '@qclass': 'slot', - iface: 'Alleged: iface', - index: 0, - }), + body: '#"$0.Alleged: iface"', slots: ['slot'], }; diff --git a/packages/marshal/test/test-marshal-justin.js b/packages/marshal/test/test-marshal-justin.js index 017498fe52..258db7604d 100644 --- a/packages/marshal/test/test-marshal-justin.js +++ b/packages/marshal/test/test-marshal-justin.js @@ -100,6 +100,8 @@ test('serialize decodeToJustin eval round trip pairs', t => { // We're turning `errorTagging`` off only for the round trip tests, not in // general. errorTagging: 'off', + // justin or its tests not yet ready for smallcaps + // TODO make Justin work with smallcaps and turn this option on useSmallcaps: false, }); for (const [body, justinSrc] of jsonPairs) { diff --git a/packages/marshal/test/test-marshal-smallcaps.js b/packages/marshal/test/test-marshal-smallcaps.js index c1fedd58d0..8b8604322d 100644 --- a/packages/marshal/test/test-marshal-smallcaps.js +++ b/packages/marshal/test/test-marshal-smallcaps.js @@ -2,9 +2,12 @@ import { test } from './prepare-test-env-ava.js'; +import { Far } from '../src/make-far.js'; import { makeMarshal } from '../src/marshal.js'; import { roundTripPairs } from './test-marshal.js'; +import { makeTagged } from '../src/makeTagged.js'; +import { passStyleOf } from '../src/passStyleOf.js'; const { freeze, isFrozen, create, prototype: objectPrototype } = Object; @@ -66,12 +69,12 @@ test('smallcaps serialize errors', t => { const ser = val => serialize(val); t.deepEqual(ser(harden(Error())), { - body: '#{"@qclass":"error","message":"","name":"Error"}', + body: '#{"#error":"","name":"Error"}', slots: [], }); t.deepEqual(ser(harden(ReferenceError('msg'))), { - body: '#{"@qclass":"error","message":"msg","name":"ReferenceError"}', + body: '#{"#error":"msg","name":"ReferenceError"}', slots: [], }); @@ -87,8 +90,7 @@ test('smallcaps serialize errors', t => { // @ts-ignore Check dynamic consequences of type violation t.falsy(isFrozen(errExtra.foo)); t.deepEqual(ser(errExtra), { - body: - '#{"@qclass":"error","message":"has extra properties","name":"Error"}', + body: '#{"#error":"has extra properties","name":"Error"}', slots: [], }); // @ts-ignore Check dynamic consequences of type violation @@ -98,7 +100,7 @@ test('smallcaps serialize errors', t => { const nonErrorProto1 = { __proto__: Error.prototype, name: 'included' }; const nonError1 = { __proto__: nonErrorProto1, message: [] }; t.deepEqual(ser(harden(nonError1)), { - body: '#{"@qclass":"error","message":"","name":"included"}', + body: '#{"#error":"","name":"included"}', slots: [], }); }); @@ -107,18 +109,16 @@ test('smallcaps unserialize errors', t => { const { unserialize } = makeTestMarshal(); const uns = body => unserialize({ body, slots: [] }); - const em1 = uns( - '#{"@qclass":"error","message":"msg","name":"ReferenceError"}', - ); + const em1 = uns('#{"#error":"msg","name":"ReferenceError"}'); t.truthy(em1 instanceof ReferenceError); t.is(em1.message, 'msg'); t.truthy(isFrozen(em1)); - const em2 = uns('#{"@qclass":"error","message":"msg2","name":"TypeError"}'); + const em2 = uns('#{"#error":"msg2","name":"TypeError"}'); t.truthy(em2 instanceof TypeError); t.is(em2.message, 'msg2'); - const em3 = uns('#{"@qclass":"error","message":"msg3","name":"Unknown"}'); + const em3 = uns('#{"#error":"msg3","name":"Unknown"}'); t.truthy(em3 instanceof Error); t.is(em3.message, 'msg3'); }); @@ -126,7 +126,9 @@ test('smallcaps unserialize errors', t => { test('smallcaps mal-formed @qclass', t => { const { unserialize } = makeTestMarshal(); const uns = body => unserialize({ body, slots: [] }); - t.throws(() => uns('#{"@qclass": 0}'), { message: /invalid sclass/ }); + t.throws(() => uns('#{"#foo": 0}'), { + message: 'Unrecognized record type "#foo": {"#foo":0}', + }); }); test('smallcaps records', t => { @@ -217,3 +219,109 @@ test('smallcaps records', t => { shouldThrow(['nonenumStringData'], REC_ONLYENUM); shouldThrow(['nonenumStringData', 'enumStringData'], REC_ONLYENUM); }); + +/** + * A test case to illustrate each of the encodings + * * `'` - escaped string + * * `+` - non-negative bigint + * * `-` - negative bigint + * * `#` - manifest constant + * * `%` - symbol + * * `$` - remotable + * * `&` - promise + */ +test('smallcaps encoding examples', t => { + const { serialize } = makeMarshal(() => 'slot', undefined, { + errorTagging: 'off', + useSmallcaps: true, + }); + const assertSer = (val, body, slots, message) => + t.deepEqual(serialize(val), { body, slots }, message); + + // Numbers + assertSer(0, '#0', [], 'zero'); + assertSer(500n, '#"+500"', [], 'bigint'); + assertSer(-400n, '#"-400"', [], '-bigint'); + + // Constants + assertSer(NaN, '#"#NaN"', [], 'NaN'); + assertSer(Infinity, '#"#Infinity"', [], 'Infinity'); + assertSer(-Infinity, '#"#-Infinity"', [], '-Infinity'); + assertSer(undefined, '#"#undefined"', [], 'undefined'); + + // Strings + assertSer('unescaped', '#"unescaped"', [], 'unescaped'); + assertSer('#escaped', `#"'#escaped"`, [], 'escaped #'); + assertSer('+escaped', `#"'+escaped"`, [], 'escaped +'); + assertSer('-escaped', `#"'-escaped"`, [], 'escaped -'); + assertSer('%escaped', `#"'%escaped"`, [], 'escaped %'); + + // Symbols + assertSer(Symbol.iterator, '#"%@@iterator"', [], 'well known symbol'); + assertSer(Symbol.for('foo'), '#"%foo"', [], 'reg symbol'); + assertSer( + Symbol.for('@@foo'), + '#"%@@@@foo"', + [], + 'reg symbol that looks well known', + ); + + // Remotables + const foo = Far('foo', {}); + const bar = Far('bar', {}); + assertSer(foo, '#"$0.Alleged: foo"', ['slot'], 'Remotable object'); + assertSer( + harden([foo, bar, foo]), + '#["$0.Alleged: foo","$1.Alleged: bar","$0"]', + ['slot', 'slot'], + 'Only show iface once', + ); + + // Promises + const p = harden(Promise.resolve(null)); + assertSer(p, '#"&0"', ['slot'], 'Promise'); + + // Arrays + assertSer(harden([1, 2n]), '#[1,"+2"]', [], 'array'); + + // Records + assertSer(harden({ foo: 1, bar: 2n }), '#{"bar":"+2","foo":1}', [], 'record'); + + // Tagged + const tagged = makeTagged('foo', 'bar'); + assertSer(tagged, '#{"#tag":"foo","payload":"bar"}', [], 'tagged'); + + // Error + const err = harden(URIError('bad uri')); + + // non-passable errors alone still serialize + assertSer(err, '#{"#error":"bad uri","name":"URIError"}', [], 'error'); + const nonPassableErr = Error('foo'); + // @ts-expect-error this type error is what we're testing + nonPassableErr.extraProperty = 'something bad'; + harden(nonPassableErr); + t.throws(() => passStyleOf(nonPassableErr), { + message: + 'Passed Error has extra unpassed properties {"extraProperty":{"value":"something bad","writable":false,"enumerable":true,"configurable":false}}', + }); + assertSer( + nonPassableErr, + '#{"#error":"foo","name":"Error"}', + [], + 'non passable errors pass', + ); + + // Hilbert record + assertSer( + harden({ + '#tag': 'what', + '#error': 'me', + '#huh': 'worry', + '': 'empty', + '%sym': 'not a symbol', + }), + '#{"":"empty","\'#error":"me","\'#huh":"worry","\'#tag":"what","\'%sym":"not a symbol"}', + [], + 'hilbert property names', + ); +});