Skip to content

Conversation

@jpco
Copy link
Collaborator

@jpco jpco commented Jul 5, 2025

This PR:

  1. Removes the retry exception from $&catch. This makes $&catch into a quite minimal wrapper around the CatchException macro, which is probably how it should have been all along.
  2. Adds the retry exception back, via fn-catch in initial.es. This maintains backwards compatibility, though I'm personally sold on the idea of removing retry from es. It's spooky, hard-to-predict magic, and can lead to awful infinite loops if people don't defend against it in exception handlers. It's also (as proven in this PR) entirely possible to re-implement, for any users who are adamant about having it, or simulate in a simpler form where it's used.
  3. Adds the "selectable exceptions" behavior proposed in Proposal: extension to catch to only catch certain exceptions #165.

With this PR, the following two are equivalent:

catch @ e type rest {
    if {!~ $e error} {
        throw $e $type $rest
    }
    # handle the error
} {
    # do something risky
}
catch error @ from rest {
    # handle the error $from $rest
} {
    # do something risky
}

Note that when an exception is "selected", the first term isn't passed to the catcher. This is a little more magic than necessary, but it does make the code read very nice.

This version of "selectable exceptions" is fairly conservative; your only options are catch foo @ {catcher} {body} or catch @ {catcher} {body}. I think this is fine; it's simple in simple cases and the complicated version isn't actually all that complicated. If people want more sophisticated catch-like mechanisms, they can write them; the default shell shouldn't have much magic. (Admittedly, this is a more-than-minimal amount of magic, but in practical use, I think the "ROI" is good. Better than retry, for sure.)

As usual, there are alternatives to this PR, all of which I'm pretty open to. I'd love to rip out retry, ever I saw since memreflect suggest it :)

jpco added 3 commits July 5, 2025 11:59
With this change, the $&catch primitive is about as complex as a
primitive should be.  Arguably, 'retry' should be removed from the shell
entirely, but this is a backwards-compatible half-measure, and
it does demonstrate how to "reimplement" retry in case it is actually
removed and people want to add it back.
This allows users to say `catch return @ value {echo $value} {command}`
and have that `catch` only catch return, and "pass through" anything
else.
@memreflect
Copy link
Contributor

This is good work!
I fully agree that for a shell like es, there are compromises that must be made, and simplifying things to just catch [exception] catcher body is a good one.

There is a regression in $&catch: if a catcher throws an exception while signals are blocked, they remain blocked:

; forever {}
^C
; $&catch @ { throw hello } { throw world }
uncaught exception: hello
; forever {}
^C^C^C^C^C^C^C^C^C^C    # can't interrupt loop

Restoring the original $&catch implementation and simply dropping retry behavior fixes things.

jpco added 2 commits July 26, 2025 18:12
Also add a test case for signal blocking/unblocking in exception
handlers.
That move adds a lot more complexity than it removes, so we'll just
avoid changing $&catch for now and instead add selectable exceptions to
fn-catch.
@jpco
Copy link
Collaborator Author

jpco commented Jul 27, 2025

Well, that's disappointing, I was feeling good about having removed that nested CatchException in $&catch.

I fixed it -- and added a test case that I think should prevent fools like me from breaking it again -- but now I'm not so sure that moving the retry handling from $&catch to initial.es is worthwhile. It adds a lot to the implementation of fn-catch and doesn't remove much of anything from $&catch. Undid it.

For reasons I don't totally understand, I had to add a SIGCHK() in $&catch for the tests to work. It makes some sense that we should make sure to actually throw any signal that came in when we leave the catcher.

@jpco jpco changed the title Simplify $&catch and add "selectable exceptions" Add "selectable exceptions" to $&catch Jul 27, 2025
@memreflect
Copy link
Contributor

Well, that's disappointing, I was feeling good about having removed that nested CatchException in $&catch.

Understandable—i encountered the same issue when i was attempting to adapt Scheme's guard procedure to es in response to the originally proposed idea.

For reasons I don't totally understand, I had to add a SIGCHK() in $&catch for the tests to work. It makes some sense that we should make sure to actually throw any signal that came in when we leave the catcher.

Inside a catcher, signals are blocked, so when kill -INT $pid fires the SIGINT signal, it is caught for later transformation into an exception.
This transformation occurs by executing SIGCHK(), and that doesn't happen in the REPL until after you exit the catch, so your $thrown variable is never set to the correct value.

I can't quite understand why throwing signal exceptions while executing a catcher is undesirable in the first place.
The idea is probably that an incoming Unix signal shouldn't interrupt a catcher currently handling a signal exception, like how things work in C with the non-realtime signals.
On the other hand, dropping blocksignals() and unblocksignals() in $&catch means $&catch @ e {forever {}} {throw x} won't require you to use kill -KILL $pid on the shell just to terminate things.

@jpco
Copy link
Collaborator Author

jpco commented Jul 29, 2025

I can't quite understand why throwing signal exceptions while executing a catcher is undesirable in the first place.
The idea is probably that an incoming Unix signal shouldn't interrupt a catcher currently handling a signal exception, like how things work in C with the non-realtime signals.

I think this is exactly it, and I think I lean towards it being the most reasonable (that is, least surprising) behavior.

Hypothetically, we could expose it to users better if we added blocked state to signals; maybe $&catch would set signals = blocked $signals and users could set signals = $signals(2 ...) if they wanted to unblock signal delivery during a particularly fancy exception handler.

I've been thinking a little about how handling "all signals" is a thing that's hard to wrangle in es -- perhaps a $&signals primitive that will return all the signals the shell knows about would be useful too.

@memreflect
Copy link
Contributor

Other than moving the SIGCHK() inside the loop, this PR looks good to me.
$&catch @ {$&throw retry} {$&throw retry} can cause trouble for example.

(I can open a new discussion or issue to get ideas on how $&catch and signals should interact if that is better.)

I can't quite understand why throwing signal exceptions while executing a catcher is undesirable in the first place.
The idea is probably that an incoming Unix signal shouldn't interrupt a catcher currently handling a signal exception, like how things work in C with the non-realtime signals.

I think this is exactly it, and I think I lean towards it being the most reasonable (that is, least surprising) behavior.

The least surprising behavior for a shell, or any program, is the ability to interrupt execution at any time.
For example, if you're handling a retry exception, and it's taking too long to finish executing, shouldn't you be able to interrupt it with Ctrl+C?

On the other hand, the least surprising behavior for a signal handler is the inability for another signal to interrupt that signal handler.
If you're handling a signal sigint exception, and it's taking too long to finish executing, should you be able to interrupt it with another signal sigint exception (Ctrl+C), or is that a bad idea?

Hypothetically, we could expose it to users better if we added blocked state to signals; maybe $&catch would set signals = blocked $signals and users could set signals = $signals(2 ...) if they wanted to unblock signal delivery during a particularly fancy exception handler.

It sounds easy enough to implement with set-signals invoking $&setsignals and the use of local (signals = ...) {...}, but it also means a poorly written function that sets signals could leave the shell unable to deal with interrupts.

I've been thinking a little about how handling "all signals" is a thing that's hard to wrangle in es -- perhaps a $&signals primitive that will return all the signals the shell knows about would be useful too.

This bugs me as well.
One idea might be to keep signals blocked all the time and periodically check whether a signal needs turned into an exception.

@jpco jpco force-pushed the master branch 4 times, most recently from 64361a8 to a23a7a0 Compare September 19, 2025 00:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants