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 Oct 13, 2023
1 parent 41e1215 commit e7c0a09
Show file tree
Hide file tree
Showing 14 changed files with 256 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 @@ -16,6 +16,7 @@ import {
* @template {Passable} [T=Passable]
* @typedef {import('@endo/pass-style').CopyRecord<T>} CopyRecord
*/
/** @typedef {import('@endo/pass-style').CopyBytes} CopyBytes */
/** @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 {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 @@ -381,6 +394,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 @@ -500,6 +516,7 @@ export const passStylePrefixes = {
tagged: ':',
promise: '?',
copyArray: '[',
copyBytes: '', // 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 '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
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 copyBytes 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 @@ -211,6 +213,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
84 changes: 84 additions & 0 deletions packages/pass-style/src/copyBytes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/// <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 @@ -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 { CopyBytesHelper } from './copyBytes.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,
copyBytes: undefined,
copyRecord: undefined,
tagged: undefined,
error: undefined,
Expand Down Expand Up @@ -211,6 +213,7 @@ export const passStyleOf =
(globalThis && globalThis[PassStyleOfEndowmentSymbol]) ||
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 @@ -5,6 +5,7 @@ import { passStyleOf } from './passStyleOf.js';
* @template {Passable} [T=Passable]
* @typedef {import('./types.js').CopyArray<T>} CopyArray
*/
/** @typedef {import('./types.js').CopyBytes} CopyBytes */
/**
* @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 "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 @@ -59,6 +70,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 @@ -99,8 +127,10 @@ harden(assertRemotable);
export {
assertRecord,
assertCopyArray,
assertCopyBytes,
assertRemotable,
isRemotable,
isRecord,
isCopyArray,
isCopyBytes,
};
22 changes: 17 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 @@ -29,6 +29,7 @@ export {};
* | '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
Expand All @@ -53,10 +54,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 @@ -93,6 +93,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=Passable]
* @typedef {Record<string, T>} CopyRecord
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 @@ -560,6 +560,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 @@ -145,6 +145,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
1 change: 1 addition & 0 deletions packages/patterns/src/patterns/internal-types.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
* @template {Passable} [T=Passable]
* @typedef {import('@endo/pass-style').CopyArray<T>} CopyArray
*/
/** @typedef {import('@endo/pass-style').CopyBytes} CopyBytes */
/** @typedef {import('@endo/pass-style').Checker} Checker */
/** @typedef {import('@endo/marshal').RankCompare} RankCompare */
/** @typedef {import('@endo/marshal').RankCover} RankCover */
Expand Down
Loading

0 comments on commit e7c0a09

Please sign in to comment.