[Cuis-dev] Unwind mechanism during termination is broken and inconsistent
Jaromir Matas
mail at jaromir.net
Wed Sep 24 14:55:44 PDT 2025
Hi Juan,
many thanks for your answer. It's a pleasure to read your remarks, as always :)
I admit I was under a silly impression that "undefined" was rather synonymous with "illegal" but you're right of course, and that clarifies a lot for me.
Thanks again, I hope I'll have more time to contribute some more :)
Best regards,
Jaromir
On Sep 24 2025, at 6:06 pm, Juan Vuletich via Cuis-dev <cuis-dev at lists.cuis.st> wrote:
> Hi Jaromir!
>
> Long time no see. It is nice to hear from you again!
> (inline)
> On 24/09/2025 11:18 AM, Jaromir Matas via Cuis-dev wrote:
> >
> > Hi Juan,
> > I've just come across a situation brought up by Christoph Thiede where your example/reasoning might apply - or maybe not. Please help :)
>
> Not sure if I'll be of help, but let's try.
> > Consider a simple example:
> > [^1] ensure: [^42]
> > What would you expect to be the return value of the expression?
> > There seem to be three candidates: 1, or 42, or undefined. The reason for considering undefined comes from the ANSI specification (draft 1997):
> > "If the evaluation of a termination block concludes with the execution of a return statement the result is undefined. The
> > result is also undefined if evaluation of the termination block results in evaluation of any block that concludes with a return statement and whose home activation is not on the call chain that starts with the activation of the termination block." (paragraph 3.4.5.1, page 22)
> > If I'm reading this correctly the authors didn't approve of jumping over the #ensure: argument block activation using a non-local return from the #ensure: argument block (aka terminationBlock). Hence declaring such an act undefined.
> > Returning any value from such an expression would then be an extension of the ANSI specification, I guess.
>
>
>
> Yes. "undefined" means any answer is OK. So either answering 1 or answering 42 agrees with ANSI spec.
>
>
> > Now, if I use your reasoning and modify your example from your mail below, I get this:
> > m2: a "A"
> > [1 + 2] ensure: [
> > true ifTrue: [
> > [ ^3 + 4 ] ensure: [ "*1"
> > true ifTrue: [
> > ^a at: 1 put: true ]]. "*2"
> > a at: 2 put: true ]]
> > m2: a "B"
> > [1 + 2] ensure: [
> > true ifTrue: [
> > res_inner := 3 + 4.
> > true ifTrue: [
> > ^a at: 1 put: true ].
> > a at: 2 put: true.
> > ^res_inner]]
> > m2: a "C"
> > res_outer := 1 + 2.
> > true ifTrue: [
> > res_inner := 3 + 4.
> > true ifTrue: [
> > ^a at: 1 put: true ].
> > a at: 2 put: true.
> > ^res_inner].
> > ^res_outer
> > Would this be a correct and appropriate application of your example and reasoning on this situation?
>
>
>
> I'd say that both in "B" and in "C" the `^res_inner` should go above the `a at: 2 put: true` meaning that `a second` is always nil. But that's unrelated to the point here.
>
>
> > Would you expect true to be returned from m2?
>
>
> Yes. I think the code transformation you did is OK (except for the detail I mentioned above), and I guess that's a reasonable argument to say that a method that only contains `[^1] ensure: [^42]` should return 42.
>
>
> > In this case you'd allow jumping over the activation of the terminationBlock from the terminationBlock and the expression `[^1] ensure: [^42]` would return 42. However, the semantics used in the unwind algorithm during termination is the opposite - it won't let the non-local return from the terminationBlock escape/jump over the #ensure: activation - which would result in the expression `[^1] ensure: [^42]` returning 1.
> > Funny thing is in Squeak I've unified the unwind logic for termination and general returns, so Squeak returns 1, but in Cuis we kept the previous unwind logic in #return:through: and as a result Cuis returns 42 :)
>
>
>
> I think that it is fair to say that the termination block takes precedence. It's purpose is to give guarantees, so it is reasonable to give it higher "authority".
>
>
> > I'm aware it's rather "academic" and not very useful in 99.9% of situations but I'd be very interested in your opinion if you could spare a few moments out of your busy schedule.
>
>
> People could run into this kind of things! I'm tempted to say that returning from the termination block could be forbidden, but if you need it, you really need it. Perhaps a better rule could be that the ensured block (the receiver of #ensure:) should #hasNonLocalReturn == false. Trying to exit, but also asking for #ensure: is, at a minimum, cryptic code.
>
>
> > Thanks a lot!
> > Best regards,
> > Jaromir
> >
> > PS: Christoph's original example was:
> > Object compile: 'sample
> > [[^1] ensure: [Transcript showln: #hi. self error]]
> > on: Error do: [:ex | ^ ex]'.
> > self sample
> > but I think my simplification captures the same idea...
>
>
>
> I think it is pretty clear that you want it to return the error here (if the error actually happens). If you want to return 1 in the normal (no error) case, it is easy enough to do it after all that, at the end of the #sample method, right?
> Cheers!
>
>
> >
> >
> > On 30-Apr-21 7:31:54 PM, "Juan Vuletich via Cuis-dev" <cuis-dev at lists.cuis.st (mailto:cuis-dev at lists.cuis.st)> wrote:
> >
> > > Hi Jaromir,
> > > > I’ve tried to rewrite your #test1ATerminate without the method calls – and indeed it passes… and that’s why I missed that – it was too simple; when using sends is where your fix comes to the rescue – THANKS!
> > > >
> > > >
> > > > | p a |
> > > > a := Array new: 4 withAll: false.
> > > > p := [
> > > > [
> > > > [ ] ensure: [
> > > > [Processor activeProcess suspend] ensure: [
> > > > ^a at: 1 put: true]. "line L1"
> > > > a at: 2 put: true] "line L2"
> > > > ] ensure: [a at: 3 put: true].
> > > > a at: 4 put: true
> > > > ] newProcess.
> > > > p resume.
> > > > Processor yield.
> > > > "make sure p is suspended and none of the unwind blocks has finished yet"
> > > > self assert: p isSuspended.
> > > > a noneSatisfy: [ :b | b ].
> > > > "now terminate the process and make sure all unwind blocks have finished"
> > > > p terminate.
> > > > self assert: p isTerminated.
> > > > self assert: a first & a third.
> > > > self assert: (a second | a fourth) not.
> > > Yes it does. I just thought that a more real-life like test of non local returns should also include actual method calls!
> > > > I’d like to raise a question here: I feel the second item, on line L2 should ideally execute too because it’s inside an unwind block halfway through it’s termination. The problem is though the non-local return at line L1 invokes it’s own unwind algorithm in #resume:through: which ignores halfway through unwind blocks – the reason for that is #resume:through: operates on the active process’s stack which makes it extremely difficult to unwind halfway through blocks. I tried to apply a similar tactics like in termination (control the unwind from another stack) and it works well but it’s very intrusive… I may open a separate discussion on that later to share the results. Do you think it may be worth exploring or it’s just not worth the bother?
> > > >
> > >
> > >
> > > Well. This is not just in the case of process #terminate, right? To play with this without involving process handling, but including actual method calls I just tried this:
> > >
> > > m1
> > > | a |
> > > a := Array new: 3.
> > > self m2: a.
> > > a at: 3 put: true.
> > > a print.
> > >
> > > m2: a "A"
> > > [1 + 2] ensure: [
> > > [ 3 + 4 ] ensure: [ "*1"
> > > true ifTrue: [
> > > ^a at: 1 put: true ]]. "*2"
> > > a at: 2 put: true ]
> > >
> > > In this example, the *1ensure is there only to guarantee that *2is ran, even if [3+4] happens to fail. If [3+4] it runs without problems, the result should be exactly the same as :
> > > m2: a "B"
> > > [1 + 2] ensure: [
> > > 3 + 4.
> > > true ifTrue: [
> > > ^a at: 1 put: true ].
> > > a at: 2 put: true ]
> > >
> > > Applying the same argument, the result should be the same as:
> > > m2: a "C"
> > > 1 + 2.
> > > 3 + 4.
> > > true ifTrue: [
> > > ^a at: 1 put: true ].
> > > a at: 2 put: true
> > >
> > > In implementation C it is clear that a second isNil. So, the same should be the case for B and A.
> > > I think that an ensured block should be guaranteed to run without external interference. But if it decides on its own to exit before running all its statements, it is it's own decision.
> > > > Another issue: I considered using the error part of the result of #runUntilErrorOrReturnFrom: to deal with situations like this (careful – crashes the Cuis image without the terminate fix; with the fix it works “ok”):
> > > >
> > > >
> > > > x := nil.
> > > > [self error: 'x1'] ensure: [
> > > > [self error: 'x2'] ensure: [
> > > > [self error: 'x3'] ensure: [
> > > > x:=3].
> > > > x:=2].
> > > > x:=1].
> > > > x
> > > >
> > > >
> > > > Here you have nested errors and the question is: If we abandon the Debugger window, what do we want to see as a result? Without the fix the image crashes badly with unwind errors, with the fix however the Debugger closes without unwinding – it’s a consequence of # runUntilErrorOrReturnFrom: behavior – it returns errors rather than opens a debugger (and leaves the decision with the user). So what do we want to see as a result – keep opening debugger windows and abandoning them manually or ignoring the errors and executing the assignments? That sounds like “resuming” rather than abandoning to me so at the moment I don’t know and will have to think about it. I just didn’t want to complicate the #terminate prematurely :)
> > >
> > > I think this case is very similar to the one above.. For example, if we proceed the first debugger (the x1 error), the x2 debugger opens. If we abandon it, we are abandoning the execution of the first ensured block (that includes the x := 1 assignment at the end). So, no assignment is done. I think that the behavior of our fix is correct in this case. No need to simulate "resuming".
> > >
> > > > Juan, many thanks again, I’ll study your tests and learn from them.
> > > I'm happy to be of help. There's not nothing in those tests that could be new to you. All I did was to add nested method calls, and add the #resume cases, that already did work with the fix. Let me thank you. You did a great analysis of the issues at hand, and your fix is a great contribution.
> > > I'll integrate it right now. If any further analysis provides additional changes, we'll integrate them too.
> > >
> > > > Best regards,
> > > >
> > > >
> > > > jaromir
> > > >
> > > >
> > >
> > >
> > > Cheers,
> > > --
> > > Juan Vuletich
> > > www.cuis-smalltalk.org (http://www.cuis-smalltalk.org)https://github.com/Cuis-Smalltalk/Cuis-Smalltalk-Devhttps://github.com/jvuletichhttps://www.linkedin.com/in/juan-vuletich-75611b3
> > > @JuanVuletich
> --
> Juan Vuletich
> www.cuis.st (http://www.cuis.st)
> github.com/jvuletich
> researchgate.net/profile/Juan-Vuletich
> independent.academia.edu/JuanVuletich
> patents.justia.com/inventor/juan-manuel-vuletich
> --
> Cuis-dev mailing list
> Cuis-dev at lists.cuis.st
> https://lists.cuis.st/mailman/listinfo/cuis-dev
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.cuis.st/mailman/archives/cuis-dev/attachments/20250924/b7ed433b/attachment-0001.htm>
More information about the Cuis-dev
mailing list