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
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
Hi Jakob,
I changed my code a little bit to open a Debugger. Yet I still have two issues.
On 6/21/20 5:53 PM, Robert Withers wrote:
You can load the following and run the tests.
Installer ss project: 'Cryptography'; install: 'PromisesLocal'.
The latest PromisesLocal-rww.2.mcz has the changes. You can load with the above Installer doIt.
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.
I removed catch-all error handling from the PriorityVat>>#processSends. I then added to the error handling in EventualMessageSend>>#value to open a debugger after smashing the resolver.
value
| value | [value := receiver perform: selector withArguments: (self collectArguments: arguments) inSuperclass: receiver class. self resolver notNil ifTrue: [ self resolver resolve: value ] ] on: Exception do: [:ex | self resolver notNil ifTrue: [self resolver smash: ex]. Processor activeProcess signalException: ex].
I have a couple of issues with this:
- When I close the debugger, the EventualProcess with the stack that ended up in this exception is terminated. I would need this process to restart and continue, after clearing the stack from this exception. I am unsure how to make that happen. Any suggestions are MOST WELCOME! - The stack in the Debugger is not opened on the stack frame that causes the failure. I would like to rewind the stack to the bolded line, where the exception is produced, in this case it would be the method: ZeroDivide(Exception)>>signal. Is this not where the Debugger should be opened? Again, I am unsure how to do this and all suggestions are MOST WELCOME!
--- The full stack --- EventualProcess(Process)>>signalException: [] in EventualMessageSend>>value FullBlockClosure(BlockClosure)>>cull: [] in Context>>handleSignal: FullBlockClosure(BlockClosure)>>ensure: Context>>handleSignal: ZeroDivide(Exception)>>signal SmallInteger>>/ [] in EventualMessageSend>>value FullBlockClosure(BlockClosure)>>on:do: EventualMessageSend>>value PriorityVat>>processSends [] in EventualProcess>>setupContext
Kindly, Rabbit
Hi Jakob,
On 6/23/20 8:40 AM, Robert Withers wrote:
I removed catch-all error handling from the PriorityVat>>#processSends. I then added to the error handling in EventualMessageSend>>#value to open a debugger after smashing the resolver.
I forgot to specify that I use the following code to test these changes:
1 eventual / 0
After running this and seeing the Debugger and closing this Debugger, you have to restart the localVat to refresh the event loop, with the following code:
PriorityVat class>>#clearLocalVat
K, r
Hi Jakob,
I have new ideas, in thinking on this problem, one per point I made.
On 6/23/20 8:40 AM, Robert Withers wrote:
I removed catch-all error handling from the PriorityVat>>#processSends. I then added to the error handling in EventualMessageSend>>#value to open a debugger after smashing the resolver.
value
| value | [value := receiver perform: selector withArguments: (self collectArguments: arguments) inSuperclass: receiver class. self resolver notNil ifTrue: [ self resolver resolve: value ] ] on: Exception do: [:ex | self resolver notNil ifTrue: [self resolver smash: ex]. Processor activeProcess signalException: ex].
I have a couple of issues with this:
- When I close the debugger, the EventualProcess with the stack that ended up in this exception is terminated. I would need this process to restart and continue, after clearing the stack from this exception. I am unsure how to make that happen. Any suggestions are MOST WELCOME!
- I would like to copy the stack to the side and restart the Vat's eventLoop right away so that it does not deadlock the event loop process. And so continue processing message sends.
The stack in the Debugger is not opened on the stack frame that causes the failure. I would like to rewind the stack to the bolded line, where the exception is produced, in this case it would be the method: ZeroDivide(Exception)>>signal. Is this not where the Debugger should be opened? Again, I am unsure how to do this and all suggestions are MOST WELCOME!
--- The full stack --- EventualProcess(Process)>>signalException: [] in EventualMessageSend>>value FullBlockClosure(BlockClosure)>>cull: [] in Context>>handleSignal: FullBlockClosure(BlockClosure)>>ensure: Context>>handleSignal: ZeroDivide(Exception)>>signal SmallInteger>>/ [] in EventualMessageSend>>value FullBlockClosure(BlockClosure)>>on:do: EventualMessageSend>>value PriorityVat>>processSends [] in EventualProcess>>setupContext
With the stack copied to the side, I want to massage the stack so the top frame is the Exception's frame where the Exception gets signaled. Then I would like to remove the frames below the frame that causes the Exception signal or perhaps the stack down to the EventualMessageSend>>#value. This is the stack I envision opening in a Debugger:
ZeroDivide(Exception)>>signal SmallInteger>>/ [] in EventualMessageSend>>value FullBlockClosure(BlockClosure)>>on:do: EventualMessageSend>>value
What do you think? How can I do each of these?
- Copy stack
- Restart eventLoop
- Prune stack
- Open Debugger
K, r
Hi Jakob,
I was wondering if you had any thoughts regarding PromisesLocal. Have you had a chance to look at this project? I suppose the promise protocol could be extended to capture the protocol specified in Promises/A+, but the same behavior is there (#whenResolved:). I added the ability to open a debugger when an exception gets thrown. Unfortunately, for now this exception blocks the event loop Process. I have another email on how to unblock the event loop Process, yet still open an appropriate Debugger Notifier.
I would welcome any feedback you may have. I am working on the Remote version (PromisesRemote). You can load PromisesLocal and run the tests.
Installer ss project: 'Cryptography'; install: 'PromisesLocal'.
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
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
Here's my images:
On 7/18/20 12:35 PM, Robert Withers wrote:
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
Hi Jakob,
I might be a little bit late to the party, but this is indeed a serious issue with promises that we should fix. Most importantly, you must never catch Exceptions instead of Errors but the current implementation does so. At the moment, we cannot even access any source files from a promise due to CurrentReadOnlySourceFiles. And not removing interactivity from the user during promises is also very worthwhile. Is it true that nothing newer has happened on this issue after this message?
Based on what I can tell your proposal sounds very well. I agree with all your expectations. Proceeding/returning from a debugger is the exact equivalent of resuming an exception (unchecked). However, I have to note that I'm not familiar with using promises in Squeak, not to mention promise implementations in general, and I'm also not familiar with #outer. You might want to double-check whether its semantics have changed since your last message.
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).
I don't understand the context of this note, but speaking in terms of Squeak, I think we have indeed defined both behaviors: Non-local exits from unwind contexts are usually disrecommended but can be used to cancel a termination ("zombie process") or a regular return (from a method or an exception). #resume: and #return: can be sent at any place to an exception, thinking of the exception like a "RemoteGoto" object.
One additional note: What should happen if you terminate the active process from a promise (or do the same from the outside)? Should this also reject this promise, or should it be scheduled later again? However, this is likely a different story, so let's not add another dependency to this issue.
Best, Christoph
--- Sent from Squeak Inbox Talk
On 2020-06-21T22:57:10+02:00, forums.jakob@resfarm.de 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
Hi Christoph,
Am Fr., 18. Aug. 2023 um 21:06 Uhr schrieb < christoph.thiede@student.hpi.uni-potsdam.de>:
I might be a little bit late to the party,
Well, if the party is still going on after three years, it must be a good one... ;-)
Is it true that nothing newer has happened on this issue after this message?
Not entirely sure, but if you still experience the problem, it apparently has not changed. I also still have #halt-s in the Promise methods in my working images so that I can deal with errors.
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).
I don't understand the context of this note, but speaking in terms of Squeak,
To implement it "Squeak-conformant" is probably enough. In 2020 it was not long ago that I worked with VA Smalltalk and I was regularly annoyed by non-portable (not "ANSI-conformant") code. The chances that somebody will want to use the Squeak implementation of Promises in another Smalltalk are probably quite slim. Nevertheless, I have made it a habit to always use return: or resume: in handler blocks.
One additional note: What should happen if you terminate the active process from a promise (or do the same from the outside)? Should this also reject this promise, or should it be scheduled later again? However, this is likely a different story, so let's not add another dependency to this issue.
Agreed. Intuitively it sounds right to eventually reject something that
was aborted. But since we do have multi-processing/-threading, the Promise might yet be fulfilled by another process. Or not... depends on the application. We might not be able to find a general answer that suits all use cases. You could also think about rejecting unfulfilled promises when they are garbage-collected... but JavaScript does not do that either, as far as I know.
Maybe the answer is: just don't do that in your application. :-)
Kind regards, Jakob
Hi Jakob,
alright, so would you like to upload a patch to the inbox? Should anyone else review it? :-)
Best, Christoph
--- Sent from Squeak Inbox Talk
On 2023-08-19T11:44:55+02:00, jakres+squeak@gmail.com wrote:
Hi Christoph,
Am Fr., 18. Aug. 2023 um 21:06 Uhr schrieb < christoph.thiede(a)student.hpi.uni-potsdam.de>:
I might be a little bit late to the party,
Well, if the party is still going on after three years, it must be a good one... ;-)
Is it true that nothing newer has happened on this issue after this message?
Not entirely sure, but if you still experience the problem, it apparently has not changed. I also still have #halt-s in the Promise methods in my working images so that I can deal with errors.
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).
I don't understand the context of this note, but speaking in terms of Squeak,
To implement it "Squeak-conformant" is probably enough. In 2020 it was not long ago that I worked with VA Smalltalk and I was regularly annoyed by non-portable (not "ANSI-conformant") code. The chances that somebody will want to use the Squeak implementation of Promises in another Smalltalk are probably quite slim. Nevertheless, I have made it a habit to always use return: or resume: in handler blocks.
One additional note: What should happen if you terminate the active process from a promise (or do the same from the outside)? Should this also reject this promise, or should it be scheduled later again? However, this is likely a different story, so let's not add another dependency to this issue.
Agreed. Intuitively it sounds right to eventually reject something that
was aborted. But since we do have multi-processing/-threading, the Promise might yet be fulfilled by another process. Or not... depends on the application. We might not be able to find a general answer that suits all use cases. You could also think about rejecting unfulfilled promises when they are garbage-collected... but JavaScript does not do that either, as far as I know.
Maybe the answer is: just don't do that in your application. :-)
Kind regards, Jakob
Hi Christoph,
It takes me quite a while to get back into this, and also to validate my message from three years ago. Here are my current conclusions:
I must distinguish better between the use of #future and promise chaining with #then:ifRejected:. While my #halt-s in then:ifRejected: might be nice at development time, they do not make sense in general, where ifRejected blocks are actually the error handlers, much like on:do: handlers. When I complained about the fact that promises swallow errors, it is inconsistent on my behalf to not complain about the fact that on:do: handlers can swallow errors, too... On the other hand, we could and probably should adapt future sends—everything that comes between #future and a #then: or #wait.
So I must rectify something from three years ago:
Am So., 21. Juni 2020 um 22:57 Uhr schrieb Jakob Reschke < forums.jakob@resfarm.de>:
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."
Promises with when/ifRejected blocks must "swallow" errors that happened in other whenResolved or whenRejected blocks because that is how this model works.
But nevertheless we should show the ZeroDivide in a debugger. The continuation to evaluate "1 / 0" is not inside of a promise callback block, hence the rules to turn the error into a rejection do not apply. Appropriately, the evaluation happens in #fulfillWith:passErrors: rather than in the blocks of #then:ifRejected: in the Promise implementation, so different on:do: handlers apply.
The example is mostly still alright, except for a mistake at the end:
promise := (1 future / 0) then: [:result | result + 3] ifRejected: [:reason | #cancelled]. "<= inspect it => Actual: resolved with #cancelled immediately. Preferable: show debugger on ZeroDivide. Expected on abandon: resolve with #cancelled. Expected on proceed: new debugger "ZeroDivide does not understand +". Expected on abandon: resolve with #cancelled. Expected on proceed: the usual MNU loop..."
The inner promise that is the result of `(1 future / 0)` is actually rejected on abandon, but the `ifRejected: [:reason | #cancelled]` always gets the outer promise resolved.
Kind regards, Jakob
Am Sa., 19. Aug. 2023 um 14:40 Uhr schrieb < christoph.thiede@student.hpi.uni-potsdam.de>:
Hi Jakob,
alright, so would you like to upload a patch to the inbox? Should anyone else review it? :-)
Best, Christoph
*Sent from **Squeak Inbox Talk https://github.com/hpi-swa-lab/squeak-inbox-talk*
We also have at least two more differences between Squeak promises and ECMAScript promises at the moment:
Promises/A+ demands that the onFulfilled and onRejected callbacks are called with a "clean" stack. [1] First, the Squeak implementation does not do that. It evaluates the callback blocks immediately when the promise changes state, and thus in the exception environment of whatever caused that. Second, this implies that a Promises/A+-conforming application cannot rely on try..catch/on:do: exception handlers anyway (not around the send of #then:, but also not around the send of #resolveWith:). Thus, it would be somewhat futile to expect exceptions to be signalled out of the promise callback blocks. The repeated stack unwinding, or ubiquitous asynchrony, also means that promises as in Promises/A+ are fundamentally not compatible with the "error -> edit any method on stack -> restart" debugging style that we are used to in Smalltalk. At least not without a debugger that is specialized in promises.
ECMAScript specifies that if no onRejected callback is provided to .then(), or rather if the onRejected argument is not callable, then rejecting the promise will throw the reason for the rejection. In Squeak terms: `p then: [:v | ...]. "===" p then: [:v | ...] ifRejected: [:e | e signal]`. In ECMAScript 6 this is specified almost straightforward [2], whereas in the current standard it seems to be more complicated, but with the same end result of throwing the error. In practice, an uncaught exception in an ECMAScript promise chain may end up in the console. The Squeak equivalent would be to show a debugger (with an unrevealing stack trace if we would take the asynchrony requirement seriously) or to pop up a Transcript message. But in Squeak there is no default rejected handler that signals the error, so currently just nothing happens. Well, the promise gets rejected, but there is no visible reaction to an unhandled error.
Taking that into account, we could remove the ifRejected: block from the previous example and make further expectations:
promise := (1 future / 0) then: [:result | result + 3]. "<= inspect it => Actual: rejected with ZeroDivide. Preferable: show debugger on ZeroDivide. Expected on abandon: announce that the promise was rejected with an uncaught error. Expected on proceed: new debugger "ZeroDivide does not understand +". Expected on abandon: announce as above, but referring to the MNU"
The "announcement" could be yet another debugger, the Transcript, or something customizable. The debugger might not always be the right choice, let's have another example:
p := [Promise new rejectWith: 0] future value then: [:result | result + 3]. "...send some further messages..." p whenRejected: [].
The current ECMAScript standard made me aware of this race condition [3]. If the example above is not run in the UI process, p might be rejected before the empty handler gets attached or afterwards. If it is rejected afterwards, a debugger must not appear. If it is rejected before, we might show a debugger, only to regret it a few microseconds later.
[1] https://promisesaplus.com/#point-34 [2] https://262.ecma-international.org/6.0/#sec-performpromisethen [3] https://262.ecma-international.org/#sec-host-promise-rejection-tracker (see Note 1)
Am So., 20. Aug. 2023 um 23:44 Uhr schrieb Jakob Reschke < jakres+squeak@gmail.com>:
Hi Christoph,
It takes me quite a while to get back into this, and also to validate my message from three years ago. Here are my current conclusions:
I must distinguish better between the use of #future and promise chaining with #then:ifRejected:. While my #halt-s in then:ifRejected: might be nice at development time, they do not make sense in general, where ifRejected blocks are actually the error handlers, much like on:do: handlers. When I complained about the fact that promises swallow errors, it is inconsistent on my behalf to not complain about the fact that on:do: handlers can swallow errors, too... On the other hand, we could and probably should adapt future sends—everything that comes between #future and a #then: or #wait.
So I must rectify something from three years ago:
Am So., 21. Juni 2020 um 22:57 Uhr schrieb Jakob Reschke < forums.jakob@resfarm.de>:
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."
Promises with when/ifRejected blocks must "swallow" errors that happened in other whenResolved or whenRejected blocks because that is how this model works.
But nevertheless we should show the ZeroDivide in a debugger. The continuation to evaluate "1 / 0" is not inside of a promise callback block, hence the rules to turn the error into a rejection do not apply. Appropriately, the evaluation happens in #fulfillWith:passErrors: rather than in the blocks of #then:ifRejected: in the Promise implementation, so different on:do: handlers apply.
The example is mostly still alright, except for a mistake at the end:
promise := (1 future / 0) then: [:result | result + 3] ifRejected: [:reason | #cancelled]. "<= inspect it => Actual: resolved with #cancelled immediately. Preferable: show debugger on ZeroDivide. Expected on abandon: resolve with #cancelled. Expected on proceed: new debugger "ZeroDivide does not understand +". Expected on abandon: resolve with #cancelled. Expected on proceed: the usual MNU loop..."
The inner promise that is the result of `(1 future / 0)` is actually rejected on abandon, but the `ifRejected: [:reason | #cancelled]` always gets the outer promise resolved.
Kind regards, Jakob
Am Sa., 19. Aug. 2023 um 14:40 Uhr schrieb < christoph.thiede@student.hpi.uni-potsdam.de>:
Hi Jakob,
alright, so would you like to upload a patch to the inbox? Should anyone else review it? :-)
Best, Christoph
*Sent from **Squeak Inbox Talk https://github.com/hpi-swa-lab/squeak-inbox-talk*
Hi Christoph, and everyone else who is still interested,
Who would have thought: it is not so easy.
The existing test cases already look quite as desired: future sends are expected to signal errors. But only if there is no rejection callback attached to their promise! Unfortunately, then:ifRejected: does register a rejection callback to reject the newly derived promise that gets returned from then:ifRejected:, even if there is eventually no rejection callback on that derived promise. For added fun, attaching callbacks to a future promise suffers from the race condition mentioned in my last message if the future send is not registered from within the UI process.
You can attach multiple rejection callbacks to a promise. Let's say there are two of them, and they both signal an exception, and both do not get caught by further callbacks. Then we should actually get two uncaught errors, so maybe two debuggers. But if we just iterate over the handlers one after another, as currently implemented, and encounter a non-resumable exception in the first callback, then the second callback would never be run, which violates the contract. Strange situation: we don't want to handle the errors, so that the debuggers can show up, but we must handle them to ensure that further callbacks are not impeded.
We could schedule all callback blocks to run asynchronously, as required in ECMAScript anyway. It might have repercussions on existing applications though.
If we make the callbacks asynchronous by using future sends, they will actually all run in the UI process. There goes the multi-process concurrency out of the window, and the GUI responsiveness if you have longer callbacks. If we fork one process per callback instead, it has a chance to break existing applications that use promises only from the UI process, or generally code that is not thread-safe.
Finally #wait enters the game. It uses semaphores to wait until another process resolves the promise. Guess what happens to the test cases if I schedule all callbacks using futureDo:at:args:?* All waits time out. The test cases run from the UI process, and so do the deferred UI messages to run the callbacks, so the UI process (test case) waits for itself (deferred message).
I am trying a heuristic to use futureDo:at:args: to schedule the callbacks if the current process is the UI process, and use fork otherwise. I don't like the heuristic because I don't fully trust it yet, and of course it couples the Promise implementation more closely to Morphic. Overall, more of the Promise test cases will become dependent on Morphic to do one or more world cycles...
Kind regards, Jakob
*I tried using just #future at first, but got endlessly spinning loops via the deferred messages queue, generating one more promise to resolve on every next step. At least it didn't freeze the GUI like that, just made it slow.
Am So., 20. Aug. 2023 um 23:50 Uhr schrieb Jakob Reschke < jakres+squeak@gmail.com>:
We also have at least two more differences between Squeak promises and ECMAScript promises at the moment:
Promises/A+ demands that the onFulfilled and onRejected callbacks are called with a "clean" stack. [1] First, the Squeak implementation does not do that. It evaluates the callback blocks immediately when the promise changes state, and thus in the exception environment of whatever caused that. Second, this implies that a Promises/A+-conforming application cannot rely on try..catch/on:do: exception handlers anyway (not around the send of #then:, but also not around the send of #resolveWith:). Thus, it would be somewhat futile to expect exceptions to be signalled out of the promise callback blocks. The repeated stack unwinding, or ubiquitous asynchrony, also means that promises as in Promises/A+ are fundamentally not compatible with the "error -> edit any method on stack -> restart" debugging style that we are used to in Smalltalk. At least not without a debugger that is specialized in promises.
ECMAScript specifies that if no onRejected callback is provided to .then(), or rather if the onRejected argument is not callable, then rejecting the promise will throw the reason for the rejection. In Squeak terms: `p then: [:v | ...]. "===" p then: [:v | ...] ifRejected: [:e | e signal]`. In ECMAScript 6 this is specified almost straightforward [2], whereas in the current standard it seems to be more complicated, but with the same end result of throwing the error. In practice, an uncaught exception in an ECMAScript promise chain may end up in the console. The Squeak equivalent would be to show a debugger (with an unrevealing stack trace if we would take the asynchrony requirement seriously) or to pop up a Transcript message. But in Squeak there is no default rejected handler that signals the error, so currently just nothing happens. Well, the promise gets rejected, but there is no visible reaction to an unhandled error.
Taking that into account, we could remove the ifRejected: block from the previous example and make further expectations:
promise := (1 future / 0) then: [:result | result + 3]. "<= inspect it => Actual: rejected with ZeroDivide. Preferable: show debugger on ZeroDivide. Expected on abandon: announce that the promise was rejected with an
uncaught error. Expected on proceed: new debugger "ZeroDivide does not understand +". Expected on abandon: announce as above, but referring to the MNU"
The "announcement" could be yet another debugger, the Transcript, or something customizable. The debugger might not always be the right choice, let's have another example:
p := [Promise new rejectWith: 0] future value then: [:result | result
- 3]. "...send some further messages..." p whenRejected: [].
The current ECMAScript standard made me aware of this race condition [3]. If the example above is not run in the UI process, p might be rejected before the empty handler gets attached or afterwards. If it is rejected afterwards, a debugger must not appear. If it is rejected before, we might show a debugger, only to regret it a few microseconds later.
[1] https://promisesaplus.com/#point-34 [2] https://262.ecma-international.org/6.0/#sec-performpromisethen [3] https://262.ecma-international.org/#sec-host-promise-rejection-tracker (see Note 1)
Am So., 20. Aug. 2023 um 23:44 Uhr schrieb Jakob Reschke < jakres+squeak@gmail.com>:
Hi Christoph,
It takes me quite a while to get back into this, and also to validate my message from three years ago. Here are my current conclusions:
I must distinguish better between the use of #future and promise chaining with #then:ifRejected:. While my #halt-s in then:ifRejected: might be nice at development time, they do not make sense in general, where ifRejected blocks are actually the error handlers, much like on:do: handlers. When I complained about the fact that promises swallow errors, it is inconsistent on my behalf to not complain about the fact that on:do: handlers can swallow errors, too... On the other hand, we could and probably should adapt future sends—everything that comes between #future and a #then: or #wait.
So I must rectify something from three years ago:
Am So., 21. Juni 2020 um 22:57 Uhr schrieb Jakob Reschke < forums.jakob@resfarm.de>:
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."
Promises with when/ifRejected blocks must "swallow" errors that happened in other whenResolved or whenRejected blocks because that is how this model works.
But nevertheless we should show the ZeroDivide in a debugger. The continuation to evaluate "1 / 0" is not inside of a promise callback block, hence the rules to turn the error into a rejection do not apply. Appropriately, the evaluation happens in #fulfillWith:passErrors: rather than in the blocks of #then:ifRejected: in the Promise implementation, so different on:do: handlers apply.
The example is mostly still alright, except for a mistake at the end:
promise := (1 future / 0) then: [:result | result + 3] ifRejected: [:reason | #cancelled]. "<= inspect it => Actual: resolved with #cancelled immediately. Preferable: show debugger on ZeroDivide. Expected on abandon: resolve with #cancelled. Expected on proceed: new debugger "ZeroDivide does not understand +". Expected on abandon: resolve with #cancelled. Expected on proceed: the usual MNU loop..."
The inner promise that is the result of `(1 future / 0)` is actually rejected on abandon, but the `ifRejected: [:reason | #cancelled]` always gets the outer promise resolved.
Kind regards, Jakob
Am Sa., 19. Aug. 2023 um 14:40 Uhr schrieb < christoph.thiede@student.hpi.uni-potsdam.de>:
Hi Jakob,
alright, so would you like to upload a patch to the inbox? Should anyone else review it? :-)
Best, Christoph
*Sent from **Squeak Inbox Talk https://github.com/hpi-swa-lab/squeak-inbox-talk*
Am Fr., 18. Aug. 2023 um 21:06 Uhr schrieb < christoph.thiede@student.hpi.uni-potsdam.de>:
Most importantly, you must never catch Exceptions instead of Errors but the current implementation does so. At the moment, we cannot even access any source files from a promise due to CurrentReadOnlySourceFiles.
Can you please provide a concrete example for this? Maybe we can at least find a solution for this aspect quicker than for all the rest.
Hi Jakob,
short answer only at the current time, but thank you for looking into this!
Can you please provide a concrete example for this? Maybe we can at least find a solution for this aspect quicker than for all the rest.
Object future comment ifRejected: #halt.
A workaround might be only catching Error or Error , Warning in Promise>>#fulfillWith:passErrors: but not Exception, I guess ...
Best,
Christoph
________________________________ Von: Jakob Reschke jakres+squeak@gmail.com Gesendet: Mittwoch, 23. August 2023 23:23:27 An: The general-purpose Squeak developers list Betreff: [squeak-dev] Re: On the rejection of Promises due to errors
Am Fr., 18. Aug. 2023 um 21:06 Uhr schrieb <christoph.thiede@student.hpi.uni-potsdam.demailto:christoph.thiede@student.hpi.uni-potsdam.de>: Most importantly, you must never catch Exceptions instead of Errors but the current implementation does so. At the moment, we cannot even access any source files from a promise due to CurrentReadOnlySourceFiles.
Can you please provide a concrete example for this? Maybe we can at least find a solution for this aspect quicker than for all the rest.
Thanks so much for this discussion! I am deep in my other projects, at this time. I will definitely add this test to my implementation of Promises. I am not handling them very well. I regressed. I used to be able to Halt inside an eventual send stack, but not at this time. I believe my Halts are silently consumed. Not a good situation. It is on my list. More in a few...
• rabbit ❤️🔥🐰
On 8/23/23 17:20, Jakob Reschke wrote:
Hi Christoph, and everyone else who is still interested,
Who would have thought: it is not so easy.
The existing test cases already look quite as desired: future sends are expected to signal errors. But only if there is no rejection callback attached to their promise! Unfortunately, then:ifRejected: does register a rejection callback to reject the newly derived promise that gets returned from then:ifRejected:, even if there is eventually no rejection callback on that derived promise. For added fun, attaching callbacks to a future promise suffers from the race condition mentioned in my last message if the future send is not registered from within the UI process.
You can attach multiple rejection callbacks to a promise. Let's say there are two of them, and they both signal an exception, and both do not get caught by further callbacks. Then we should actually get two uncaught errors, so maybe two debuggers. But if we just iterate over the handlers one after another, as currently implemented, and encounter a non-resumable exception in the first callback, then the second callback would never be run, which violates the contract. Strange situation: we don't want to handle the errors, so that the debuggers can show up, but we must handle them to ensure that further callbacks are not impeded.
We could schedule all callback blocks to run asynchronously, as required in ECMAScript anyway. It might have repercussions on existing applications though.
If we make the callbacks asynchronous by using future sends, they will actually all run in the UI process. There goes the multi-process concurrency out of the window, and the GUI responsiveness if you have longer callbacks. If we fork one process per callback instead, it has a chance to break existing applications that use promises only from the UI process, or generally code that is not thread-safe.
Finally #wait enters the game. It uses semaphores to wait until another process resolves the promise. Guess what happens to the test cases if I schedule all callbacks using futureDo:at:args:?* All waits time out. The test cases run from the UI process, and so do the deferred UI messages to run the callbacks, so the UI process (test case) waits for itself (deferred message).
I am trying a heuristic to use futureDo:at:args: to schedule the callbacks if the current process is the UI process, and use fork otherwise. I don't like the heuristic because I don't fully trust it yet, and of course it couples the Promise implementation more closely to Morphic. Overall, more of the Promise test cases will become dependent on Morphic to do one or more world cycles...
Kind regards, Jakob
*I tried using just #future at first, but got endlessly spinning loops via the deferred messages queue, generating one more promise to resolve on every next step. At least it didn't freeze the GUI like that, just made it slow.
Am So., 20. Aug. 2023 um 23:50 Uhr schrieb Jakob Reschke <[jakres+squeak@gmail.com](mailto:jakres%2Bsqueak@gmail.com)>:
We also have at least two more differences between Squeak promises and ECMAScript promises at the moment:
Promises/A+ demands that the onFulfilled and onRejected callbacks are called with a "clean" stack. [1] First, the Squeak implementation does not do that. It evaluates the callback blocks immediately when the promise changes state, and thus in the exception environment of whatever caused that. Second, this implies that a Promises/A+-conforming application cannot rely on try..catch/on:do: exception handlers anyway (not around the send of #then:, but also not around the send of #resolveWith:). Thus, it would be somewhat futile to expect exceptions to be signalled out of the promise callback blocks. The repeated stack unwinding, or ubiquitous asynchrony, also means that promises as in Promises/A+ are fundamentally not compatible with the "error -> edit any method on stack -> restart" debugging style that we are used to in Smalltalk. At least not without a debugger that is specialized in promises.
ECMAScript specifies that if no onRejected callback is provided to .then(), or rather if the onRejected argument is not callable, then rejecting the promise will throw the reason for the rejection. In Squeak terms: `p then: [:v | ...]. "===" p then: [:v | ...] ifRejected: [:e | e signal]`. In ECMAScript 6 this is specified almost straightforward [2], whereas in the current standard it seems to be more complicated, but with the same end result of throwing the error. In practice, an uncaught exception in an ECMAScript promise chain may end up in the console. The Squeak equivalent would be to show a debugger (with an unrevealing stack trace if we would take the asynchrony requirement seriously) or to pop up a Transcript message. But in Squeak there is no default rejected handler that signals the error, so currently just nothing happens. Well, the promise gets rejected, but there is no visible reaction to an unhandled error.
Taking that into account, we could remove the ifRejected: block from the previous example and make further expectations:
promise := (1 future / 0) then: [:result | result + 3]. "<= inspect it => Actual: rejected with ZeroDivide. Preferable: show debugger on ZeroDivide. Expected on abandon: announce that the promise was rejected with an uncaught error. Expected on proceed: new debugger "ZeroDivide does not understand +". Expected on abandon: announce as above, but referring to the MNU"
The "announcement" could be yet another debugger, the Transcript, or something customizable. The debugger might not always be the right choice, let's have another example:
p := [Promise new rejectWith: 0] future value then: [:result | result + 3]. "...send some further messages..." p whenRejected: [].
The current ECMAScript standard made me aware of this race condition [3]. If the example above is not run in the UI process, p might be rejected before the empty handler gets attached or afterwards. If it is rejected afterwards, a debugger must not appear. If it is rejected before, we might show a debugger, only to regret it a few microseconds later.
[1] https://promisesaplus.com/#point-34 [2] https://262.ecma-international.org/6.0/#sec-performpromisethen [3] https://262.ecma-international.org/#sec-host-promise-rejection-tracker (see Note 1)
Am So., 20. Aug. 2023 um 23:44 Uhr schrieb Jakob Reschke <[jakres+squeak@gmail.com](mailto:jakres%2Bsqueak@gmail.com)>:
Hi Christoph,
It takes me quite a while to get back into this, and also to validate my message from three years ago. Here are my current conclusions:
I must distinguish better between the use of #future and promise chaining with #then:ifRejected:. While my #halt-s in then:ifRejected: might be nice at development time, they do not make sense in general, where ifRejected blocks are actually the error handlers, much like on:do: handlers. When I complained about the fact that promises swallow errors, it is inconsistent on my behalf to not complain about the fact that on:do: handlers can swallow errors, too... On the other hand, we could and probably should adapt future sends—everything that comes between #future and a #then: or #wait.
So I must rectify something from three years ago:
Am So., 21. Juni 2020 um 22:57 Uhr schrieb Jakob Reschke forums.jakob@resfarm.de:
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."
Promises with when/ifRejected blocks must "swallow" errors that happened in other whenResolved or whenRejected blocks because that is how this model works.
But nevertheless we should show the ZeroDivide in a debugger. The continuation to evaluate "1 / 0" is not inside of a promise callback block, hence the rules to turn the error into a rejection do not apply. Appropriately, the evaluation happens in #fulfillWith:passErrors: rather than in the blocks of #then:ifRejected: in the Promise implementation, so different on:do: handlers apply.
The example is mostly still alright, except for a mistake at the end:
promise := (1 future / 0) then: [:result | result + 3] ifRejected: [:reason | #cancelled]. "<= inspect it => Actual: resolved with #cancelled immediately. Preferable: show debugger on ZeroDivide. Expected on abandon: resolve with #cancelled. Expected on proceed: new debugger "ZeroDivide does not understand +". Expected on abandon: resolve with #cancelled. Expected on proceed: the usual MNU loop..."
The inner promise that is the result of `(1 future / 0)` is actually rejected on abandon, but the `ifRejected: [:reason | #cancelled]` always gets the outer promise resolved.
Kind regards, Jakob
Am Sa., 19. Aug. 2023 um 14:40 Uhr schrieb christoph.thiede@student.hpi.uni-potsdam.de:
Hi Jakob,
alright, so would you like to upload a patch to the inbox? Should anyone else review it? :-)
Best, Christoph
---Sent from [Squeak Inbox Talk](https://github.com/hpi-swa-lab/squeak-inbox-talk)
-- ••• rabbit ❤️🔥🐰
Hi Christoph,
I would have liked to use #isResumable rather than Exception vs Error to determine whether something should be caught to reject the promise, or to let it pass. Unfortunately, ZeroDivide is declared as resumable... so future divisions by zero would not reject promises as they used to.
Changing fulfillWith:passErrors: to only catch Error instead of Exception lets all tests still pass on my side. It also makes this new one (in FutureTest) green:
testNotificationsAreNotCaught "The deferred evaluation below uses CurrentReadOnlySourceFiles. The error handling of the future promise must not interfere with this Exception." | p1 | p1 := Object future comment. self waitUntil: [p1 isResolved] orCycleCount: 1. self assert: p1 isResolved.
You could change the fulfill method accordingly and see whether you encounter any strange behavior during the next few weeks. Or have you already done it?
In general, the change seems right to me, as there are many examples of Exceptions that are not supposed to be caught (like IllegalResumeAttempt, TestFailure), or are not meant to abort any control flow (ProgressNotification, MCNoChangesException).
Kind regards, Jakob
Am Do., 24. Aug. 2023 um 03:21 Uhr schrieb Thiede, Christoph Christoph.Thiede@student.hpi.uni-potsdam.de:
Hi Jakob,
short answer only at the current time, but thank you for looking into this!
Can you please provide a concrete example for this? Maybe we can at least find a solution for this aspect quicker than for all the rest.
Object future comment ifRejected: #halt.
A workaround might be only catching Error or Error , Warning in Promise>>#fulfillWith:passErrors: but not Exception, I guess ...
Best,
Christoph
Von: Jakob Reschke jakres+squeak@gmail.com Gesendet: Mittwoch, 23. August 2023 23:23:27 An: The general-purpose Squeak developers list Betreff: [squeak-dev] Re: On the rejection of Promises due to errors
Am Fr., 18. Aug. 2023 um 21:06 Uhr schrieb christoph.thiede@student.hpi.uni-potsdam.de:
Most importantly, you must never catch Exceptions instead of Errors but the current implementation does so. At the moment, we cannot even access any source files from a promise due to CurrentReadOnlySourceFiles.
Can you please provide a concrete example for this? Maybe we can at least find a solution for this aspect quicker than for all the rest.
Hi Jakob,
I don't think #isResumable would be the right hook for this. Currently, the canonical way in the EHS to mark an exception as erroneous or fatal IMO is to subclass from Error. For anything better, we would have to simulate what would happen when resuming the exception ... and there we go the way to the ultimate solution you proposed at the beginning at this thread again. Catching errors in #fulfillWith:passErrors: for now sounds like a good workaroud to me. :-)
You could change the fulfill method accordingly and see whether you encounter any strange behavior during the next few weeks. Or have you already done it?
In one of my image, this change has been active for 1.5 months ... corresponding to a couple of full-time Squeak days.
So, from my side, please upload that patch. :-)
Best, Christoph
--- Sent from Squeak Inbox Talk
On 2023-08-26T18:30:24+02:00, jakres+squeak@gmail.com wrote:
Hi Christoph,
I would have liked to use #isResumable rather than Exception vs Error to determine whether something should be caught to reject the promise, or to let it pass. Unfortunately, ZeroDivide is declared as resumable... so future divisions by zero would not reject promises as they used to.
Changing fulfillWith:passErrors: to only catch Error instead of Exception lets all tests still pass on my side. It also makes this new one (in FutureTest) green:
testNotificationsAreNotCaught "The deferred evaluation below uses
CurrentReadOnlySourceFiles. The error handling of the future promise must not interfere with this Exception." | p1 | p1 := Object future comment. self waitUntil: [p1 isResolved] orCycleCount: 1. self assert: p1 isResolved.
You could change the fulfill method accordingly and see whether you encounter any strange behavior during the next few weeks. Or have you already done it?
In general, the change seems right to me, as there are many examples of Exceptions that are not supposed to be caught (like IllegalResumeAttempt, TestFailure), or are not meant to abort any control flow (ProgressNotification, MCNoChangesException).
Kind regards, Jakob
Am Do., 24. Aug. 2023 um 03:21 Uhr schrieb Thiede, Christoph <Christoph.Thiede(a)student.hpi.uni-potsdam.de>:
Hi Jakob,
short answer only at the current time, but thank you for looking into this!
Can you please provide a concrete example for this? Maybe we can at least find a solution for this aspect quicker than for all the rest.
Object future comment ifRejected: #halt.
A workaround might be only catching Error or Error , Warning in Promise>>#fulfillWith:passErrors: but not Exception, I guess ...
Best,
Christoph
Von: Jakob Reschke <jakres+squeak(a)gmail.com> Gesendet: Mittwoch, 23. August 2023 23:23:27 An: The general-purpose Squeak developers list Betreff: [squeak-dev] Re: On the rejection of Promises due to errors
Am Fr., 18. Aug. 2023 um 21:06 Uhr schrieb <christoph.thiede(a)student.hpi.uni-potsdam.de>:
Most importantly, you must never catch Exceptions instead of Errors but the current implementation does so. At the moment, we cannot even access any source files from a promise due to CurrentReadOnlySourceFiles.
Can you please provide a concrete example for this? Maybe we can at least find a solution for this aspect quicker than for all the rest.
Finally submitted the change as: Kernel-jr.1532 KernelTests-jr.446
Am Do., 12. Okt. 2023 um 21:25 Uhr schrieb christoph.thiede@student.hpi.uni-potsdam.de:
Hi Jakob,
I don't think #isResumable would be the right hook for this. Currently, the canonical way in the EHS to mark an exception as erroneous or fatal IMO is to subclass from Error. For anything better, we would have to simulate what would happen when resuming the exception ... and there we go the way to the ultimate solution you proposed at the beginning at this thread again. Catching errors in #fulfillWith:passErrors: for now sounds like a good workaroud to me. :-)
You could change the fulfill method accordingly and see whether you encounter any strange behavior during the next few weeks. Or have you already done it?
In one of my image, this change has been active for 1.5 months ... corresponding to a couple of full-time Squeak days.
So, from my side, please upload that patch. :-)
Best, Christoph
Sent from Squeak Inbox Talk
On 2023-08-26T18:30:24+02:00, jakres+squeak@gmail.com wrote:
Hi Christoph,
I would have liked to use #isResumable rather than Exception vs Error to determine whether something should be caught to reject the promise, or to let it pass. Unfortunately, ZeroDivide is declared as resumable... so future divisions by zero would not reject promises as they used to.
Changing fulfillWith:passErrors: to only catch Error instead of Exception lets all tests still pass on my side. It also makes this new one (in FutureTest) green:
testNotificationsAreNotCaught "The deferred evaluation below uses
CurrentReadOnlySourceFiles. The error handling of the future promise must not interfere with this Exception." | p1 | p1 := Object future comment. self waitUntil: [p1 isResolved] orCycleCount: 1. self assert: p1 isResolved.
You could change the fulfill method accordingly and see whether you encounter any strange behavior during the next few weeks. Or have you already done it?
In general, the change seems right to me, as there are many examples of Exceptions that are not supposed to be caught (like IllegalResumeAttempt, TestFailure), or are not meant to abort any control flow (ProgressNotification, MCNoChangesException).
Kind regards, Jakob
Am Do., 24. Aug. 2023 um 03:21 Uhr schrieb Thiede, Christoph <Christoph.Thiede(a)student.hpi.uni-potsdam.de>:
Hi Jakob,
short answer only at the current time, but thank you for looking into this!
Can you please provide a concrete example for this? Maybe we can at least find a solution for this aspect quicker than for all the rest.
Object future comment ifRejected: #halt.
A workaround might be only catching Error or Error , Warning in Promise>>#fulfillWith:passErrors: but not Exception, I guess ...
Best,
Christoph
Von: Jakob Reschke <jakres+squeak(a)gmail.com> Gesendet: Mittwoch, 23. August 2023 23:23:27 An: The general-purpose Squeak developers list Betreff: [squeak-dev] Re: On the rejection of Promises due to errors
Am Fr., 18. Aug. 2023 um 21:06 Uhr schrieb <christoph.thiede(a)student.hpi.uni-potsdam.de>:
Most importantly, you must never catch Exceptions instead of Errors but the current implementation does so. At the moment, we cannot even access any source files from a promise due to CurrentReadOnlySourceFiles.
Can you please provide a concrete example for this? Maybe we can at least find a solution for this aspect quicker than for all the rest.
squeak-dev@lists.squeakfoundation.org