Hi Jakob,
Whew! I got her done! Exceptions now work with the event loop in the Vat. Using a suggestion from the Squeak Slack channel, I am using StandardToolSet>>#debugEventualException:, a method I defined in PromisesLocal, modifying #debugException:, called from EventualMessageSend>>#value, on Exception. Yay!
- Unblocks the event loop and discards the exception in this context.
- Resolves the promise to broken.
- Displays a Debugger on the signaler context, pruned.
- Debugger is proceedable, and schedules the context back into the vat for resumption on the event loop process.
I intend to ask you your view of PromisesLocal as adhering to Promises A+ standard. With #whenResolved: & #whenRejected: this implementation has the equivalent of #then:. Exceptions are captured and linked back to the Vat it came from.
Installer ss project: 'Cryptography'; install: 'PromisesLocal'.
Then run this script:
(1 eventual / 0) explore. 1 eventual explore.
You should get two explorers on ERefs ( 1 PromiseERef that #become: a BrokenERef and one NearERef) and one Debugger on ZeroDivide. The second line guarantees the event loop is not blocked.
I am working on ASN1 encoding of Remote Promise objects (EventualMessage & EventualDesc), so they can be bit identical between Squeak & Java. Bringing remote capabilities, following the Promise A+ specification.
Since you were working with Futures in Squeak, I welcome your views on this implementation.
Kindly, rabbit
On 6/21/20 5:53 PM, Robert Withers wrote:
Hi Jakob,
I also have a promises implementation for Squeak and Java, that was derived from ERights, which precedes the JavaScript impl, both by Mark Miller. They do NOT throw up exceptions but they do resolve the promise to a BrokenERef, encapsulating the exception.
You can load the following and run the tests.
Installer ss project: 'Cryptography'; install: 'PromisesLocal'.
Then I converted the first code you presented as the following.
promise := 1 eventual / 0.
Number compile: 'methodWithTypo ^ self asstring'. promise := 1 eventual methodWithTypo.
They both resolve to BrokenERefs.
I got a little lost in capturing exceptions, within the Vat's event loop #processSends. I have tickled my implementation a little to try and get the Vat event thread to throw an exception, which presents a Debugger. I have been unable to pop up the Debugger on the error, but the promise does get smashed.
These Promises also pipelines the failure to subsequent message sends, with subsequent broken promises. So this also breaks with ZeroDivide.
(1 eventual / 0) * 10.
Here are the immediate promise return and the smashed promise to a BrokenERef.
Kindly, Robert
On 6/21/20 4:57 PM, Jakob Reschke wrote:
Hi all,
Tony has recently fixed our Promise implementation to make it more Promises/A+ compliant, thank you!
In the discussion that lead to this fix [1], I already pointed out a difference between exceptions in Smalltalk and in JavaScript, where the Promises/A+ specification originates: Smalltalk exceptions can be resumed, while JavaScript exceptions cannot be resumed and always unroll the stack.
The spec [2] says that if the onFulfilled or onRejected callback of a #then call on a promise throws an exception, then the promise returned by the #then call shall be rejected with the exception thrown.
Our current Promise implementation matches this for the blocks supplied to #then:, #ifRejected: or #then:ifRejected, by catching all Errors in the blocks and rejecting the promise. But this does not allow a Squeak user to deal with exceptions in a debugger if they are signalled in the callbacks, because they are caught. The same also applies to #future promises. The latter are not really covered by the Promises/A+ spec (because it does not force the resolution or rejection of a promise that is not the result of a #then, and there is no #future in JavaScript), but futures exhibit the same problem of not being resumable in the debugger. Example:
promise := 1 future / 0. "<= inspect it => promise is rejected, regardless of your actions in the debugger" Number compile: 'methodWithTypo ^ self asstring'. promise := 1 future methodWithTypo. "<= inspect it => promise is rejected, no chance to fix the misspelling of asString in the debugger and proceed"
I could imagine instead letting all exceptions pass during the future or callback block evaluation, and only reject the promise if the evaluation is eventually curtailed due to the exception (be it an Error or not, think of Warning or ModificationForbidden). Example expectations:
promise := 1 future / 0. "<= inspect it, press Proceed in the debugger, => promise is resolved" promise := 1 future / 0. "<= inspect it, press Abandon in the debugger, => promise is rejected" promise := 1 future methodWithTypo. "<= inspect it, fix the typo of asString in the debugger, proceed, => promise is resolved with '1'"
It could be done by fulfilling a Promise about aBlock similar to this:
[ self resolveWith: aBlock value ] on: Exception do: [ :ex | | resumed | resumed := false. [ | result | result := ex outer. resumed := true. ex resume: result] ifCurtailed: [resumed ifFalse: [self future rejectWith: ex]]]
(Find the current implementations here: Promise>>#fulfillWith:passErrors: and Promise>>#then:ifRejected:)
Note that the #outer send would only trigger handlers in the Project/World loop, or the defaultAction of the exception. The #future in front of #rejectWith: is there to avoid curtailing the unwind block context of ifCurtailed: itself if there are further errors in the rejection callbacks of the promise. The behavior of non-local exits from unwind contexts is undefined in the Smalltalk ANSI standard (just like resume: or return: in a defaultAction, or not sending resume: or return: in an on:do: exception handler at all -- VA Smalltalk interprets that as resume, while Squeak does return, for example).
This implementation would also allow all deferred Notifications to pass and not reject the promise. That is because true notifications just resume silently if they are not handled.
promise := [Notification signal: 'hi there'. 42] future value. "<= inspect it => Expected: resolved with 42. Actual (today): it is needlessly rejected with Notification 'hi there'"
Pressing Proceed in the debugger on officially non-resumable errors (which is possible) would also not reject the promise. But further errors/debuggers are likely to appear, of which one may eventually be used to abort the execution. If the execution finishes after repeatedly pressing Proceed, then fine, resolve the promise with whatever the outcome was.
promise := [self error: 'Fatal error'. 42] future value. "<= inspect it, proceed after the so-called fatal error, => Expected: resolved with 42. Actual: there is no debugger, the promise is immediately rejected."
promise := [1 / 0 + 3] future value. "<= Cannot be resumed/proceeded because if ZeroDivide is resumed, it will return the exception, and ZeroDivide does not understand +, which cannot be resumed without changing the code. So you'd have to curtail the block execution => Expected: rejected with ZeroDivide or MessageNotUnderstood (depending on when you press Abandon or recompile the DoIt)."
promise := [1 / 0 + 3] future value. "... or instead of changing the code or aborting, you could choose 'return entered value' in one of the debuggers, and thereby complete the evaluation of the block => Expected: resolved with whatever you entered to return in the debugger"
Promises with whenRejected:/ifRejected: callbacks would no longer swallow errors, and would only be rejected when the user aborts in the debuggers, or if the future execution catches errors by itself and converts them to rejected promises, so the future promise will also be rejected. This could pose a compatibility problem for existing code.
promise := (1 future / 0) then: [:result | result + 3] ifRejected: [:reason | #cancelled]. "<= inspect it => Actual: resolved with #cancelled immediately. Expected with my proposed changes: it would first show the ZeroDivide debugger, which you can abandon to resolve with #cancelled, or proceed to a MessageNotUnderstood +. If you abandon the MNU, the promise would be rejected with the MNU, not #cancelled, in accordance with the Promises/A+ spec."
How to get back a catch-all->reject-immediately future under these circumstances:
promise := [[1 / 0] on: Error do: [:e | e return: (Promise new rejectWith: e)]] future value. promise := [1 future + 1 then: [:n | [n / 0] on: Error do: [:e | e return: (Promise new rejectWith: e)]] future value.
We could also introduce a convenience constructor for immediately-rejected promises like in JavaScript: Promise rejected: e. Or a convenience exception handler: [...] rejectOn: Error. Or [...] rejectIfCurtailed (the fullfill/then methods would probably use this as well).
What do you think?
As Tom Beckmann has already suggested in the last thread on the topic [1], I could also use a custom class of Promise to get just the behavior I want. But then I cannot solve it for the use of #future. At least not without patching something about the compiler in my package preamble... ;-)
[1] http://lists.squeakfoundation.org/pipermail/squeak-dev/2020-April/208546.htm... [2] https://promisesaplus.com/ Kind regards, Jakob