Subject: bug#46009: exception from inside false-if-exception?

From: Maxime Devos <[email-masked]>
Date: Thu, 20 Jun 2024 00:03:30 +0200

> Also, I don’t see a fundamental change in meaning
>>just look at my test.scm:

>>without the explicit (let/ec return ...) wrapper, and the explicit use of (return ...) in the handlers, the two invocations lead to different flow of control.
>>It's icing on the cake that if the underlying ERROR were in fact a RAISE-CONTINUABLE, then >it wouldn't even lead to a &NON-CONTINUABLE error in the #:unwind? #f case, only to a >different return value (different flow of control).

‘error’ is a procedure, not something that would be raise-continuable (I guess you could pass it to ‘raise-continuable’, but seems rather unlikely). I propose replacing ERROR by condition (lack of quotes and capitals intentional, since I’m referring to the concept here, not a Scheme object or macro and in particular not the macro ‘condition’ from SRFI-35

Now I look at the output (somehow when I previously read it I thought it was testing control flow with dynamic-wind, but it turns out it is testing semantics instead (without dynamic-wind) – reading comprehension failure on my part?), the observed behaviour (for v3.0.9) is indeed a significant change.

I don’t think test.scm is a good example though – the output is, AFAICT, just a bug – for the nested error, AFAICT neither of the exception handlers receive the exception, which is plain nonsense.

>>In my vocabulary, a different flow of control when using a control-flow primitive is a >>fundamental change to its meaning. but your miliage may wary of course.

It’s not in mine. What matters to me is (denotational) semantics (in a broad sense, including things like complexity, whether something is constant-time (important for cryptography), etc.). Control-flow is more a operational semantics thing. Often a convenient way of defining the semantics of control structures, but ultimately, if with a different control flow you achieve the same semantics (and aren’t compiling with the equivalent of -Og), that’s fine too.

>> If we are talking about reading comprehension, I would like to point
>> out that (Guile) Scheme is case-sensitive.
>for effective communication we need a syntax to designate when a word is to be
> interpreted in the scheme/lisp domain (as opposed to plain english). using all-caps
> in english prose is a somewhat well-spread syntax for that, even in non-CL lisp
> contexts in my experience.

A simple syntax is to use quotes, e.g. ‘call-with-current-continuation’, which is well-spread (even outside programming to some degree), and does not require mangling the casing. Depending on the medium, there are other options like monotype, putting a small box around it or coloring. For texts (but not e-mail or terminal things), I recommend monotype (and depending on capabilities of the writing software and aesthetic preferences, maybe try out things like coloring and boxes).

So …

>> Someone coming from case-insensitive languages and someone
> unfamiliar with the upcasing practice might get confused for a
> moment.
>for a moment. and that is key.

… why not both eliminate the moment of confusion _and_ use a syntax that avoids the ambiguity?

>> >that would be better, but i still wouldn't like the fact that it's
>> >focusing on the dynamic context of the handler, instead of the
>> >different flow of control.
>> 
> I don’t get the distinction. These are two sides of the same coin?
>
>not really, because there are two, orthogonal dimensions here:
>
>1) whether to unwind the stack,
>
>2) where to return the control flow when the handler returns.
>
>(even if one combination is impossible)

Err, all four combinations are possible with sufficient application of delimited continuations. If you have unwinded you can just rewind (if you have captured continuation things and accept the change in visible behaviour to ‘dynamic-wind’, the first is not a given because of performance considérations and the latter isn’t a given because, well, different behaviour).

There are two components to (1):

   (1)(a): what does dynamic-wind see?
   (1)(b): how does unwinding affect the dynamic environment of the handler.

For (2): first, there is no ‘return the control flow’ happening! The returning is all in tail position (albeit with some extra layers in the dynamic environment, so don’t do tail loops with with-exception-handler or you will use lots of memory) – according to the docs, once the handler is invoked, with-exception-handler stops doing things.

As mentioned in the documentation:

> https://www.gnu.org/software/guile/manual/html_node/Raising-and-Handling-Exceptions.html
>Unless with-exception-handler was invoked with #:unwind? #t, exception handlers are invoked __within the continuation of the error__, without unwinding the stack. The dynamic environment of the handler call will be that of the raise-exception call, with the difference that the current exception handler will be “unwound” to the \"outer\" handler (the one that was in place when the corresponding with-exception-handler was called).
> […]
> if with-exception-handler was invoked with #:unwind? #t is true, raise-exception will first unwind the stack by invoking an escape continuation (see call/ec), and then invoke the handler __with the continuation of the with-exception-handler call__.

(emphasis added with __)

Now, while ‘return the control flow’ is not accurate, there is definitely some control flow manipulation going on (see: ‘call/ec’). But consider this: whether you are in tail-position w.r.t. something, is a property of the dynamic environment! I mean, it definitely is visible in backtraces (which you can make with ‘(backtrace)’, doesn’t need exceptions).

Let’s look at some R6RS definitions :

>For a procedure call, the time between when it is initiated and when it returns is called its dynamic extent. [currently irrelevant things about call/cc]
>Some operations described in the report acquire information in addition to their explicit arguments from the dynamic environment.

(Technically it doesn’t state that the concept of ‘dynamic environment’ includes the ‘dynamic extent’, but it seems quite natural to consider all the dynamicness together, and ‘environment’ seems a sufficiently generic word to also include the ‘extent’.)

(dynamic extent <-> a region of stack, without focus on what the dynamic bindings in that stack are, dynamic environment <-> the full stack upto a point, with a focus at what’s visible at the point]

Summarised: stack stuff is part of the dynamic environment, control flow is stack stuff, so control flow is dynamic environment stuff.

>> >for reference, in CL they are called HANDLER-CASE and HANDLER-BIND, with >completely different syntaxes.
>>
>> Can’t honestly say I like those names (those suffixes -CASE / -BIND
>> don’t really provide information, you have to go into the spec of
>> HANDLER-CASE / HANDLER-BIND itself to get the differences).

>my point is not whether it's deducible, or whether the names are good. my point is that one API is misleading (i.e. wastes human time), while the other forces the reader to, at worst, look up the documentation. but certainly not conflate the two, thinking that they are doing the same thing -- plus some implementation detail regarding unwinding that he can postpone learning about.

It wasn’t my point either, I didn’t think you liked them either, I didn’t think your point was whether it’s deducible, I just interpreted it as an example of splitting stuff up and wanted to make clear those don’t seem good names.

>here's a caricature of this situation:
>
>imagine a hypothetical (raise [a potentially larger sexp] #:enabled? #t/#f) control flow primitive that simply returns when #:enabled? is #f.
>
>one can argue that "well, it's disabled, dummy!" (turing tarpit), or try to walk in the shoes of the reader or a newcomer.
>
>yes, you're right when you say something is possible, or deducible. but no, with all due respect, you're wrong when you imply that it doesn't make a difference (in human-brain efficiency).

With all due respect, that’s a strawman (as expected by a caricature).

I never claimed #:unwind? is a great name, that keyword #:enabled? is terribly named and even worse than #:disable, both you and I previously made clear that naming is important, I proposed renamings of #:unwind? to other things (that you even agreed to were better) or proposals for names for splitting up the procedure (not great names, but still more descriptive than #:enabled? Of all things).

Also, let’s do this walking into the shoes of the newcomer. As a newcomer, one of the things I do is _reading the documentation_ (I have heard this is a rare thing, but I actually do this, and did this).

As a newcomer, I find these references to delimited continuations, dynamic environment and tail call thingies, really interesting and cool, so I read a lot about it (more than I actually use them, even!) and try it out a bit. I also see these mentions of virtual machines and stack frames. I read those too of course, how stack frames are done is really cool and interesting (VM a bit less so, but still ok to skim through).

As a reader, I also think that sometimes the descriptions aren’t done well – it’s not a verbosity thing despite what some would say, but more a structure thing (both local or global). But that’s a property of the documentation, not the API.

I also thing that occassionally the APIs aren’t quite right, making it difficult to understand when to, say, use #:unwind? #false/#true. But as a newcomer, I don’t _need_ to know those distinctions, I can just assume the default is fine and avoid doing fancy dynamic environment stuff inside the exception handler. So, I can just avoid the complicated unwinding-or-not stuff until I’m not a newcomer anymore, and once that’s the case, I can start thinking about how to improve API stuff and utilise the fancy stuff.

Going back to the strawman:

> you're wrong when you imply that it doesn't make a difference (in human-brain efficiency).

I didn’t imply this! Just because I said you need to _read the documentation_ doesn’t mean that naming and the like can’t be improved, the existence of a ‘problem between monitor and chair’ does not exclude the existence of a problem in the software as well. Also that the status quo is ok doesn’t mean it can’t be be improved.

(I think the _name_ itself is fine, rather, I think that the _concept_ that it represents would preferably be changed to something different (and naturally this change in concept induces a change in name and perhaps semantics as well).)

>> Maybe you can install a breakpoint on
>> scm_c_with_continuation_barrier or something
>
>it assumes that it's already known that the curlpit is w-c-b, but that was already well >into the debugging effort. see my attached patch that intends to greatly shorten the >debugging time for anyone else walking down this path after me.
>
>and it's been decades i've done any serious C coding, and i'm not looking forward to >learning the ins and outs of gdb when i'm debugging an issue in scheme.

Of course it assumes that the culprit is ‘with-continuation-barrier’. You provided the information that ‘with-continuation-barrier’ might have something to do with the issue to me. Also, I didn’t mention gdb anywhere. It’s a common debugger, but not the only one. And neither did I mention learning the ins and outs – setting up a breakpoint and asking gdb to print a backtrace is hardly ‘the ins and outs’. (Whether the information of the backtrace will prove useful is another matter.)

Also, difficulty with C debugging is your problem, not mine – this thread isn’t named “how to simplify debugging from C”, as a volunteer I’m just mentioning some options to consider and I’m not your boss. Besides, given that the existence of FFI and the like, and that Guile it partially implemented in C instead of Scheme, it isn’t a given that the issue you are debugging is, in fact, fully in Scheme.

(Also maybe look if Shepherd is doing REPL stuff. The REPL implementation in Guile installs (in Scheme!) a continuation barrier.)

> I don’t think Shepherd has much use of with-continuation-barrier.
>
> IIUC, any time the posix thread enters C code is an implicit continuation barrier. if a
> fiber in shepherd uses any primitive in guile that is implemented in C, then it's a
> continuation barrier (and breaks when an attempt is made to suspend that fiber).

Not in the sense of with-continuation-barrier. C code can throw Scheme exceptions and those exception can in turn be caught by normal Scheme exception (but no raise-continuable stuff from C).

On a related note, there is also some (non-Scheme) dynwind-rewind stuff in Guile’s C code. I don’t have a clue on how that works and if it even works at all.

> changing shepherd not to unconditionally unwind (so that meaningful backtraces
> can be logged) can lead to subtle issues due to now having some extra C frames on
> the stack that were previously unwound prior to handling the error.

That’s not unwinding, that’s rewinding. Unwinding can’t lead to leaving extra C frames on the stack, because removing those C frames is part of the unwinding process and C frames are only generated by C code.  (Except perhaps for that dynwind-rewind stuff, but if it does that stuff, presumably it is prepared to deal with rewinding.)

Hypothetically a Scheme implementation could put C frames back onto the stack, but Guile doesn’t do this – whenever this would happen, it simply refuses(*). (IIRC it does so by throwing an exception.). From the documentation:

(*) besides perhaps that dynwind-rewind stuff.

>Scheme Procedure: suspendable-continuation? tag
>Return #t if a call to abort-to-prompt with the prompt tag tag would produce a >delimited continuation that could be resumed later.
>
>Almost all continuations have this property. The exception is where some code between the call-with-prompt and the abort-to-prompt recursed through C for some reason, the abort-to-prompt will succeed but any attempt to resume the continuation (by calling it) would fail. This is because composing a saved continuation with the current continuation involves relocating the stack frames that were saved from the old stack onto a (possibly) new position on the new stack, and Guile can only do this for stack frames that it created for Scheme code, not stack frames created by the C compiler. It’s a bit gnarly but if you stick with Scheme, you won’t have any problem.
>
>If no prompt is found with the given tag, this procedure just returns #f.

Best regards,
Maxime Devos.