-
Notifications
You must be signed in to change notification settings - Fork 74
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
WIP: Event-based encoding API based on Data-E
- Loading branch information
Showing
6 changed files
with
459 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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} | ||
*/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}; |
Oops, something went wrong.