Skip to content

Commit

Permalink
WIP: Event-based encoding API based on Data-E
Browse files Browse the repository at this point in the history
  • Loading branch information
erights committed Dec 31, 2023
1 parent 3e9e0dd commit d5ebb3c
Show file tree
Hide file tree
Showing 7 changed files with 452 additions and 37 deletions.
41 changes: 41 additions & 0 deletions packages/marshal/src/builders/builder-types.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/** @typedef {import('@endo/pass-style').Remotable} Remotable */

/**
* @template N
* @template R
* @typedef {object} Builder
* @property {(node: N) => R} buildRoot
*
* @property {() => N} buildUndefined
* @property {() => N} buildNull
* @property {(num: number) => N} buildNumber
* @property {(flag: boolean) => N} buildBoolean
* @property {(bigint: bigint) => N} buildBigint
* @property {(str: string) => N} buildString
* @property {(sym: symbol) => N} buildSymbol
*
* @property {(builtEntries: [string, N][]) => N} buildRecord
* The recognizer must pass the actual property names through. It is
* up to the builder whether it wants to encode them.
* It is up to the recognizer to sort the entries by their actual
* property name first, and to encode their values in the resulting
* sorted order. The builder may assume that sorted order.
* @property {(builtElements: N[]) => N} buildArray
* @property {(tagName: string, builtPayload: N) => N} buildTagged
* The recognizer must pass the actual tagName through. It is
* up to the builder whether it wants to encode it.
*
* @property {(error :Error) => N} buildError
* @property {(remotable: Remotable) => N} buildRemotable
* @property {(promise: Promise) => N} buildPromise
*/

/**
* @template E
* @template N
* @template R
* @callback Recognize
* @param {E} encoding
* @param {Builder<N,R>} builder
* @returns {R}
*/
223 changes: 223 additions & 0 deletions packages/marshal/src/builders/smallcapsBuilder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
/// <reference types="ses"/>

import {
assertPassableSymbol,
Far,
getErrorConstructor,
hasOwnPropertyOf,
nameForPassableSymbol,
passableSymbolForName,
} from '@endo/pass-style';
import {
startsSpecial,
encodeStringToSmallcaps as buildString,
} from '../encodeToSmallcaps.js';

/** @typedef {import('../encodeToSmallcaps.js').SmallcapsEncoding} SmallcapsEncoding */

const { is, fromEntries, entries } = Object;
const { isArray } = Array;
const { ownKeys } = Reflect;
const { quote: q, details: X, Fail } = assert;

const makeSmallcapsBuilder = () => {
/** @type {Builder<SmallcapsEncoding,SmallcapsEncoding>} */
const smallcapsBuilder = Far('SmallcapsBuilder', {
buildRoot: node => node,

buildUndefined: () => '#undefined',
buildNull: () => null,
buildBoolean: flag => flag,
buildNumber: num => {
// Special-case numbers with no digit-based representation.
if (Number.isNaN(num)) {
return '#NaN';
} else if (num === Infinity) {
return '#Infinity';
} else if (num === -Infinity) {
return '#-Infinity';
}
// Pass through everything else, replacing -0 with 0.
return is(num, -0) ? 0 : num;
},
buildBigint: bigint => {
const str = String(bigint);
return bigint < 0n ? str : `+${str}`;
},
buildString,
buildSymbol: sym => {
assertPassableSymbol(sym);
const name = /** @type {string} */ (nameForPassableSymbol(sym));
return `%${name}`;
},
buildRecord: builtEntries =>
// TODO Should be fromUniqueEntries, but utils needs to be
// relocated first.
fromEntries(
builtEntries.map(([name, builtValue]) => [
buildString(name),
builtValue,
]),
),
buildArray: builtElements => builtElements,
buildTagged: (tagName, builtPayload) => ({
'#tag': buildString(tagName),
payload: builtPayload,
}),

// TODO slots and options and all that. Also errorId
buildError: error => ({
'#error': buildString(error.message),
name: buildString(error.name),
}),
// TODO slots and options and all that.
buildRemotable: _remotable => '$',
// TODO slots and options and all that.
buildPromise: _promise => '&',
});
return smallcapsBuilder;
};
harden(makeSmallcapsBuilder);

/**
* Must be consistent with the full string recognition algorithm.
* Must return what that algorithm would pass to builder.buildString.
*
* @param {string} str
*/
const recognizeString = str => {
typeof str === 'string' || Fail`${str} must be a string`;
if (!startsSpecial(str)) {
return str;
}
const c = str.charAt(0);
c === '!' || Fail`${str} must encode a string`;
return str.slice(1);
};

const makeSmallcapsRecognizer = () => {
const smallcapsRecognizer = (encoding, builder) => {
switch (typeof encoding) {
case 'boolean': {
return builder.buildBoolean(encoding);
}
case 'number': {
return builder.buildNumber(encoding);
}
case 'string': {
if (!startsSpecial(encoding)) {
return builder.buildString(encoding);
}
const c = encoding.charAt(0);
switch (c) {
case '!': {
// un-hilbert-ify the string
return builder.buildString(encoding.slice(1));
}
case '%': {
return builder.buildSymbol(
passableSymbolForName(encoding.slice(1)),
);
}
case '#': {
switch (encoding) {
case '#undefined': {
return builder.buildUndefined();
}
case '#NaN': {
return builder.buildNumber(NaN);
}
case '#Infinity': {
return builder.buildNumber(Infinity);
}
case '#-Infinity': {
return builder.buildNumber(-Infinity);
}
default: {
assert.fail(X`unknown constant "${q(encoding)}"`, TypeError);
}
}
}
case '+':
case '-': {
return builder.buildBigint(BigInt(encoding));
}
case '$': {
// TODO slots and options and all that.
return builder.buildRemotable(Far('dummy'));
}
case '&': {
// TODO slots and options and all that.
return builder.buildPromise(Promise.resolve('dummy'));
}
default: {
throw Fail`Special char ${q(
c,
)} reserved for future use: ${encoding}`;
}
}
}
case 'object': {
if (encoding === null) {
return builder.buildNull();
}

if (isArray(encoding)) {
const builtElements = encoding.map(val =>
smallcapsRecognizer(val, builder),
);
return builder.buildArray(builtElements);
}

if (hasOwnPropertyOf(encoding, '#tag')) {
const { '#tag': tag, payload, ...rest } = encoding;
ownKeys(rest).length === 0 ||
Fail`#tag record unexpected properties: ${q(ownKeys(rest))}`;
return builder.buildTagged(
recognizeString(tag),
smallcapsRecognizer(payload, builder),
);
}

if (hasOwnPropertyOf(encoding, '#error')) {
// TODO slots and options and all that. Also errorId
const { '#error': message, name } = encoding;
const dMessage = recognizeString(message);
const dName = recognizeString(name);
const EC = getErrorConstructor(dName) || Error;
const errorName = `Remote${EC.name}`;
const error = assert.error(dMessage, EC, { errorName });
return builder.buildError(error);
}

const buildEntry = ([encodedName, encodedVal]) => {
typeof encodedName === 'string' ||
Fail`Property name ${q(
encodedName,
)} of ${encoding} must be a string`;
!encodedName.startsWith('#') ||
Fail`Unrecognized record type ${q(encodedName)}: ${encoding}`;
const name = recognizeString(encodedName);
return [name, smallcapsRecognizer(encodedVal, builder)];
};
const builtEntries = entries(encoding).map(buildEntry);
return builder.buildRecord(builtEntries);
}
default: {
assert.fail(
X`internal: unrecognized JSON typeof ${q(
typeof encoding,
)}: ${encoding}`,
TypeError,
);
}
}
};
return smallcapsRecognizer;
};
harden(makeSmallcapsRecognizer);

export {
makeSmallcapsBuilder as makeBuilder,
makeSmallcapsRecognizer as makeRecognizer,
};
108 changes: 108 additions & 0 deletions packages/marshal/src/builders/subgraphBuilder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/// <reference types="ses"/>

import { Far, getTag, makeTagged, passStyleOf } from '@endo/pass-style';

const { fromEntries } = Object;
const { ownKeys } = Reflect;
const { quote: q, details: X } = assert;

const makeSubgraphBuilder = () => {
const ident = val => val;

/** @type {Builder<any,any>} */
const subgraphBuilder = Far('SubgraphBuilder', {
buildRoot: ident,

buildUndefined: () => undefined,
buildNull: () => null,
buildBoolean: ident,
buildNumber: ident,
buildBigint: ident,
buildString: ident,
buildSymbol: ident,
buildRecord: builtEntries => harden(fromEntries(builtEntries)),
buildArray: ident,
buildTagged: (tagName, builtPayload) => makeTagged(tagName, builtPayload),

buildError: ident,
buildRemotable: ident,
buildPromise: ident,
});
return subgraphBuilder;
};
harden(makeSubgraphBuilder);

const makeSubgraphRecognizer = () => {
const subgraphRecognizer = (passable, builder) => {
// First we handle all primitives. Some can be represented directly as
// JSON, and some must be encoded into smallcaps strings.
const passStyle = passStyleOf(passable);
switch (passStyle) {
case 'null': {
return builder.buildNull();
}
case 'boolean': {
return builder.buildBoolean(passable);
}
case 'string': {
return builder.buildString(passable);
}
case 'undefined': {
return builder.buildUndefined();
}
case 'number': {
return builder.buildNumber(passable);
}
case 'bigint': {
return builder.buildBigint(passable);
}
case 'symbol': {
return builder.buildSymbol(passable);
}
case 'copyRecord': {
// copyRecord allows only string keys so this will
// work.
const names = ownKeys(passable).sort();
return builder.buildRecord(
names.map(name => [
name,
subgraphRecognizer(passable[name], builder),
]),
);
}
case 'copyArray': {
return builder.buildArray(
passable.map(el => subgraphRecognizer(el, builder)),
);
}
case 'tagged': {
return builder.buildTagged(
getTag(passable),
subgraphRecognizer(passable.payload, builder),
);
}
case 'remotable': {
return builder.buildRemotable(passable);
}
case 'promise': {
return builder.buildPromise(passable);
}
case 'error': {
return builder.buildError(passable);
}
default: {
assert.fail(
X`internal: Unrecognized passStyle ${q(passStyle)}`,
TypeError,
);
}
}
};
return subgraphRecognizer;
};
harden(makeSubgraphRecognizer);

export {
makeSubgraphBuilder as makeBuilder,
makeSubgraphRecognizer as makeRecognizer,
};
Loading

0 comments on commit d5ebb3c

Please sign in to comment.