Zsh Mailing List Archive
Messages sorted by: Reverse Date, Date, Thread, Author

Re: BUG: Always blocks perform tail call process optimization



Here is an updated patch with tests.

Philippe


On Sun, May 11, 2025 at 11:28 PM Philippe Altherr <philippe.altherr@xxxxxxxxx> wrote:
When the last command of a subshell is another subshell or an external command, Zsh reuses the subshell's process instead of forking. Zsh allows this optimization in try blocks of always statements. This leads to bogus exit statuses.

Test script (note that all always statements are surrounded by a subshell):
if [[ -v ZSH_EXEPATH ]]; then alias zsh=$ZSH_EXEPATH; fi;
zsh --version;

zsh <<EOF
({
  echo "false"
} always {
  false
})
EOF
echo status=$?
echo

zsh <<EOF
({
  echo "/usr/bin/false"
} always {
  /usr/bin/false
})
EOF
echo status=$?
echo

zsh <<EOF
({
  echo "( false )"
} always {
  ( false )
})
EOF
echo status=$?
echo

zsh <<EOF
({
  echo "( return 42 )"
} always {
  ( return 42 )
})
EOF
echo status=$?
echo


Output:
zsh 5.9.0.2-test (x86_64-apple-darwin24.4.0)
false
status=0

/usr/bin/false
status=1

( false )
status=1

( return 42 )
status=42


All statuses in the output should be equal to 0. What happens, is that when Zsh executes /usr/bin/false, it reuses the process of the subshell surrounding the always statement. Thus, this subshell becomes /usr/bin/false and when it terminates there is no longer any subshell around to replace its exit status with the exit status of the try block. Instead the exit status of /usr/bin/false becomes the exit status of the surrounding subshell.

The attached patch fixes the issues (and doesn't break any existing test). If everyone agrees with this I can add a test. If you know in which file it should go, let me know.

Philippe

diff --git a/Src/loop.c b/Src/loop.c
index 979285abc..ba01b1da5 100644
--- a/Src/loop.c
+++ b/Src/loop.c
@@ -774,7 +774,7 @@ exectry(Estate state, int do_exec)
     contflag = 0;
 
     state->pc = always;
-    execlist(state, 1, do_exec);
+    execlist(state, 1, 0);
 
     if (try_errflag)
 	errflag |= ERRFLAG_ERROR;
diff --git a/Test/A01grammar.ztst b/Test/A01grammar.ztst
index 660602caf..a65dc57f1 100644
--- a/Test/A01grammar.ztst
+++ b/Test/A01grammar.ztst
@@ -720,6 +720,12 @@
 >always 1
 >try 2
 
+  () { { return 2 } always { return 3 } }
+2:Exit status of always block is ignored
+
+  () { ( { return 2 } always { ( return 3 ) } ) }
+2:Regression test for exit status of always block is ignored also in tailcalls
+
   (
   mywrap() { echo BEGIN; true; echo END }
   mytest() { { exit 3 } always { mywrap }; print Exited before this }


Messages sorted by: Reverse Date, Date, Thread, Author