Skip to content

Commit

Permalink
fix: switch to iterables
Browse files Browse the repository at this point in the history
  • Loading branch information
erights committed Aug 29, 2023
1 parent 5dc3a2b commit eeacc77
Show file tree
Hide file tree
Showing 10 changed files with 887 additions and 81 deletions.
8 changes: 4 additions & 4 deletions packages/marshal/src/builders/builder-types.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* @template N
* @template R
* @typedef {object} Builder
* @property {(node: N) => R} buildRoot
* @property {(buildTopFn: () => N) => R} buildRoot
*
* @property {() => N} buildUndefined
* @property {() => N} buildNull
Expand All @@ -14,14 +14,14 @@
* @property {(str: string) => N} buildString
* @property {(sym: symbol) => N} buildSymbol
*
* @property {(builtEntries: [string, N][]) => N} buildRecord
* @property {(names: string[], buildValuesIter: Iterable<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
* @property {(count: number, buildElementsIter: Iterable<N>) => N} buildArray
* @property {(tagName: string, buildPayloadFn: () => N) => N} buildTagged
* The recognizer must pass the actual tagName through. It is
* up to the builder whether it wants to encode it.
*
Expand Down
132 changes: 132 additions & 0 deletions packages/marshal/src/builders/justinBuilder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/// <reference types="ses"/>

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

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

export const makeJustinBuilder = (shouldIndent = false, _slots = []) => {
let out;
let slotIndex;
const outNextJSON = val => out.next(stringify(val));

/** @type {Builder<number,string>} */
const justinBuilder = Far('JustinBuilder', {
buildRoot: buildTopFn => {
const makeIndenter = shouldIndent ? makeYesIndenter : makeNoIndenter;
out = makeIndenter();
slotIndex = -1;
buildTopFn();
return out.done();
},

buildUndefined: () => out.next('undefined'),
buildNull: () => out.next('null'),
buildBoolean: outNextJSON,
buildNumber: num => {
if (num === Infinity) {
return out.next('Infinity');
} else if (num === -Infinity) {
return out.next('-Infinity');
} else if (is(num, NaN)) {
return out.next('NaN');
} else {
return out.next(stringify(num));
}
},
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: (names, buildValuesIter) => {
if (names.length === 0) {
return out.next('{}');
}
out.open('{');
const iter = buildValuesIter[Symbol.iterator]();
for (const name of names) {
out.line();
if (name === '__proto__') {
// JavaScript interprets `{__proto__: x, ...}`
// as making an object inheriting from `x`, whereas
// in JSON it is simply a property name. Preserve the
// JSON meaning.
out.next(`["__proto__"]:`);
} else if (identPattern.test(name)) {
out.next(`${name}:`);
} else {
out.next(`${stringify(name)}:`);
}
const { value: _, done } = iter.next();
if (done) {
break;
}
out.next(',');
}
return out.close('}');
},
buildArray: (count, buildElementsIter) => {
if (count === 0) {
return out.next('[]');
}
out.open('[');
const iter = buildElementsIter[Symbol.iterator]();
for (let i = 0; ; i += 1) {
if (i < count) {
out.line();
}
const { value: _, done } = iter.next();
if (done) {
break;
}
out.next(',');
}
return out.close(']');
},
buildTagged: (tagName, buildPayloadFn) => {
out.next(`makeTagged(${stringify(tagName)},`);
buildPayloadFn();
return out.next(')');
},

buildError: error => out.next(`${error.name}(${stringify(error.message)})`),
buildRemotable: remotable => {
slotIndex += 1;
return out.next(
`slot(${slotIndex},${stringify(getInterfaceOf(remotable))})`,
);
},
buildPromise: _promise => {
slotIndex += 1;
return out.next(`slot(${slotIndex})`);
},
});
return justinBuilder;
};
harden(makeJustinBuilder);

export const makeBuilder = () => makeJustinBuilder();
harden(makeBuilder);
80 changes: 48 additions & 32 deletions packages/marshal/src/builders/smallcapsBuilder.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
Far,
getErrorConstructor,
hasOwnPropertyOf,
mapIterable,
nameForPassableSymbol,
passableSymbolForName,
} from '@endo/pass-style';
Expand All @@ -15,15 +16,15 @@ import {

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

const { is, fromEntries, entries } = Object;
const { is, fromEntries } = 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,
buildRoot: buildTopFn => buildTopFn(),

buildUndefined: () => '#undefined',
buildNull: () => null,
Expand All @@ -50,19 +51,17 @@ const makeSmallcapsBuilder = () => {
const name = /** @type {string} */ (nameForPassableSymbol(sym));
return `%${name}`;
},
buildRecord: builtEntries =>
buildRecord: (names, buildValuesIter) => {
const builtValues = [...buildValuesIter];
assert(names.length === builtValues.length);
// 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) => ({
return fromEntries(names.map((name, i) => [name, builtValues[i]]));
},
buildArray: (_count, buildElementsIter) => harden([...buildElementsIter]),
buildTagged: (tagName, buildPayloadFn) => ({
'#tag': buildString(tagName),
payload: builtPayload,
payload: buildPayloadFn(),
}),

// TODO slots and options and all that. Also errorId
Expand Down Expand Up @@ -96,7 +95,12 @@ const recognizeString = str => {
};

const makeSmallcapsRecognizer = () => {
const smallcapsRecognizer = (encoding, builder) => {
/**
* @param {SmallcapsEncoding} encoding
* @param {Builder<SmallcapsEncoding, SmallcapsEncoding>} builder
* @returns {SmallcapsEncoding}
*/
const recognizeNode = (encoding, builder) => {
switch (typeof encoding) {
case 'boolean': {
return builder.buildBoolean(encoding);
Expand All @@ -115,9 +119,9 @@ const makeSmallcapsRecognizer = () => {
return builder.buildString(encoding.slice(1));
}
case '%': {
return builder.buildSymbol(
passableSymbolForName(encoding.slice(1)),
);
const sym = passableSymbolForName(encoding.slice(1));
assert(sym !== undefined);
return builder.buildSymbol(sym);
}
case '#': {
switch (encoding) {
Expand All @@ -134,7 +138,10 @@ const makeSmallcapsRecognizer = () => {
return builder.buildNumber(-Infinity);
}
default: {
assert.fail(X`unknown constant "${q(encoding)}"`, TypeError);
throw assert.fail(
X`unknown constant "${q(encoding)}"`,
TypeError,
);
}
}
}
Expand Down Expand Up @@ -163,20 +170,18 @@ const makeSmallcapsRecognizer = () => {
}

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

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),
);
const buildPayloadFn = () => recognizeNode(payload, builder);
return builder.buildTagged(recognizeString(tag), buildPayloadFn);
}

if (hasOwnPropertyOf(encoding, '#error')) {
Expand All @@ -190,21 +195,25 @@ const makeSmallcapsRecognizer = () => {
return builder.buildError(error);
}

const buildEntry = ([encodedName, encodedVal]) => {
const encodedNames = /** @type {string[]} */ (ownKeys(encoding)).sort();
for (const encodedName of encodedNames) {
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);
}
const buildValuesIter = mapIterable(encodedNames, encodedName =>
recognizeNode(encoding[encodedName], builder),
);
return builder.buildRecord(
encodedNames.map(recognizeString),
buildValuesIter,
);
}
default: {
assert.fail(
throw assert.fail(
X`internal: unrecognized JSON typeof ${q(
typeof encoding,
)}: ${encoding}`,
Expand All @@ -213,7 +222,14 @@ const makeSmallcapsRecognizer = () => {
}
}
};
return smallcapsRecognizer;
/**
* @param {SmallcapsEncoding} encoding
* @param {Builder<SmallcapsEncoding, SmallcapsEncoding>} builder
* @returns {SmallcapsEncoding}
*/
const recognizeSmallcaps = (encoding, builder) =>
builder.buildRoot(() => recognizeNode(encoding, builder));
return harden(recognizeSmallcaps);
};
harden(makeSmallcapsRecognizer);

Expand Down
Loading

0 comments on commit eeacc77

Please sign in to comment.