-
Notifications
You must be signed in to change notification settings - Fork 75
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(marshal): Allow promise stand-ins #1313
base: master
Are you sure you want to change the base?
Conversation
@gibson042 since this is stacked on #1311 , I edited the github PR to compare to #1311 's fork for easier reviewability. If this is not appropriate, feel free to change back. (#1311 has since been merged, so this note is no longer relevant) |
return; | ||
} | ||
|
||
PromiseHelper.canBeValid(candidate, assertChecker); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems like we're duplicating the isPromise
check if the candidate is not a native promise, which means really we're creating 2 throw away Promise
objects.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But only one extra one, since we gotta do at least one. Unless we wanna invert the order of tests, which is likely to be net more expensive.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's actually even more than that, because passStyleOf
itself uses isPromise
on every [frozen] object, and then every one that fails the check and does not have an explicit PASS_STYLE is subject to iteration over each helper's canBeValid
in priority order, with promise's using isPromise
and then each object accepted by that canBeValid
is passed to assertValid
(which also uses isPromise
). I'll reorder to check PASS_STYLE first, but that will still leave two calls for ersatz promises, and if we really care then I think we'll need a WeakMap.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
passStyleOf
memoizes
const descKeys = ownKeys(candidate); | ||
(descKeys.length === 1 && descKeys[0] === PASS_STYLE) || | ||
assert.fail(X`Must not have any own properties: ${candidate}`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Being able to specify the toString()
method is literally the only thing that matters to me here. Everything else is just machinery to enable that to work.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
...
toString()
method ...
Cannot do that in any case, and this PR wisely does not attempt to. What do you really need? What are you doing now, so that your current state works? My sense is that you only need a pair of functions which can be local to a context, where
getSomething(makePromiseWithSomething(something)) === something
for some appropriate somethings. We can do that trivially with real promises and a weakmap, yes?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When discussing this with @erights we didn't see a need to disallow any more methods than for remotables.
True. And what I said then contradicts what I said above. There are two simple extremes, and we should do one or the other.
- Allow mostly-arbitrary unvalidatable methods, like we do for remotables. This is why remotables can have an arbitrary
toString
method. - Data-only, which is where this PR currently is, and why I'm trying to avoid any unvalidatable behavior.
The problem is that we are still operating under the "any module might be multiply instantiated" restriction, and so any objective predicate, like passStyleOf
, cannot brand things to remember that they are safe by construction. It can only tell that it is safe by inspection. passStyleOf
does use a memo table to avoid redundant work, which is much like a brand table. Except that if absent, it always falls back to inspection. The behavior of non-primordial non-global methods cannot be inspected to be safe.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This comes down to a tension between practicality and the principle that unmarshaling should recover full state. We can relax this constraint if the use case is in fact sufficiently strong, but we can't go in the other direction.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not approving for two reasons:
- The requested changes, in particular using
checkTagRecord
for better validation. - In light of subsequent discussions, I am queasy about introducing something
passStyleOf
recognizes as a promise type, but which cannot yet work withE(
norE.when(
.
Despite the second bullet, please do respond to requested changes, so we'll be closer to moving ahead if/when the second bullet is resolved. There are two needs this PR would be a step towards:
- @FUDCo 's immediate need for his encode/decode manipulation in the post-smallcaps SwingSet tests,
- @mhofman 's plans for virtual persistent promises.
@FUDCo already has something working. I'd like to see that before judging that use to be urgent. Assuming it is not urgent, I'd like to hold this off until we find something that works with E
and E.when
, probably as part of doing virtual and durable promises as the same time.
@@ -20,7 +21,7 @@ import { assertSafePromise } from './helpers/safe-promise.js'; | |||
/** @typedef {import('./types.js').PassStyleOf} PassStyleOf */ | |||
/** @typedef {import('./types.js').PrimitiveStyle} PrimitiveStyle */ | |||
|
|||
/** @typedef {Exclude<PassStyle, PrimitiveStyle | "promise">} HelperPassStyle */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Whoa! TIL Exclude
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see this is from #1043 which I should have paid more attention to at the time. It surprises me, but it does look good.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No change suggested.
@@ -195,6 +197,7 @@ export const passStyleOf = makePassStyleOf([ | |||
CopyRecordHelper, | |||
TaggedHelper, | |||
RemotableHelper, | |||
PromiseHelper, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't you also delete
if (isPromise(inner)) {
assertSafePromise(inner);
return 'promise';
}
from lines 150-153 above?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Only if we also move the prohibition of a then
method. It seemed cleaner to keep it at the expense of some redundancy.
serializeBodyFormat: 'capdata', | ||
}, | ||
); | ||
const assertRoundTrip = (val, body, slots, description) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
More better testing once we have arb-passable.js
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Absolutely!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
;)
No change suggested for this PR.
08b7e45
to
c206d14
Compare
6dff770
to
b016e59
Compare
packages/marshal/src/passStyleOf.js
Outdated
// "promise" was a late addition, so we tolerate its absence. | ||
if (HelperTable.promise === undefined) { | ||
delete HelperTable.promise; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But it is an addition provided by this same PR. In what scenario could this condition ever happen?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If any code outside of this package calls makePassStyleOf
, which is exported. However, there are no examples of that anywhere in endo or agoric-sdk AFAICT... should I remove this leeway?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Where is makePassStyleOf
exported? I should not be.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, my mistake! I guess I was looking elsewhere. Fixed.
return ( | ||
(candidate[PASS_STYLE] === 'promise' || | ||
((passStyleDesc && passStyleDesc.value === 'promise') || |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Aha. Would be vulnerable to the obscure Object.prototype.value = junk
arrack on descriptors we just talked about in the endo meeting. (Were you there for that?) However, this is in the marshal package, not the ses package, which should only execute after lockdown, and so is not vulnerable. But amusing nonetheless!
No change suggested.
const proto = getPrototypeOf(candidate); | ||
proto === objectPrototype || | ||
proto === null || | ||
checkTagRecord(candidate, 'promise', assertChecker); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, good choice moving checkTagRecord
from canBeValid
to assertValid
.
No change suggested.
// XXX Should this (and TaggedHelper.assertValid) support a null prototype? | ||
getPrototypeOf(candidate) === objectPrototype || | ||
assert.fail(X`Unexpected prototype for: ${candidate}`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good question. Yes, I think both should. But here's a counter-argument that says we shouldn't support it at all, i.e., that we should remove such support even from copyRecords:
Any such non-standard null
prototype is not pass invariant. If such a copyRecord is passed through any marshal barrier, the copyRecord that gets unserialized on the other side will always inherit directly from `Object.prototype'.
So, I am still on the fence. Opinions?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am in favor of removing support for all null-prototype passables unless we have a clear scenario benefiting from them, in which case every check for Object.prototype should be expanded accordingly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree. So no change suggested here. Anytime you'd like to propagate this reform (assuming it doesn't break anything but tests), I'll happily review. Thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Necroing this thread but an argument could be made that a copyRecord should be unmarshalled with a null prototype instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given that we agree on preserving pass-invariance, there are only two solutions:
- All CopyRecords inherit from
Object.prototype
(status quo) - All CopyRecords inherit from
null
(what we’d all prefer if we could have it everywhere at reasonable cost)
Yes, we could change passStyleOf
and unserialize
to be consistent with (2). What we cannot reasonably change is requiring everyone to write
harden({
__proto__: null,
foo: 3
})
rather than the status quo
harden({foo: 3})
Yes, there are approaches to this notational constraint, like using something instead of harden
that either
- modifies the argument record in place (like
Far
does), or - produces a new object derived from the argument object
Altogether, for the way we use CopyRecords everywhere, I don’t think either of these coping strategies are at reasonable notational and explanatory cost. Such a cure would be worse than the disease.
@@ -20,7 +21,7 @@ import { assertSafePromise } from './helpers/safe-promise.js'; | |||
/** @typedef {import('./types.js').PassStyleOf} PassStyleOf */ | |||
/** @typedef {import('./types.js').PrimitiveStyle} PrimitiveStyle */ | |||
|
|||
/** @typedef {Exclude<PassStyle, PrimitiveStyle | "promise">} HelperPassStyle */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No change suggested.
serializeBodyFormat: 'capdata', | ||
}, | ||
); | ||
const assertRoundTrip = (val, body, slots, description) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
;)
No change suggested for this PR.
@@ -55,6 +77,89 @@ test('some passStyleOf rejections', t => { | |||
message: /\[Promise\]" - Must not have any own properties: \["then"\]/, | |||
}); | |||
|
|||
// Pseudo-promise | |||
for (const proto of [null, {}]) { | |||
const fakeprbadProto = makePseudoPromiseVariant(proto); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This one is genuinely fake. No change suggested.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Were we to go ahead with this, its current state would LGTM
@@ -45,6 +45,12 @@ test('passStyleOf basic success cases', t => { | |||
t.is(passStyleOf(harden({ then: 'non-function then ok' })), 'copyRecord'); | |||
t.is(passStyleOf(makeTagged('unknown', undefined)), 'tagged'); | |||
t.is(passStyleOf(harden(Error('ok'))), 'error'); | |||
|
|||
const hairlessError = Error('hairless'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool name!
No change suggested.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
c206d14
to
7f0aa17
Compare
3bcc377
to
389d4c7
Compare
Fixes #1312 This is the most restrictive solution, in which an ersatz promise is not allowed to have any properties or methods (although that may be relaxed in the future to support a `then` method and/or settlement/resolution data).
Better aligns with tag records
Co-authored-by: Mark S. Miller <[email protected]>
4c41e58
to
397a2bf
Compare
const tagDesc = getOwnPropertyDescriptor(candidate, toStringTag); | ||
(tagDesc && tagDesc.value === 'Pseudo-promise') || | ||
assert.fail( | ||
X`Pseudo-promise must be an object with ${q(toStringTag)} ${q( | ||
'Pseudo-promise', | ||
)}: ${candidate}`, | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems overly restrictive. I'm wondering if we may ever want to differentiate between different kinds of pseudo promises in which case toStringTag
would be appropriate.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I figure we can relax this constraint if that need arises.
); | ||
const keys = ownKeys(candidate); | ||
keys.every(k => k === PASS_STYLE || k === toStringTag) || | ||
assert.fail(X`Unexpected properties on pseudo-promise ${keys}`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
assert.fail(X`Unexpected properties on pseudo-promise ${keys}`); | |
assert.fail(X`Unexpected properties on pseudo-promise ${keys.filter(k => k !== PASS_STYLE && k !== toStringTag)}`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done with the ...rest
pattern used elsewhere.
@mhofman I just created the label |
Stacked on #1311
Fixes #1312
This is the most restrictive solution, in which a pseudo-promise is not
allowed to have any properties or methods (although that may be relaxed
in the future to support a
then
method and/or settlement/resolutiondata).
TODO:
check
calls #1310 )