From 2e004c96c0edf5eb618b8f84e3a378b78a9ae43b Mon Sep 17 00:00:00 2001 From: Jack Conger Date: Sat, 5 Jul 2025 11:59:19 -0700 Subject: [PATCH 1/5] Move 'retry' exception handling from $&catch to fn-catch 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. --- initial.es | 25 ++++++++++++++++++++++++- prim-ctl.c | 34 +++++++--------------------------- 2 files changed, 31 insertions(+), 28 deletions(-) diff --git a/initial.es b/initial.es index 987919a0..4b760764 100644 --- a/initial.es +++ b/initial.es @@ -66,7 +66,6 @@ fn-. = $&dot fn-access = $&access fn-break = $&break -fn-catch = $&catch fn-echo = $&echo fn-exec = $&exec fn-forever = $&forever @@ -102,6 +101,30 @@ fn-break = throw break fn-exit = throw exit fn-return = throw return +# The catch function wraps $&catch and adds handling for the +# retry exception. + +fn catch catcher body { + let (result = <=true; retry = ()) + forever { + retry = false + $&catch @ e rest { + if {~ $e retry} { + retry = true + } {~ $e return} { + result = $rest + } { + throw $e $rest + } + } { + result = <={$&catch $catcher $body} + } + if {!$retry} { + return $result + } + } +} + # unwind-protect is a simple wrapper around catch that is used # to ensure that some cleanup code is run after running a code # fragment. This function must be written with care to make diff --git a/prim-ctl.c b/prim-ctl.c index 19cb1dd6..fdf65c38 100644 --- a/prim-ctl.c +++ b/prim-ctl.c @@ -46,44 +46,24 @@ PRIM(throw) { } PRIM(catch) { - Atomic retry; - if (list == NULL) fail("$&catch", "usage: catch catcher body"); Ref(List *, result, NULL); Ref(List *, lp, list); - do { - retry = FALSE; - - ExceptionHandler + ExceptionHandler - result = eval(lp->next, NULL, evalflags); + result = eval(lp->next, NULL, evalflags); - CatchException (frombody) + CatchException (e) - blocksignals(); - ExceptionHandler - result - = prim("noreturn", - mklist(lp->term, frombody), - NULL, - evalflags); - unblocksignals(); - CatchException (fromcatcher) + blocksignals(); + result = prim("noreturn", mklist(lp->term, e), NULL, evalflags); + unblocksignals(); - if (termeq(fromcatcher->term, "retry")) { - retry = TRUE; - unblocksignals(); - } else { - unblocksignals(); - throw(fromcatcher); - } - EndExceptionHandler + EndExceptionHandler - EndExceptionHandler - } while (retry); RefEnd(lp); RefReturn(result); } From 2618df2bd658b89d36845cfb10cc6c842a55cd45 Mon Sep 17 00:00:00 2001 From: Jack Conger Date: Sat, 5 Jul 2025 12:46:26 -0700 Subject: [PATCH 2/5] Add "selectable exceptions" for catch This allows users to say `catch return @ value {echo $value} {command}` and have that `catch` only catch return, and "pass through" anything else. --- initial.es | 51 ++++++++++++++++++++++++++++++++------------- share/path-cache.es | 8 ++----- share/status.es | 8 +++---- 3 files changed, 42 insertions(+), 25 deletions(-) diff --git a/initial.es b/initial.es index 4b760764..da25cb4f 100644 --- a/initial.es +++ b/initial.es @@ -102,25 +102,48 @@ fn-exit = throw exit fn-return = throw return # The catch function wraps $&catch and adds handling for the -# retry exception. - -fn catch catcher body { - let (result = <=true; retry = ()) - forever { - retry = false +# retry exception as well as "selectable exceptions": if catch is +# called like `catch foo $catcher $body`, then `catch` will only +# actually catch the 'foo' exception, and no others. + +fn-catch = $&noreturn @ catcher body { + let (exception = (); result = <=true; retry = ()) { + if {!~ $#body (0 1)} { + (exception catcher body) = $catcher $body + } + if {!~ $#body (0 1)} { + throw error catch 'usage: catch [exception] catcher body' + } $&catch @ e rest { - if {~ $e retry} { - retry = true - } {~ $e return} { - result = $rest + if {~ $e return-from-catch} { + result $rest } { throw $e $rest } } { - result = <={$&catch $catcher $body} - } - if {!$retry} { - return $result + forever { + retry = false + $&catch @ e rest { + if {~ $e retry} { + retry = true + } { + throw $e $rest + } + } { + result = <={$&catch @ e rest { + if {~ $#exception 0} { + $&noreturn $catcher $e $rest + } {~ $exception $e} { + $&noreturn $catcher $rest + } { + throw $e $rest + } + } $body} + } + if {!$retry} { + throw return-from-catch $result + } + } } } } diff --git a/share/path-cache.es b/share/path-cache.es index d9c8137b..17872a3d 100644 --- a/share/path-cache.es +++ b/share/path-cache.es @@ -58,12 +58,8 @@ fn recache progs { fn precache progs { let (result = ()) for (p = $progs) { - catch @ e type msg { - if {~ $e error} { - echo >[1=2] $msg - } { - throw $e $type $msg - } + catch error @ _ msg { + echo >[1=2] $msg } { result = $result <={%pathsearch $p} } diff --git a/share/status.es b/share/status.es index faf5ae3b..24ff5737 100644 --- a/share/status.es +++ b/share/status.es @@ -20,11 +20,9 @@ fn %interactive-loop { noexport = $noexport status status = <=true fn-%dispatch = $&noreturn @ { - catch @ e rest { - if {~ $e return} { - status = $rest - } - throw $e $rest + catch return @ value { + status = $value + return $value } { status = <={$d $*} } From 58f9a7b027fd724f491aeed9f6725c13a407327b Mon Sep 17 00:00:00 2001 From: Jack Conger Date: Sun, 6 Jul 2025 08:29:14 -0700 Subject: [PATCH 3/5] Add selectable exceptions to man page --- doc/es.1 | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/doc/es.1 b/doc/es.1 index 3fb8e790..1dd8b0f9 100644 --- a/doc/es.1 +++ b/doc/es.1 @@ -1959,12 +1959,18 @@ Exits the current loop. .I Value is used as the return value for the loop command. .TP -.Cr "catch \fIcatcher body\fP" +.Cr "catch \fR[\fP\fIexception\fP\fR]\fP \fIcatcher body\fP" Runs .IR body . -If it raises an exception, +If it raises an exception, and +.IR exception +is either not provided or matches the first term of the exception, .IR catcher is run and passed the exception as an argument. +If +.IR exception +is provided, then the passed argument does not include the first term +of the exception, which has already been matched. .TP .Cr "cd \fR[\fP\fIdirectory\fP\fR]\fP" Changes the current directory to From 801cd6e2d33b8eb96e1dfcaae8138e03ecedda9a Mon Sep 17 00:00:00 2001 From: Jack Conger Date: Sat, 26 Jul 2025 18:12:48 -0700 Subject: [PATCH 4/5] Make sure signals are unblocked after exceptions. Also add a test case for signal blocking/unblocking in exception handlers. --- prim-ctl.c | 11 ++++++++--- test/tests/trip.es | 30 ++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/prim-ctl.c b/prim-ctl.c index fdf65c38..506a9578 100644 --- a/prim-ctl.c +++ b/prim-ctl.c @@ -56,11 +56,16 @@ PRIM(catch) { result = eval(lp->next, NULL, evalflags); - CatchException (e) + CatchException (frombody) blocksignals(); - result = prim("noreturn", mklist(lp->term, e), NULL, evalflags); - unblocksignals(); + ExceptionHandler + result = prim("noreturn", mklist(lp->term, frombody), NULL, evalflags); + unblocksignals(); + CatchException (fromcatcher) + unblocksignals(); + throw(fromcatcher); + EndExceptionHandler EndExceptionHandler diff --git a/test/tests/trip.es b/test/tests/trip.es index 69dc4736..266c57af 100644 --- a/test/tests/trip.es +++ b/test/tests/trip.es @@ -159,6 +159,36 @@ never succeeded } } +test 'exceptions + signals' { + local (signals = sigint) { + let ( + was-blocked = false + thrown = () + thrown2 = () + ) { + catch @ { + thrown = $* + } { + catch @ e { + kill -INT $pid + was-blocked = true + } { + throw exception + } + } + catch @ { + thrown2 = $* + } { + catch @ e {kill -INT $pid} {throw exception2} + } + + assert $was-blocked signal is blocked during catcher + assert {~ $thrown(1) signal} signal exception during catcher is thrown + assert {~ $thrown2(1) signal} second signal is caught + } + } +} + test 'heredocs and herestrings' { let (bigfile = `{mktemp big-file.XXXXXX}) unwind-protect { From a858e3b2da143b5a9c554b1f77919cdac0c881a5 Mon Sep 17 00:00:00 2001 From: Jack Conger Date: Sat, 26 Jul 2025 21:23:23 -0700 Subject: [PATCH 5/5] Undo the $&catch to fn-catch move for `retry` handling 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. --- initial.es | 42 +++++++++--------------------------------- prim-ctl.c | 40 ++++++++++++++++++++++++++++------------ 2 files changed, 37 insertions(+), 45 deletions(-) diff --git a/initial.es b/initial.es index da25cb4f..b24b5357 100644 --- a/initial.es +++ b/initial.es @@ -65,7 +65,6 @@ fn-. = $&dot fn-access = $&access -fn-break = $&break fn-echo = $&echo fn-exec = $&exec fn-forever = $&forever @@ -101,13 +100,12 @@ fn-break = throw break fn-exit = throw exit fn-return = throw return -# The catch function wraps $&catch and adds handling for the -# retry exception as well as "selectable exceptions": if catch is -# called like `catch foo $catcher $body`, then `catch` will only -# actually catch the 'foo' exception, and no others. +# The catch function wraps $&catch and adds handling for "selectable +# exceptions": if catch is called like `catch foo $catcher $body`, then +# `catch` will only actually catch the 'foo' exception, and no others. fn-catch = $&noreturn @ catcher body { - let (exception = (); result = <=true; retry = ()) { + let (exception = ()) { if {!~ $#body (0 1)} { (exception catcher body) = $catcher $body } @@ -115,36 +113,14 @@ fn-catch = $&noreturn @ catcher body { throw error catch 'usage: catch [exception] catcher body' } $&catch @ e rest { - if {~ $e return-from-catch} { - result $rest + if {~ $#exception 0} { + $&noreturn $catcher $e $rest + } {~ $exception $e} { + $&noreturn $catcher $rest } { throw $e $rest } - } { - forever { - retry = false - $&catch @ e rest { - if {~ $e retry} { - retry = true - } { - throw $e $rest - } - } { - result = <={$&catch @ e rest { - if {~ $#exception 0} { - $&noreturn $catcher $e $rest - } {~ $exception $e} { - $&noreturn $catcher $rest - } { - throw $e $rest - } - } $body} - } - if {!$retry} { - throw return-from-catch $result - } - } - } + } $body } } diff --git a/prim-ctl.c b/prim-ctl.c index 506a9578..58da3b46 100644 --- a/prim-ctl.c +++ b/prim-ctl.c @@ -46,29 +46,45 @@ PRIM(throw) { } PRIM(catch) { + Atomic retry; + if (list == NULL) fail("$&catch", "usage: catch catcher body"); Ref(List *, result, NULL); Ref(List *, lp, list); - ExceptionHandler + do { + retry = FALSE; - result = eval(lp->next, NULL, evalflags); + ExceptionHandler - CatchException (frombody) + result = eval(lp->next, NULL, evalflags); - blocksignals(); - ExceptionHandler - result = prim("noreturn", mklist(lp->term, frombody), NULL, evalflags); - unblocksignals(); - CatchException (fromcatcher) - unblocksignals(); - throw(fromcatcher); - EndExceptionHandler + CatchException (frombody) + + blocksignals(); + ExceptionHandler + result + = prim("noreturn", + mklist(lp->term, frombody), + NULL, + evalflags); + unblocksignals(); + CatchException (fromcatcher) - EndExceptionHandler + if (termeq(fromcatcher->term, "retry")) { + retry = TRUE; + unblocksignals(); + } else { + unblocksignals(); + throw(fromcatcher); + } + EndExceptionHandler + EndExceptionHandler + } while (retry); + SIGCHK(); RefEnd(lp); RefReturn(result); }