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 May 20, 2023
1 parent 0ab46f7 commit 6a0cd77
Show file tree
Hide file tree
Showing 6 changed files with 459 additions and 3 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('../types').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}
*/
52 changes: 52 additions & 0 deletions packages/marshal/src/builders/justinBuilder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/// <reference types="ses"/>

import { Far, nameForPassableSymbol } from '@endo/pass-style';
import { identPattern, AtAtPrefixPattern } from '../marshal-justin.js';

const { stringify } = JSON;
const { Fail, quote: q } = assert;

export const makeJustinBuilder = out => {
const outNextJSON = val => out.next(stringify(val));

/** @type {Builder<string,string>} */
const justinBuilder = Far('JustinBuilder', {
buildRoot: _node => out.done(),

buildUndefined: () => out.next('undefined'),
buildNull: () => out.next('null'),
buildBoolean: outNextJSON,
buildNumber: outNextJSON,
buildBigint: bigint => out.next(`${bigint}n`),
buildString: outNextJSON,
buildSymbol: sym => {
assert.typeof(sym, 'symbol');
const name = nameForPassableSymbol(sym);
if (name === undefined) {
throw Fail`Symbol must be either registered or well known: ${q(sym)}`;
}
const registeredName = Symbol.keyFor(sym);
if (registeredName === undefined) {
const match = AtAtPrefixPattern.exec(name);
assert(match !== null);
const suffix = match[1];
assert(Symbol[suffix] === sym);
assert(identPattern.test(suffix));
return out.next(`Symbol.${suffix}`);
}
return out.next(`Symbol.for(${stringify(registeredName)})`);
},

buildRecord: _builtEntries => 'x',
buildArray: _builtElements => 'x',
buildTagged: (_tagName, _builtPayload) => 'x',

buildError: _error => 'x',
buildRemotable: _remotable => 'x',
buildPromise: _promise => 'x',
});
return justinBuilder;
};
harden(makeJustinBuilder);

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

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

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

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

export const makeSmallcapsBuilder = () => {
const buildString = str => {
if (startsSpecial(str)) {
// Strings that start with a special char are quoted with `!`.
// Since `!` is itself a special character, this trivially does
// the Hilbert hotel. Also, since the special characters are
// a continuous subrange of ascii, this quoting is sort-order
// preserving.
return `!${str}`;
}
// All other strings pass through to JSON
return str;
};

/** @type {Builder<Encoding,Encoding>} */
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 =>
fromEntries(
builtEntries.map(([name, builtValue]) => [
buildString(name),
builtValue,
]),
),
buildArray: builtElements => builtElements,
buildTagged: (tagName, builtPayload) => ({
'#tag': buildString(tagName),
payload: builtPayload,
}),

buildError: _error => 'x',
buildRemotable: _remotable => 'x',
buildPromise: _promise => 'x',
});
return smallcapsBuilder;
};
harden(makeSmallcapsBuilder);

const makeSmallcapsRecognizer = () => {
/**
* 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 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 '+': {
return builder.buildBigint(BigInt(encoding.slice(1)));
}
case '-': {
return builder.buildBigint(BigInt(encoding));
}
// case '$': {
// const result = decodeRemotableFromSmallcaps(
// encoding,
// decodeFromSmallcaps,
// );
// if (passStyleOf(result) !== 'remotable') {
// Fail`internal: decodeRemotableFromSmallcaps option must return a remotable: ${result}`;
// }
// return result;
// }
// case '&': {
// const result = decodePromiseFromSmallcaps(
// encoding,
// decodeFromSmallcaps,
// );
// if (passStyleOf(result) !== 'promise') {
// Fail`internal: decodePromiseFromSmallcaps option must return a promise: ${result}`;
// }
// return result;
// }
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')) {
// const result = decodeErrorFromSmallcaps(
// encoding,
// decodeFromSmallcaps,
// );
// passStyleOf(result) === 'error' ||
// Fail`internal: decodeErrorFromSmallcaps option must return an error: ${result}`;
// return result;
// }

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,
};
Loading

0 comments on commit 6a0cd77

Please sign in to comment.