Skip to content

Commit

Permalink
fix: CopyBytes new binary Passable type
Browse files Browse the repository at this point in the history
  • Loading branch information
erights committed Apr 6, 2023
1 parent fc74466 commit dfd0450
Show file tree
Hide file tree
Showing 14 changed files with 223 additions and 7 deletions.
3 changes: 3 additions & 0 deletions packages/marshal/src/deeplyFulfilled.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ export const deeplyFulfilled = async val => {
const valPs = val.map(p => deeplyFulfilled(p));
return E.when(Promise.all(valPs), vals => harden(vals));
}
case 'copyBytes': {
return val;
}
case 'tagged': {
const tag = getTag(val);
return E.when(deeplyFulfilled(val.payload), payload =>
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 @@ -12,6 +12,7 @@ import {
/** @typedef {import('./types.js').PassStyle} PassStyle */
/** @typedef {import('./types.js').Passable} Passable */
/** @typedef {import('./types.js').Remotable} Remotable */
/** @typedef {import('./types.js').CopyBytes} CopyBytes */
/** @template T @typedef {import('./types.js').CopyRecord<T>} CopyRecord */
/** @typedef {import('./types.js').RankCover} RankCover */

Expand Down Expand Up @@ -267,6 +268,18 @@ const decodeArray = (encoded, decodePassable) => {
return harden(elements);
};

/**
* @param {CopyBytes} copyBytes
* @param {(copyBytes: CopyBytes) => string} _encodePassable
* @returns {string}
*/
const encodeCopyBytes = (copyBytes, _encodePassable) => {
// TODO implement
throw Fail`encodePassable(copyData) not yet implemented: ${copyBytes}`;
// 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 @@ -378,6 +391,9 @@ export const makeEncodePassable = (encodeOptions = {}) => {
case 'copyArray': {
return encodeArray(passable, encodePassable);
}
case 'copyBytes': {
return encodeCopyBytes(passable, encodePassable);
}
case 'copyRecord': {
return encodeRecord(passable, encodePassable);
}
Expand Down Expand Up @@ -501,6 +517,7 @@ export const passStylePrefixes = harden({
tagged: ':',
promise: '?',
copyArray: '[',
// copyBytes: TODO
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 'copyBytes': {
// TODO implement
throw Fail`marsal of copyBytes 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 'copyBytes': {
// TODO implement
throw Fail`marsal of copyBytes not yet implemented: ${passable}`;
}
case 'tagged': {
return {
'#tag': encodeToSmallcapsRecur(getTag(passable)),
Expand Down
20 changes: 20 additions & 0 deletions packages/marshal/src/rankOrder.js
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,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 'copyBytes': {
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 CopyBytes X is a prefix of
// the data of CopyBytes Y, then X is smaller than Y.
return comparator(left.byteLength, right.byteLength);
}
case 'tagged': {
// Lexicographic by `[Symbol.toStringTag]` then `.payload`.
const labelComp = comparator(getTag(left), getTag(right));
Expand Down
1 change: 1 addition & 0 deletions packages/marshal/src/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -146,5 +146,6 @@ export {};
/** @typedef {import('@endo/pass-style').Passable} Passable */
/** @typedef {import('@endo/pass-style').Remotable} Remotable */
/** @template T @typedef {import('@endo/pass-style').CopyArray<T>} CopyArray */
/** @typedef {import('@endo/pass-style').CopyBytes} CopyBytes */
/** @template T @typedef {import('@endo/pass-style').CopyRecord<T>} CopyRecord */
/** @typedef {import('@endo/pass-style').InterfaceSpec} InterfaceSpec */
82 changes: 82 additions & 0 deletions packages/pass-style/src/copyBytes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/// <reference types="ses"/>

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

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

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

/**
* @type {WeakSet<CopyBytes>}
*/
const genuineCopyBytes = new WeakSet();

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

/**
* A CopyBytes is much like an ArrayBuffer, but immutable.
* It cannot be used as an ArrayBuffer argument when a genuine ArrayBuffer is
* needed. But a `copyBytes.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 `copyBytes` 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 {CopyBytes}
*/
export const makeCopyBytes = 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 {CopyBytes} */
const copyBytes = {
// Can't say it this way because it confuses TypeScript
// __proto__: ArrayBuffer.prototype,
byteLength: arrayBuffer.byteLength,
slice(start, end) { return sliceOf(arrayBuffer, start, end); },
[Symbol.toStringTag]: 'CopyBytes',
};
setPrototypeOf(copyBytes, ArrayBuffer.prototype);
harden(copyBytes);
genuineCopyBytes.add(copyBytes);
return copyBytes;
};
harden(makeCopyBytes);

/**
* TODO: This technique for recognizing genuine CopyBytes 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.
genuineCopyBytes.has(candidate);

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

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 @@ -4,6 +4,7 @@ import { isPromise } from '@endo/promise-kit';
import { isObject, isTypedArray, PASS_STYLE } from './passStyle-helpers.js';

import { CopyArrayHelper } from './copyArray.js';
import { CopyBytesHelper } from './copyBytes.js';
import { CopyRecordHelper } from './copyRecord.js';
import { TaggedHelper } from './tagged.js';
import { ErrorHelper } from './error.js';
Expand Down Expand Up @@ -34,6 +35,7 @@ const makeHelperTable = passStyleHelpers => {
const HelperTable = {
__proto__: null,
copyArray: undefined,
copyBytes: undefined,
copyRecord: undefined,
tagged: undefined,
error: undefined,
Expand Down Expand Up @@ -190,6 +192,7 @@ const makePassStyleOf = passStyleHelpers => {

export const passStyleOf = makePassStyleOf([
CopyArrayHelper,
CopyBytesHelper,
CopyRecordHelper,
TaggedHelper,
ErrorHelper,
Expand Down
30 changes: 30 additions & 0 deletions packages/pass-style/src/typeGuards.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { passStyleOf } from './passStyleOf.js';

/** @typedef {import('./types.js').Passable} Passable */
/** @template T @typedef {import('./types.js').CopyArray<T>} CopyArray */
/** @typedef {import('./types.js').CopyBytes} CopyBytes */
/** @template T @typedef {import('./types.js').CopyRecord<T>} CopyRecord */
/** @typedef {import('./types.js').Remotable} Remotable */

Expand All @@ -17,6 +18,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 "copyBytes"
* in @endo/marshal terms
*
* @param {Passable} arr
* @returns {arr is CopyBytes}
*/
const isCopyBytes = arr => passStyleOf(arr) === 'copyBytes';
harden(isCopyBytes);

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

/**
* @callback AssertCopyBytes
* @param {Passable} array
* @param {string=} optNameOfArray
* @returns {asserts array is CopyBytes}
*/

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

/**
* @callback AssertRecord
* @param {Passable} record
Expand Down Expand Up @@ -93,8 +121,10 @@ harden(assertRemotable);
export {
assertRecord,
assertCopyArray,
assertCopyBytes,
assertRemotable,
isRemotable,
isRecord,
isCopyArray,
isCopyBytes,
};
23 changes: 18 additions & 5 deletions packages/pass-style/src/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export {};

/**
* @typedef { PrimitiveStyle |
* "copyRecord" | "copyArray" | "tagged" |
* "copyRecord" | "copyArray" | "copyBytes" | "tagged" |
* "remotable" |
* "error" | "promise"
* } PassStyle
Expand All @@ -27,6 +27,7 @@ export {};
* "undefined" | "null" | "boolean" | "number" | "bigint" | "string" | "symbol").
* * Containers aggregate other Passables into
* * sequences as CopyArrays (PassStyle "copyArray"), or
* * sequences of 8-bit bytes (PassStyle "copyBytes"), or
* * string-keyed dictionaries as CopyRecords (PassStyle "copyRecord"), or
* * higher-order types as CopyTaggeds (PassStyle "tagged").
* * PassableCaps (PassStyle "remotable" | "promise") expose local values to remote
Expand All @@ -50,9 +51,9 @@ export {};
*
* 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,
* CopyBytes, 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 @@ -88,6 +89,18 @@ export {};
* A Passable sequence of Passable values.
*/

/**
* @typedef {{
* [Symbol.toStringTag]: string,
* byteLength: number,
* slice: (start?: number, end?: number) => ArrayBuffer,
* }} CopyBytes
* 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.
*/

/**
* @template {Passable} T
* @typedef {Record<string, T>} CopyRecord
Expand All @@ -99,7 +112,7 @@ export {};
* @typedef {{
* [Symbol.toStringTag]: string,
* payload: Passable,
* [passStyle: symbol]: 'tagged' | string,
* [passStyle: symbol]: 'tagged',
* }} CopyTagged
*
* A Passable "tagged record" with semantics specific to the tag identified in
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 @@ -533,6 +533,9 @@ const checkKeyInternal = (val, check) => {
// A copyArray is a key iff all its children are keys
return val.every(checkIt);
}
case 'copyBytes': {
return true;
}
case 'tagged': {
const tag = getTag(val);
switch (tag) {
Expand Down
20 changes: 20 additions & 0 deletions packages/patterns/src/keys/compareKeys.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,26 @@ export const compareKeys = (left, right) => {
// Thus, if array X is a prefix of array Y, then X is smaller than Y.
return compareRank(left.length, right.length);
}
case 'copyBytes': {
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 CopyBytes X is a prefix of
// the data of CopyBytes Y, then X is smaller than Y.
return compareRank(left.byteLength, right.byteLength);
}
case 'copyRecord': {
// Pareto partial order comparison.
const leftNames = recordNames(left);
Expand Down
Loading

0 comments on commit dfd0450

Please sign in to comment.