Skip to content

Commit 891d9fd

Browse files
authored
fix: fix ibids. test ibids and slots (#2625)
1 parent 365e137 commit 891d9fd

File tree

2 files changed

+223
-91
lines changed

2 files changed

+223
-91
lines changed

packages/marshal/src/marshal-justin.js

+193-80
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import { Nat } from '@agoric/nat';
77
import { assert, details as X, q } from '@agoric/assert';
88
import { QCLASS, getErrorConstructor } from './marshal';
9-
// import { makeReviverIbidTable } from './ibidTables';
9+
import { makeReviverIbidTable } from './ibidTables';
1010

1111
import './types';
1212

@@ -31,16 +31,30 @@ const { stringify: quote } = JSON;
3131
const makeYesIndenter = () => {
3232
const strings = [];
3333
let level = 0;
34-
const line = () => strings.push('\n', ' '.repeat(level));
34+
let needSpace = false;
35+
const line = () => {
36+
needSpace = false;
37+
return strings.push('\n', ' '.repeat(level));
38+
};
3539
return harden({
3640
open: openBracket => {
37-
assert(level >= 1);
3841
level += 1;
39-
return strings.push(' ', openBracket);
42+
if (needSpace) {
43+
strings.push(' ');
44+
}
45+
needSpace = false;
46+
return strings.push(openBracket);
4047
},
4148
line,
42-
next: token => strings.push(' ', token),
49+
next: token => {
50+
if (needSpace && token !== ',') {
51+
strings.push(' ');
52+
}
53+
needSpace = true;
54+
return strings.push(token);
55+
},
4356
close: closeBracket => {
57+
assert(level >= 1);
4458
level -= 1;
4559
line();
4660
return strings.push(closeBracket);
@@ -96,60 +110,30 @@ const makeNoIndenter = () => {
96110
});
97111
};
98112

99-
const registerIbid = _rawTree => {
100-
// doesn't do anything yet.
101-
};
102-
103-
const startIbid = _rawTree => {
104-
// doesn't do anything yet.
105-
};
106-
107-
const finishIbid = _rawTree => {
108-
// doesn't do anything yet.
109-
};
110-
111113
const identPattern = /^[a-zA-Z]\w*$/;
112114

113115
/**
114116
* @param {Encoding} encoding
115-
* @param {CyclePolicy} _cyclePolicy
116117
* @param {boolean=} shouldIndent
117118
* @returns {string}
118119
*/
119-
const decodeToJustin = (encoding, _cyclePolicy, shouldIndent = false) => {
120-
// const ibidTable = makeReviverIbidTable(cyclePolicy);
121-
const makeIndenter = shouldIndent ? makeYesIndenter : makeNoIndenter;
122-
const out = makeIndenter();
123-
124-
const decodeProperty = (name, value) => {
125-
out.line();
126-
assert.typeof(name, 'string', X`Property name ${name} of must be a string`);
127-
if (name === '__proto__') {
128-
// JavaScript interprets `{__proto__: x, ...}`
129-
// as making an object inheriting from `x`, whereas
130-
// in JSON it is simply a property name. Preserve the
131-
// JSON meaning.
132-
out.next(`["__proto__"]:`);
133-
} else if (identPattern.test(name)) {
134-
out.next(`${name}:`);
135-
} else {
136-
out.next(`${quote(name)}:`);
137-
}
138-
// eslint-disable-next-line no-use-before-define
139-
recur(value);
140-
out.next(',');
141-
};
120+
const decodeToJustin = (encoding, shouldIndent = false) => {
121+
const ibidTable = makeReviverIbidTable('forbidCycles');
122+
const ibidMap = new Map();
142123

143124
/**
144-
* Modeled after `fullRevive` in marshal.js
125+
* The first pass populates ibidMap for use by `decode`.
126+
* Since this is the first pass, it should do all input validation.
127+
* Its control flow should mirror `recur` as closely as possible
128+
* and the two should be maintained together. They must visit everything
129+
* in the same order.
145130
*
146131
* @param {Encoding} rawTree
147-
* @returns {number}
132+
* @returns {void}
148133
*/
149-
const recur = rawTree => {
134+
const prepare = rawTree => {
150135
if (Object(rawTree) !== rawTree) {
151-
// primitives get quoted
152-
return out.next(quote(rawTree));
136+
return;
153137
}
154138
// Assertions of the above to narrow the type.
155139
assert.typeof(rawTree, 'object');
@@ -162,17 +146,12 @@ const decodeToJustin = (encoding, _cyclePolicy, shouldIndent = false) => {
162146
X`invalid qclass typeof ${q(typeof qclass)}`,
163147
);
164148
assert(!Array.isArray(rawTree));
165-
// Switching on `encoded[QCLASS]` (or anything less direct, like
166-
// `qclass`) does not discriminate rawTree in typescript@4.2.3 and
167-
// earlier.
168149
switch (rawTree['@qclass']) {
169-
// Encoding of primitives not handled by JSON
170150
case 'undefined':
171151
case 'NaN':
172152
case 'Infinity':
173153
case '-Infinity': {
174-
// Their qclass is their expression source.
175-
return out.next(qclass);
154+
return;
176155
}
177156
case 'bigint': {
178157
const { digits } = rawTree;
@@ -181,19 +160,20 @@ const decodeToJustin = (encoding, _cyclePolicy, shouldIndent = false) => {
181160
'string',
182161
X`invalid digits typeof ${q(typeof digits)}`,
183162
);
184-
return out.next(`${BigInt(digits)}n`);
163+
return;
185164
}
186165
case '@@asyncIterator': {
187-
return out.next('Symbol.asyncIterator');
166+
return;
188167
}
189-
190168
case 'ibid': {
191169
const { index } = rawTree;
192-
return out.next(`getIbid(${index})`);
170+
// ibidTable's `get` does some input validation.
171+
const rawNode = ibidTable.get(index);
172+
ibidMap.set(rawNode, index);
173+
return;
193174
}
194-
195175
case 'error': {
196-
registerIbid(rawTree);
176+
ibidTable.register(rawTree);
197177
const { name, message } = rawTree;
198178
assert.typeof(
199179
name,
@@ -209,27 +189,24 @@ const decodeToJustin = (encoding, _cyclePolicy, shouldIndent = false) => {
209189
'string',
210190
X`invalid error message typeof ${q(typeof message)}`,
211191
);
212-
return out.next(`${name}(${quote(message)})`);
192+
return;
213193
}
214-
215194
case 'slot': {
216-
registerIbid(rawTree);
217-
let { index, iface } = rawTree;
218-
index = Number(Nat(index));
195+
ibidTable.register(rawTree);
196+
const { index, iface } = rawTree;
197+
assert.typeof(index, 'number');
198+
Nat(index);
219199
assert.typeof(iface, 'string');
220-
iface = quote(iface);
221-
return out.next(`getSlotVal(${index},${iface})`);
200+
return;
222201
}
223-
224202
case 'hilbert': {
225-
startIbid(rawTree);
203+
ibidTable.start(rawTree);
226204
const { original, rest } = rawTree;
227205
assert(
228206
'original' in rawTree,
229207
X`Invalid Hilbert Hotel encoding ${rawTree}`,
230208
);
231-
out.open('{');
232-
decodeProperty(QCLASS, original);
209+
prepare(original);
233210
if ('rest' in rawTree) {
234211
assert.typeof(
235212
rest,
@@ -245,14 +222,155 @@ const decodeToJustin = (encoding, _cyclePolicy, shouldIndent = false) => {
245222
!(QCLASS in rest),
246223
X`Rest encoding ${rest} must not contain ${q(QCLASS)}`,
247224
);
248-
startIbid(rest);
225+
ibidTable.start(rest);
226+
const names = ownKeys(rest);
227+
for (const name of names) {
228+
assert.typeof(
229+
name,
230+
'string',
231+
X`Property name ${name} of ${rawTree} must be a string`,
232+
);
233+
prepare(rest[name]);
234+
}
235+
ibidTable.finish(rest);
236+
}
237+
ibidTable.finish(rawTree);
238+
return;
239+
}
240+
241+
default: {
242+
assert.fail(X`unrecognized ${q(QCLASS)} ${q(qclass)}`, TypeError);
243+
}
244+
}
245+
} else if (Array.isArray(rawTree)) {
246+
ibidTable.start(rawTree);
247+
const { length } = rawTree;
248+
for (let i = 0; i < length; i += 1) {
249+
prepare(rawTree[i]);
250+
}
251+
ibidTable.finish(rawTree);
252+
} else {
253+
ibidTable.start(rawTree);
254+
const names = ownKeys(rawTree);
255+
for (const name of names) {
256+
assert.typeof(
257+
name,
258+
'string',
259+
X`Property name ${name} of ${rawTree} must be a string`,
260+
);
261+
prepare(rawTree[name]);
262+
}
263+
ibidTable.finish(rawTree);
264+
}
265+
};
266+
267+
const makeIndenter = shouldIndent ? makeYesIndenter : makeNoIndenter;
268+
const out = makeIndenter();
269+
270+
/**
271+
* This is the second pass recursion after the first pass `prepare`.
272+
* The first pass initialized `ibidMap` and did input validation so
273+
* here we can safely assume everything it validated.
274+
*
275+
* @param {Encoding} rawTree
276+
* @returns {number}
277+
*/
278+
const decode = rawTree => {
279+
if (ibidMap.has(rawTree)) {
280+
const index = ibidMap.get(rawTree);
281+
out.next(`initIbid(${index},`);
282+
// eslint-disable-next-line no-use-before-define
283+
recur(rawTree);
284+
return out.next(')');
285+
}
286+
// eslint-disable-next-line no-use-before-define
287+
return recur(rawTree);
288+
};
289+
290+
const decodeProperty = (name, value) => {
291+
out.line();
292+
if (name === '__proto__') {
293+
// JavaScript interprets `{__proto__: x, ...}`
294+
// as making an object inheriting from `x`, whereas
295+
// in JSON it is simply a property name. Preserve the
296+
// JSON meaning.
297+
out.next(`["__proto__"]:`);
298+
} else if (identPattern.test(name)) {
299+
out.next(`${name}:`);
300+
} else {
301+
out.next(`${quote(name)}:`);
302+
}
303+
decode(value);
304+
out.next(',');
305+
};
306+
307+
/**
308+
* Modeled after `fullRevive` in marshal.js
309+
*
310+
* @param {Encoding} rawTree
311+
* @returns {number}
312+
*/
313+
const recur = rawTree => {
314+
if (Object(rawTree) !== rawTree) {
315+
// primitives get quoted
316+
return out.next(quote(rawTree));
317+
}
318+
// Assertions of the above to narrow the type.
319+
assert.typeof(rawTree, 'object');
320+
assert(rawTree !== null);
321+
if (QCLASS in rawTree) {
322+
const qclass = rawTree[QCLASS];
323+
assert.typeof(qclass, 'string');
324+
assert(!Array.isArray(rawTree));
325+
// Switching on `encoded[QCLASS]` (or anything less direct, like
326+
// `qclass`) does not discriminate rawTree in typescript@4.2.3 and
327+
// earlier.
328+
switch (rawTree['@qclass']) {
329+
// Encoding of primitives not handled by JSON
330+
case 'undefined':
331+
case 'NaN':
332+
case 'Infinity':
333+
case '-Infinity': {
334+
// Their qclass is their expression source.
335+
return out.next(qclass);
336+
}
337+
case 'bigint': {
338+
const { digits } = rawTree;
339+
return out.next(`${BigInt(digits)}n`);
340+
}
341+
case '@@asyncIterator': {
342+
return out.next('Symbol.asyncIterator');
343+
}
344+
345+
case 'ibid': {
346+
const { index } = rawTree;
347+
return out.next(`getIbid(${index})`);
348+
}
349+
350+
case 'error': {
351+
const { name, message } = rawTree;
352+
return out.next(`${name}(${quote(message)})`);
353+
}
354+
355+
case 'slot': {
356+
let { index, iface } = rawTree;
357+
index = Number(Nat(index));
358+
iface = quote(iface);
359+
return out.next(`getSlotVal(${index},${iface})`);
360+
}
361+
362+
case 'hilbert': {
363+
const { original, rest } = rawTree;
364+
out.open('{');
365+
decodeProperty(QCLASS, original);
366+
if ('rest' in rawTree) {
367+
assert.typeof(rest, 'object');
368+
assert(rest !== null);
249369
const names = ownKeys(rest);
250370
for (const name of names) {
251371
decodeProperty(name, rest[name]);
252372
}
253-
finishIbid(rest);
254373
}
255-
finishIbid(rawTree);
256374
return out.close('}');
257375
}
258376

@@ -261,38 +379,33 @@ const decodeToJustin = (encoding, _cyclePolicy, shouldIndent = false) => {
261379
}
262380
}
263381
} else if (Array.isArray(rawTree)) {
264-
startIbid(rawTree);
265382
const { length } = rawTree;
266383
if (length === 0) {
267-
finishIbid(rawTree);
268384
return out.next('[]');
269385
} else {
270386
out.open('[');
271387
for (let i = 0; i < length; i += 1) {
272388
out.line();
273-
recur(rawTree[i]);
389+
decode(rawTree[i]);
274390
out.next(',');
275391
}
276-
finishIbid(rawTree);
277392
return out.close(']');
278393
}
279394
} else {
280-
startIbid(rawTree);
281395
const names = ownKeys(rawTree);
282396
if (names.length === 0) {
283-
finishIbid(rawTree);
284397
return out.next('{}');
285398
} else {
286399
out.open('{');
287400
for (const name of names) {
288401
decodeProperty(name, rawTree[name]);
289402
}
290-
finishIbid(rawTree);
291403
return out.close('}');
292404
}
293405
}
294406
};
295-
recur(encoding);
407+
prepare(encoding);
408+
decode(encoding);
296409
return out.done();
297410
};
298411
harden(decodeToJustin);

0 commit comments

Comments
 (0)