Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(pass-style,marshal): ByteArray, a new binary Passable type #1538

Draft
wants to merge 1 commit into
base: markm-to-immutable-3
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion packages/marshal/src/encodePassable.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
} from '@endo/pass-style';

/**
* @import {CopyRecord, PassStyle, Passable, RemotableObject as Remotable} from '@endo/pass-style'
* @import {CopyRecord, PassStyle, Passable, RemotableObject as Remotable, ByteArray} from '@endo/pass-style'
*/

import { b, q, Fail } from '@endo/errors';
Expand Down Expand Up @@ -462,6 +462,17 @@ const decodeLegacyArray = (encoded, decodePassable, skip = 0) => {
return harden(elements);
};

/**
* @param {ByteArray} byteArray
* @param {(byteArray: ByteArray) => string} _encodePassable
* @returns {string}
*/
const encodeByteArray = (byteArray, _encodePassable) => {
// TODO implement
Fail`encodePassable(copyData) not yet implemented: ${byteArray}`;
return ''; // Just for the type
};

const encodeRecord = (record, encodeArray, encodePassable) => {
const names = recordNames(record);
const values = recordValues(record, names);
Expand Down Expand Up @@ -626,6 +637,9 @@ const makeInnerEncode = (encodeStringSuffix, encodeArray, options) => {
case 'copyArray': {
return encodeArray(passable, innerEncode);
}
case 'byteArray': {
return encodeByteArray(passable, innerEncode);
}
case 'copyRecord': {
return encodeRecord(passable, encodeArray, innerEncode);
}
Expand Down Expand Up @@ -870,6 +884,7 @@ export const passStylePrefixes = {
tagged: ':',
promise: '?',
copyArray: '[^',
byteArray: '', // TODO pick a prefix
boolean: 'b',
number: 'f',
bigint: 'np',
Expand Down
4 changes: 4 additions & 0 deletions packages/marshal/src/encodeToCapData.js
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,10 @@ export const makeEncodeToCapData = (encodeOptions = {}) => {
case 'copyArray': {
return passable.map(encodeToCapDataRecur);
}
case 'byteArray': {
// TODO implement
throw Fail`marsal of byteArray not yet implemented: ${passable}`;
}
case 'tagged': {
return {
[QCLASS]: 'tagged',
Expand Down
4 changes: 4 additions & 0 deletions packages/marshal/src/encodeToSmallcaps.js
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,10 @@ export const makeEncodeToSmallcaps = (encodeOptions = {}) => {
case 'copyArray': {
return passable.map(encodeToSmallcapsRecur);
}
case 'byteArray': {
// TODO implement
throw Fail`marsal of byteArray not yet implemented: ${passable}`;
}
case 'tagged': {
return {
'#tag': encodeToSmallcapsRecur(getTag(passable)),
Expand Down
22 changes: 22 additions & 0 deletions packages/marshal/src/rankOrder.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ export const trivialComparator = (left, right) =>
const passStyleRanks = /** @type {PassStyleRanksRecord} */ (
fromEntries(
entries(passStylePrefixes)
// TODO Until byteArray prefix is chosen
.filter(([_style, prefixes]) => prefixes.length >= 1)
// Sort entries by ascending prefix.
.sort(([_leftStyle, leftPrefixes], [_rightStyle, rightPrefixes]) => {
return trivialComparator(leftPrefixes, rightPrefixes);
Expand Down Expand Up @@ -209,6 +211,26 @@ export const makeComparatorKit = (compareRemotables = (_x, _y) => 0) => {
// If array X is a prefix of array Y, then X has an earlier rank than Y.
return comparator(left.length, right.length);
}
case 'byteArray': {
const leftArray = new Uint8Array(left.slice(0));
const rightArray = new Uint8Array(right.slice(0));
const byteLen = Math.min(left.byteLength, right.byteLength);
for (let i = 0; i < byteLen; i += 1) {
const leftByte = leftArray[i];
const rightByte = rightArray[i];
if (leftByte < rightByte) {
return -1;
}
if (leftByte > rightByte) {
return 1;
}
}
erights marked this conversation as resolved.
Show resolved Hide resolved
// If all corresponding bytes are the same,
// then according to their lengths.
// Thus, if the data of ByteArray X is a prefix of
// the data of ByteArray Y, then X is smaller than Y.
return comparator(left.byteLength, right.byteLength);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change to shortlex order. See https://en.wikipedia.org/wiki/Shortlex_order

Shortlex order would enable encodePassable (aka compactOrdered) to encode buffers with a prefix followed by the unescaped bytes. Avoiding escaping would be great.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this makes ByteArray different than string for ordering?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. For strings, normal lexicographic ordering is overwhelmingly more expected than shortlex. For binaries of different length, I don't think there is any one overwhelming expectation.

FWIW, if I did think we could get away with shortlex for strings too, I would go for it, for the same reason. Needing to insert escapes into bulk data sucks. Attn @gibson042

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW, if I did think we could get away with shortlex for strings too, I would go for it

That would be a breaking change that would have to be opted into at definition of the stores and other components that use the order.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. That's another reason we cannot get away with it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, because of this breaking change issue, we haven't even moved forward on #2008

}
case 'tagged': {
// Lexicographic by `[Symbol.toStringTag]` then `.payload`.
const labelComp = comparator(getTag(left), getTag(right));
Expand Down
6 changes: 4 additions & 2 deletions packages/marshal/test/marshal-stringify.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,13 @@ test('marshal stringify errors', t => {
t.throws(() => stringify({}), {
message: /Cannot pass non-frozen objects like .*. Use harden()/,
});
// @ts-expect-error intentional error
erights marked this conversation as resolved.
Show resolved Hide resolved
// at-ts-ignore rather than at-expect-error because of disagreement
// @ts-ignore intentional error
t.throws(() => stringify(harden(new Uint8Array(1))), {
message: 'Cannot pass mutable typed arrays like "[Uint8Array]".',
});
// @ts-expect-error intentional error
erights marked this conversation as resolved.
Show resolved Hide resolved
// at-ts-ignore rather than at-expect-error because of disagreement
// @ts-ignore intentional error
t.throws(() => stringify(harden(new Int16Array(1))), {
message: 'Cannot pass mutable typed arrays like "[Int16Array]".',
});
Expand Down
55 changes: 55 additions & 0 deletions packages/pass-style/src/byteArray.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { X } from '@endo/errors';
import { assertChecker } from './passStyle-helpers.js';

const { getPrototypeOf, getOwnPropertyDescriptor } = Object;
const { ownKeys, apply } = Reflect;

// @ts-expect-error TODO How do I add it to the ArrayBuffer type?
const AnImmutableArrayBuffer = new ArrayBuffer(0).transferToImmutable();
/**
* As proposed, this will be the same as `ArrayBuffer.prototype`. As shimmed,
* this will be a hidden intrinsic that inherits from `ArrayBuffer.prototype`.
* Either way, get this in a way that we can trust it after lockdown, and
* require that all immutable ArrayBuffers directly inherit from it.
*/
const ImmutableArrayBufferPrototype = getPrototypeOf(AnImmutableArrayBuffer);

// @ts-expect-error ok to implicitly assert the access is found
const immutableGetter = getOwnPropertyDescriptor(
ImmutableArrayBufferPrototype,
'immutable',
).get;

/**
* @param {unknown} candidate
* @param {import('./types.js').Checker} [check]
* @returns {boolean}
*/
const canBeValid = (candidate, check = undefined) =>
(candidate instanceof ArrayBuffer &&
// @ts-expect-error TODO How do I add it to the ArrayBuffer type?
candidate.immutable) ||
erights marked this conversation as resolved.
Show resolved Hide resolved
(!!check && check(false, X`Immutable ArrayBuffer expected: ${candidate}`));

/**
* @type {import('./internal-types.js').PassStyleHelper}
*/
export const ByteArrayHelper = harden({
styleName: 'byteArray',

canBeValid,

assertValid: (candidate, _passStyleOfRecur) => {
canBeValid(candidate, assertChecker);
getPrototypeOf(candidate) === ImmutableArrayBufferPrototype ||
assert.fail(X`Malformed ByteArray ${candidate}`, TypeError);
// @ts-expect-error assume immutableGetter was found
apply(immutableGetter, candidate, []) ||
assert.fail(X`Must be an immutable ArrayBuffer: ${candidate}`);
ownKeys(candidate).length === 0 ||
assert.fail(
X`ByteArrays must not have own properties: ${candidate}`,
TypeError,
);
},
});
9 changes: 8 additions & 1 deletion packages/pass-style/src/deeplyFulfilled.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { passStyleOf } from './passStyleOf.js';
import { makeTagged } from './makeTagged.js';

/**
* @import {Passable, Primitive, CopyRecord, CopyArray, CopyTagged, RemotableObject} from '@endo/pass-style'
* @import {Passable, ByteArray, CopyRecord, CopyArray, CopyTagged, RemotableObject} from '@endo/pass-style'
*/

const { ownKeys } = Reflect;
Expand Down Expand Up @@ -105,6 +105,13 @@ export const deeplyFulfilled = async val => {
// @ts-expect-error not assignable to type 'DeeplyAwaited<T>'
return E.when(Promise.all(valPs), vals => harden(vals));
}
case 'byteArray': {
const bytes = /** @type {ByteArray} */ (val);
// @ts-expect-error Why
// "Type 'ArrayBuffer' is not assignable to type 'DeeplyAwaited<T>'."?
// TODO fix.
return bytes;
}
case 'tagged': {
const tgd = /** @type {CopyTagged} */ (val);
const tag = getTag(tgd);
Expand Down
3 changes: 3 additions & 0 deletions packages/pass-style/src/passStyleOf.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { X, Fail, q, annotateError, makeError } from '@endo/errors';
import { isObject, isTypedArray, PASS_STYLE } from './passStyle-helpers.js';

import { CopyArrayHelper } from './copyArray.js';
import { ByteArrayHelper } from './byteArray.js';
import { CopyRecordHelper } from './copyRecord.js';
import { TaggedHelper } from './tagged.js';
import {
Expand Down Expand Up @@ -43,6 +44,7 @@ const makeHelperTable = passStyleHelpers => {
const HelperTable = {
__proto__: null,
copyArray: undefined,
byteArray: undefined,
copyRecord: undefined,
tagged: undefined,
error: undefined,
Expand Down Expand Up @@ -216,6 +218,7 @@ export const passStyleOf =
(globalThis && globalThis[PassStyleOfEndowmentSymbol]) ||
makePassStyleOf([
CopyArrayHelper,
ByteArrayHelper,
CopyRecordHelper,
TaggedHelper,
ErrorHelper,
Expand Down
34 changes: 33 additions & 1 deletion packages/pass-style/src/typeGuards.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { Fail, q } from '@endo/errors';
import { passStyleOf } from './passStyleOf.js';

/** @import {CopyArray, CopyRecord, Passable, RemotableObject} from './types.js' */
/**
* @import {CopyArray, CopyRecord, Passable, RemotableObject, ByteArray} from './types.js'
*/

/**
* Check whether the argument is a pass-by-copy array, AKA a "copyArray"
Expand All @@ -13,6 +15,16 @@ import { passStyleOf } from './passStyleOf.js';
const isCopyArray = arr => passStyleOf(arr) === 'copyArray';
harden(isCopyArray);

/**
* Check whether the argument is a pass-by-copy binary data, AKA a "byteArray"
* in @endo/marshal terms
*
* @param {Passable} arr
* @returns {arr is ByteArray}
*/
const isByteArray = arr => passStyleOf(arr) === 'byteArray';
harden(isByteArray);

/**
* Check whether the argument is a pass-by-copy record, AKA a
* "copyRecord" in @endo/marshal terms
Expand Down Expand Up @@ -47,6 +59,24 @@ const assertCopyArray = (array, optNameOfArray = 'Alleged array') => {
harden(assertCopyArray);

/**
* @callback AssertByteArray
* @param {Passable} array
* @param {string=} optNameOfArray
* @returns {asserts array is ByteArray}
*/

/** @type {AssertByteArray} */
const assertByteArray = (array, optNameOfArray = 'Alleged byteArray') => {
const passStyle = passStyleOf(array);
passStyle === 'byteArray' ||
Fail`${q(
optNameOfArray,
)} ${array} must be a pass-by-copy binary data, not ${q(passStyle)}`;
};
harden(assertByteArray);

/**
* @callback AssertRecord
* @param {any} record
* @param {string=} optNameOfRecord
* @returns {asserts record is CopyRecord<any>}
Expand Down Expand Up @@ -80,8 +110,10 @@ harden(assertRemotable);
export {
assertRecord,
assertCopyArray,
assertByteArray,
assertRemotable,
isRemotable,
isRecord,
isCopyArray,
isByteArray,
};
22 changes: 17 additions & 5 deletions packages/pass-style/src/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@ export type PrimitiveStyle =
| 'string'
| 'symbol';

export type ContainerStyle = 'copyRecord' | 'copyArray' | 'tagged';
export type ContainerStyle =
| 'copyRecord'
| 'copyArray'
| 'byteArray'
| 'tagged';

export type PassStyle =
| PrimitiveStyle
Expand All @@ -49,6 +53,7 @@ export type PassByCopy =
| Primitive
| Error
| CopyArray
| ByteArray
| CopyRecord
| CopyTagged;

Expand All @@ -67,6 +72,7 @@ export type PassByRef =
* | 'string' | 'symbol'). (Passable considers `void` to be `undefined`.)
* * Containers aggregate other Passables into
* * sequences as CopyArrays (PassStyle 'copyArray'), or
* * sequences of 8-bit bytes (PassStyle 'byteArray'), or
* * string-keyed dictionaries as CopyRecords (PassStyle 'copyRecord'), or
* * higher-level types as CopyTaggeds (PassStyle 'tagged').
* * PassableCaps (PassStyle 'remotable' | 'promise') expose local values to
Expand All @@ -86,10 +92,12 @@ export type Passable<

export type Container<PC extends PassableCap, E extends Error> =
| CopyArrayI<PC, E>
| ByteArrayI
| CopyRecordI<PC, E>
| CopyTaggedI<PC, E>;
interface CopyArrayI<PC extends PassableCap, E extends Error>
extends CopyArray<Passable<PC, E>> {}
interface ByteArrayI extends ByteArray {}
interface CopyRecordI<PC extends PassableCap, E extends Error>
extends CopyRecord<Passable<PC, E>> {}
interface CopyTaggedI<PC extends PassableCap, E extends Error>
Expand All @@ -116,10 +124,9 @@ export type PassStyleOf = {
/**
* A Passable is PureData when its entire data structure is free of PassableCaps
* (remotables and promises) and error objects.
* PureData is an arbitrary composition of primitive values into CopyArray
* and/or
* CopyRecord and/or CopyTagged containers (or a single primitive value with no
* container), and is fully pass-by-copy.
* PureData is an arbitrary composition of primitive values into CopyArray,
* ByteArray, CopyRecord, and/or CopyTagged containers
* (or a single primitive value with no container), and is fully pass-by-copy.
*
* This restriction assures absence of side effects and interleaving risks *given*
* that none of the containers can be a Proxy instance.
Expand Down Expand Up @@ -156,6 +163,11 @@ export type PassableCap = Promise<any> | RemotableObject;
*/
export type CopyArray<T extends Passable = any> = Array<T>;

/**
* A `ByteArray` is a normal hardened immutable `ArrayBuffer`
*/
export type ByteArray = ArrayBuffer;

/**
* A Passable dictionary in which each key is a string and each value is Passable.
*/
Expand Down
3 changes: 3 additions & 0 deletions packages/patterns/src/keys/checkKey.js
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,9 @@ const checkKeyInternal = (val, check) => {
// A copyArray is a key iff all its children are keys
return val.every(checkIt);
}
case 'byteArray': {
return true;
}
case 'tagged': {
const tag = getTag(val);
switch (tag) {
Expand Down
24 changes: 24 additions & 0 deletions packages/patterns/src/keys/compareKeys.js
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,30 @@ export const compareKeys = (left, right) => {
// @ts-expect-error narrowed
return compareRank(left.length, right.length);
}
case 'byteArray': {
// @ts-expect-error narrowed
const leftArray = new Uint8Array(left.slice(0));
// @ts-expect-error narrowed
const rightArray = new Uint8Array(right.slice(0));
// @ts-expect-error narrowed
const byteLen = Math.min(left.byteLength, right.byteLength);
for (let i = 0; i < byteLen; i += 1) {
const leftByte = leftArray[i];
const rightByte = rightArray[i];
if (leftByte < rightByte) {
return -1;
}
if (leftByte > rightByte) {
return 1;
}
}
// If all corresponding bytes are the same,
// then according to their lengths.
// Thus, if the data of ByteArray X is a prefix of
// the data of ByteArray Y, then X is smaller than Y.
// @ts-expect-error narrowed
return compareRank(left.byteLength, right.byteLength);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If both are byteArrays, then we should delegate the whole thing to compareRank. Which, btw, we'll be switching to shortlex order.

}
case 'copyRecord': {
// Pareto partial order comparison.
// @ts-expect-error narrowed
Expand Down
Loading
Loading