Skip to content

Commit

Permalink
feat(pass-style,marshal)!: ByteArray, a new binary Passable type
Browse files Browse the repository at this point in the history
  • Loading branch information
erights committed Jan 11, 2024
1 parent 3fae760 commit b25c593
Show file tree
Hide file tree
Showing 14 changed files with 267 additions and 10 deletions.
3 changes: 3 additions & 0 deletions packages/marshal/src/deeplyFulfilled.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ export const deeplyFulfilled = async val => {
const valPs = val.map(p => deeplyFulfilled(p));
return E.when(Promise.all(valPs), vals => harden(vals));
}
case 'byteArray': {
return val;
}
case 'tagged': {
// @ts-expect-error FIXME narrowed
const tag = getTag(val);
Expand Down
17 changes: 17 additions & 0 deletions packages/marshal/src/encodePassable.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
* @template {Passable} [T=Passable]
* @typedef {import('@endo/pass-style').CopyRecord<T>} CopyRecord
*/
/** @typedef {import('@endo/pass-style').ByteArray} ByteArray */
/** @typedef {import('./types.js').RankCover} RankCover */

const { quote: q, Fail } = assert;
Expand Down Expand Up @@ -270,6 +271,18 @@ const decodeArray = (encoded, decodePassable) => {
return harden(elements);
};

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

const encodeRecord = (record, encodePassable) => {
const names = recordNames(record);
const values = recordValues(record, names);
Expand Down Expand Up @@ -381,6 +394,9 @@ export const makeEncodePassable = (encodeOptions = {}) => {
case 'copyArray': {
return encodeArray(passable, encodePassable);
}
case 'byteArray': {
return encodeByteArray(passable, encodePassable);
}
case 'copyRecord': {
return encodeRecord(passable, encodePassable);
}
Expand Down Expand Up @@ -500,6 +516,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 @@ -196,6 +196,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 @@ -230,6 +230,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 @@ -57,6 +57,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 @@ -224,6 +226,26 @@ export const makeComparatorKit = (compareRemotables = (_x, _y) => 0) => {
// @ts-expect-error FIXME narrowed
return comparator(left.length, right.length);
}
case 'byteArray': {
const leftArray = new Uint8Array(left.slice());
const rightArray = new Uint8Array(right.slice());
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.
return comparator(left.byteLength, right.byteLength);
}
case 'tagged': {
// Lexicographic by `[Symbol.toStringTag]` then `.payload`.
// @ts-expect-error FIXME narrowed
Expand Down
85 changes: 85 additions & 0 deletions packages/pass-style/src/byteArray.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/// <reference types="ses"/>

import { PASS_STYLE, assertChecker } from './passStyle-helpers.js';

/** @typedef {import('./types.js').ByteArray} ByteArray */

const { Fail } = assert;
const { setPrototypeOf } = Object;
const { apply } = Reflect;

/**
* @type {WeakSet<ByteArray>}
*/
const genuineByteArray = new WeakSet();

const slice = ArrayBuffer.prototype.slice;
const sliceOf = (buffer, start, end) => apply(slice, buffer, [start, end]);

/**
* A ByteArray is much like an ArrayBuffer, but immutable.
* It cannot be used as an ArrayBuffer argument when a genuine ArrayBuffer is
* needed. But a `byteArray.slice()` is a genuine ArrayBuffer, initially with
* a copy of the copyByte's data.
*
* On platforms that support freezing ArrayBuffer, like perhaps a future XS,
* (TODO) the intention is that `byteArray` could hold on to a single frozen
* one and return it for every call to `arrayBuffer.slice`, rather than making
* a fresh copy each time.
*
* @param {ArrayBuffer} arrayBuffer
* @returns {ByteArray}
*/
export const makeByteArray = arrayBuffer => {
try {
// Both validates and gets an exclusive copy.
// This `arrayBuffer` must not escape, to emulate immutability.
arrayBuffer = sliceOf(arrayBuffer);
} catch {
Fail`Expected genuine ArrayBuffer" ${arrayBuffer}`;
}
/** @type {ByteArray} */
const byteArray = {
// Can't say it this way because it confuses TypeScript
// __proto__: ArrayBuffer.prototype,
byteLength: arrayBuffer.byteLength,
slice(start, end) {
return sliceOf(arrayBuffer, start, end);
},
[PASS_STYLE]: 'byteArray',
[Symbol.toStringTag]: 'ByteArray',
};
setPrototypeOf(byteArray, ArrayBuffer.prototype);
harden(byteArray);
genuineByteArray.add(byteArray);
return byteArray;
};
harden(makeByteArray);

/**
* TODO: This technique for recognizing genuine ByteArray is incompatible
* with our normal assumption of uncontrolled multiple instantiation of
* a single module. However, our only alternative to this technique is
* unprivileged re-validation of open data, which is incompat with our
* need to encapsulate `arrayBuffer`, the genuinely mutable ArrayBuffer.
*
* @param {unknown} candidate
* @param {import('./types.js').Checker} [check]
* @returns {boolean}
*/
const canBeValid = (candidate, check = undefined) =>
// @ts-expect-error `has` argument can actually be anything.
genuineByteArray.has(candidate);

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

canBeValid,

assertValid: (candidate, _passStyleOfRecur) => {
canBeValid(candidate, assertChecker);
},
});
3 changes: 3 additions & 0 deletions packages/pass-style/src/passStyleOf.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { isPromise } from '@endo/promise-kit';
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 { ErrorHelper } from './error.js';
Expand Down Expand Up @@ -36,6 +37,7 @@ const makeHelperTable = passStyleHelpers => {
const HelperTable = {
__proto__: null,
copyArray: undefined,
byteArray: undefined,
copyRecord: undefined,
tagged: undefined,
error: undefined,
Expand Down Expand Up @@ -209,6 +211,7 @@ export const passStyleOf =
(globalThis && globalThis[PassStyleOfEndowmentSymbol]) ||
makePassStyleOf([
CopyArrayHelper,
ByteArrayHelper,
CopyRecordHelper,
TaggedHelper,
ErrorHelper,
Expand Down
31 changes: 31 additions & 0 deletions packages/pass-style/src/typeGuards.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { passStyleOf } from './passStyleOf.js';
* @template {Passable} [T=Passable]
* @typedef {import('./types.js').CopyArray<T>} CopyArray
*/
/** @typedef {import('./types.js').ByteArray} ByteArray */
/**
* @template {Passable} [T=Passable]
* @typedef {import('./types.js').CopyRecord<T>} CopyRecord
Expand All @@ -23,6 +24,16 @@ const { Fail, quote: q } = assert;
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 @@ -57,6 +68,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 @@ -90,8 +119,10 @@ harden(assertRemotable);
export {
assertRecord,
assertCopyArray,
assertByteArray,
assertRemotable,
isRemotable,
isRecord,
isCopyArray,
isByteArray,
};
34 changes: 26 additions & 8 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 @@ -31,14 +35,14 @@ export type PassStyle =
| 'error'
| 'promise';

export type TaggedOrRemotable = 'tagged' | 'remotable';
export type ManifestPassStyle = 'byteArray' | 'tagged' | 'remotable';

/**
* Tagged has own [PASS_STYLE]: "tagged", [Symbol.toStringTag]: $tag.
*
* Remotable has a prototype chain in which the penultimate object has own [PASS_STYLE]: "remotable", [Symbol.toStringTag]: $iface (where both $tag and $iface must be strings, and the latter must either be "Remotable" or start with "Alleged: " or "DebugName: ").
*/
export type PassStyled<S extends TaggedOrRemotable, I extends InterfaceSpec> = {
export type PassStyled<S extends ManifestPassStyle, I extends InterfaceSpec> = {
[PASS_STYLE]: S;
[Symbol.toStringTag]: I;
};
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').
* * 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 @@ -109,17 +117,16 @@ export type PassStyleOf = {
(p: any[]): 'copyArray';
(p: Iterable<any>): 'remotable';
(p: Iterator<any, any, undefined>): 'remotable';
<T extends PassStyled<TaggedOrRemotable, any>>(p: T): ExtractStyle<T>;
<T extends PassStyled<ManifestPassStyle, any>>(p: T): ExtractStyle<T>;
(p: { [key: string]: any }): 'copyRecord';
(p: any): PassStyle;
};
/**
* 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,17 @@ export type PassableCap = Promise<any> | RemotableObject;
*/
export type CopyArray<T extends Passable = any> = Array<T>;

/**
* It has the same structural type. But because it is not a builtin ArrayBuffer,
* it does not have the same nominal type; meaning, it cannot be used as an
* argument where an ArrayBuffer is expected, like the `DataView` or typed
* array constructors.
*/
export type ByteArray = PassStyled<'byteArray', string> & {
byteLength: number;
slice(start?: number, end?: number): 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 @@ -563,6 +563,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
Loading

0 comments on commit b25c593

Please sign in to comment.