Hi. I played a bit with exceptions trying to detect that given block will open the debugger. My idea was to simply catch UnhandledError which normally means that no outer code handles given error and therefore debugger is appeared. For my surprise the following example works from playground but not from the browser editor:
In playground you will see the halt. But from the browser the debugger will show MyTestError. After breaking my head I found that there is a difference how scripts are executed in those tools. In Playground it is a deferred action. But in the browser it is evaluated as a deep event processing code (keymap processing) and there is an outer error handler (on: Error do: ) which does some work and passes the exception further (err pass). Following script demonstrates the difference in the error processing logic when there is an outer handler: [[MyTestError signal ] on: UnhandledError do: [ :e | self halt ]] on: MyTestError do: [ :z | z pass] Try it from playground. Second line separately will show the halt while all together it will stop at MyTestError signal. Debugging shows that when the outer handler is processed it sets the handler context into the exception and it skips the existing handler for UnhandledError. The question: is it a feature or a bug? Think also how following code should work (unrelated to UnhandledError logic):
Best regards, Denis |
Users of UnhandledError definitely shows that it is a critical bug. For example we rely on UnhandledError in Announcer to ensure that all subscriptions will be processed independently on errors signalled by any of them:
It will show 1, 2, 3 in transcript and open 3 debuggers. Each error is deferred to the background process allowing the delivery to continue:
Now if you will try to wrap #announce: into handler block the deliver will be interrupted by first error:
It will open single debugger at first handler error. ср, 18 дек. 2019 г. в 20:44, Denis Kudriashov <[hidden email]>:
|
I think this problem could lead to debugger issues with stepOver and stepThrough. For example: It is a method which is used for stepThrough. If current method has outer handler for Error then the UnhandledError logic will be ignored. Step over is based on following method:
I think UnhandledError branch will never be true: Handler here is executed only once with ZeroDivide error. Previous version was based on UnhandledError handler:
which had the same issue when there is an outer error handler. And we have such general error handler (probably introduced in Pharo 6):
Any UI event is processed under this method. ср, 18 дек. 2019 г. в 22:31, Denis Kudriashov <[hidden email]>:
|
In reply to this post by Denis Kudriashov
Hi Denis,
This is a really interesting discussion, and I haven't wrapped my head around it properly yet, but one initial observation: On Wed, 18 Dec 2019 at 21:45, Denis Kudriashov <[hidden email]> wrote: > > Hi. > > I played a bit with exceptions trying to detect that given block will open the debugger. My idea was to simply catch UnhandledError which normally means that no outer code handles given error and therefore debugger is appeared. > For my surprise the following example works from playground but not from the browser editor: > > > [MyTestError signal ] on: UnhandledError do: [ :e | self halt ] > > > In playground you will see the halt. But from the browser the debugger will show MyTestError. > > After breaking my head I found that there is a difference how scripts are executed in those tools. In Playground it is a deferred action. But in the browser it is evaluated as a deep event processing code (keymap processing) and there is an outer error handler (on: Error do: ) which does some work and passes the exception further (err pass). I think the difference here is that in the playground 'self' is nil, while in the browser 'self' is the class being inspected. UndefinedObject>>handleSignal: tells the exception to execute its #defaultAction, which is to signal the UnhandledError, and thus it evaluates the block. One other thing to keep in mind, UnhandledError is a subclass of Exception. It will only catch signals of itself, unlike Error or Exception, which will catch all their subclasses. So 'MyTestError signal' can only be caught if something catches it and then signals UnhandledError. In this case it is the #defaultAction. HTH, Alistair |
In reply to this post by Denis Kudriashov
On Wed, 18 Dec 2019 at 21:45, Denis Kudriashov <[hidden email]> wrote:
> > Following script demonstrates the difference in the error processing logic when there is an outer handler: > > [ > > [MyTestError signal ] on: UnhandledError do: [ :e | self halt ] > > ] on: MyTestError do: [ :z | z pass] > > > Try it from playground. Second line separately will show the halt while all together it will stop at MyTestError signal. > > Debugging shows that when the outer handler is processed it sets the handler context into the exception and it skips the existing handler for UnhandledError. It isn't skipping the UnhandledError handler. MyTestError isn't a subclass of UnhandledError, it's a subclass of Error, so we expect the first exception handler that is triggered in the code above to be the MyTestError handler. > > The question: is it a feature or a bug? Setting aside whether it is a feature or a bug, it is clearly the defined behaviour. > Think also how following code should work (unrelated to UnhandledError logic): > > [ > > [ 1/0 ] on: MyTestError do: [ :e | self halt ] > > ] on: ZeroDivide do: [ :z | MyTestError signal ] In this case the behaviour should be the same for both the browser and the playground: the 0 divide exception handler catches 1/0 and signals MyTestError, which doesn't have a handler, and a debugger is opened. Cheers, Alistair |
In reply to this post by Denis Kudriashov
On Wed, 18 Dec 2019 at 23:32, Denis Kudriashov <[hidden email]> wrote:
> > Users of UnhandledError definitely shows that it is a critical bug. > > For example we rely on UnhandledError in Announcer to ensure that all subscriptions will be processed independently on errors signalled by any of them: > > ann := Announcer new. > ann when: ValueChanged do: [:ann | 1 logCr. 1/0 ]. > ann when: ValueChanged do: [:ann | 2 logCr. 2/0 ]. > ann when: ValueChanged do: [:ann | 3 logCr. 3/0 ]. > > > ann announce: ValueChanged new > > > It will show 1, 2, 3 in transcript and open 3 debuggers. Each error is deferred to the background process allowing the delivery to continue: > > AnnouncementSubscription>>deliver: anAnnouncement > > " deliver an announcement to receiver. In case of failure, it will be handled in separate process" > > > ^ (self handlesAnnouncement: anAnnouncement ) ifTrue: [ > > [action cull: anAnnouncement cull: announcer] > > on: UnhandledError fork: [:ex | ex pass ]] > > > Now if you will try to wrap #announce: into handler block the deliver will be interrupted by first error: > > [ann announce: ValueChanged new] on: ZeroDivide do: [ :err | err logCr. err pass ]. > > > It will open single debugger at first handler error. This looks like a bug in AnnouncementSubscription>>deliver:. The exception handler should probably be on Exception since the delivery mechanism just wants to continue and let something else deal with the exception. Cheers, Alistair |
In reply to this post by alistairgrant
Hi Alistair. чт, 19 дек. 2019 г. в 07:10, Alistair Grant <[hidden email]>: I think the difference here is that in the playground 'self' is nil, #handleSignal: is sent to contexts chain in the stack during error processing which ends up at top context and its nil sender. It is not related to the receiver of messages on the stack.
That's correct. But it does not matter for my findings. It will only catch signals of itself, unlike Error or Yes, and here is the trick. When error resignals UnhandledError from the defaultAction it is caught by UnhandledError handler but only if there was no outer handler which already processed original error
|
In reply to this post by Denis Kudriashov
чт, 19 дек. 2019 г. в 00:42, Denis Kudriashov <[hidden email]>:
Sorry, the mistake is here. It's better to copy scripts to the mail. It should be with Exception. But the result is same - single message in transcript:
|
In reply to this post by alistairgrant
Hi Alistair чт, 19 дек. 2019 г. в 07:19, Alistair Grant <[hidden email]>: On Wed, 18 Dec 2019 at 21:45, Denis Kudriashov <[hidden email]> wrote: Hm. It forces me to think that I did not explain the problem. If your sentence is correct then the following code would not halt:
Notice that I use fork to avoid any "garbage" of outer handlers from the doIt processing machinery. With the handler we #pass the caught error (on: MyTestError do: [ :z | z pass]) which should be equivalent to "not caught scenario" (it should be to my view) but it doesn't (there will be MyTestError instead of halt).
Do you still think it is clear semantics?
|
In reply to this post by Denis Kudriashov
The solution to problem is to resignal UnhandledError instead of simple signalling. Resignal uses original signalContext as a starting point for handlers lookup:
For example try debug following script in playground: With error method:
Step into #methodWithError and then stepThrough "1/0". In current image it will hands for a while and then it will open another debugger with single error item. The current debugger process will be broken. Now try to apply proposed change for UnhandledError>>signalForException: and repeat experiment. StepThrough "1/0" will move debugger to ZeroDivide signal showing correct stack. Notice that debugging separate "self methodWithError" without outer handler works correctly in both cases. But you should be in clean stack without hidden outer handlers. And in Playground or when you use debugIt it is clean. But you can try in "bad" environment. Execute in browser editor "self halt. self methodWithError". And in debugger just Step Through #methodWithError. It will hangs. Proposed change fixes that. I will prepare PR with tests. But it would be nice to find other broken places which will be fixed чт, 19 дек. 2019 г. в 00:42, Denis Kudriashov <[hidden email]>:
|
I will collect here other issues if I will find them. Looking in Context methods which I mentioned here I found that debugger is broken for stepping through warnings. And it means that debugging deprecated methods can hang debugger. I will create another thread on this issue because it has more questions on current exceptions design. чт, 19 дек. 2019 г. в 18:39, Denis Kudriashov <[hidden email]>:
|
In reply to this post by Denis Kudriashov
Hi Denis,
Thanks very much for the clarification in your previous email (which I haven't quoted here), I did indeed misunderstand the point you were making. On Thu, 19 Dec 2019 at 19:40, Denis Kudriashov <[hidden email]> wrote: > > The solution to problem is to resignal UnhandledError instead of simple signalling. Resignal uses original signalContext as a starting point for handlers lookup: > > UnhandledError>>signalForException: anError > > ^anError resignalAs: (self new > > exception: anError; > > yourself) > > > I played with some of debugger issues and this code really fixes them. > For example try debug following script in playground: > > [ > self methodWithError > ] on: Error do: [ :e | e logCr. e pass ] > > With error method: > > > Object>>methodWithError > > 1/0 > > Step into #methodWithError and then stepThrough "1/0". > In current image it will hands for a while and then it will open another debugger with single error item. The current debugger process will be broken. > Now try to apply proposed change for UnhandledError>>signalForException: and repeat experiment. StepThrough "1/0" will move debugger to ZeroDivide signal showing correct stack. Confirmed. > Notice that debugging separate "self methodWithError" without outer handler works correctly in both cases. But you should be in clean stack without hidden outer handlers. And in Playground or when you use debugIt it is clean. > But you can try in "bad" environment. Execute in browser editor "self halt. self methodWithError". And in debugger just Step Through #methodWithError. It will hangs. Proposed change fixes that. > > I will prepare PR with tests. But it would be nice to find other broken places which will be fixed I thought I'd take a look at VisualWorks (8.3) to see what it does: it has the same behaviour as with your fix above. This opens some other interesting possibilities. Changing some names and reinterpreting your original example: [ result := [ MyResumableError signal ] on: UnhandledError do: [ :e | self doUltimateFallback ] ] on: MyResumableError do: [ :e | e resume: 42 ]. The MyResumableError handler may be way down the call stack. While the code is more than a little contrived, it does demonstrate the possibility of allowing callers to handle exceptions, but then have a fallback if everything else fails. Great detective work! Thanks, Alistair |
In reply to this post by Denis Kudriashov
I checked Squeak. All my examples work consistently there: UnhandledError handler is always triggered. Even more: signalling new error from the handler block passes it from the original signallerContext:
This code halts in Squeak. |
чт, 19 дек. 2019 г. в 21:43, Denis Kudriashov <[hidden email]>:
This flag is used by VM primitive to lookup next handler. We should just copy this code into Pharo with corresponding tests |
чт, 19 дек. 2019 г. в 21:51, Denis Kudriashov <[hidden email]>:
|
Free forum by Nabble | Edit this page |