bpo-17611. Move unwinding of stack for "pseudo exceptions" from interpreter to compiler. (GH-5006)



Co-authored-by: Mark Shannon <mark@hotpy.org>
Co-authored-by: Antoine Pitrou <antoine@python.org>
diff --git a/Python/ceval.c b/Python/ceval.c
index af5eb99..1a72413 100644
--- a/Python/ceval.c
+++ b/Python/ceval.c
@@ -500,17 +500,6 @@
     return 0;
 }
 
-/* Status code for main loop (reason for stack unwind) */
-enum why_code {
-        WHY_NOT =       0x0001, /* No error */
-        WHY_EXCEPTION = 0x0002, /* Exception occurred */
-        WHY_RETURN =    0x0008, /* 'return' statement */
-        WHY_BREAK =     0x0010, /* 'break' statement */
-        WHY_CONTINUE =  0x0020, /* 'continue' statement */
-        WHY_YIELD =     0x0040, /* 'yield' operator */
-        WHY_SILENCED =  0x0080  /* Exception silenced by 'with' */
-};
-
 static int do_raise(PyObject *, PyObject *);
 static int unpack_iterable(PyObject *, int, int, PyObject **);
 
@@ -556,7 +545,6 @@
     const _Py_CODEUNIT *next_instr;
     int opcode;        /* Current opcode */
     int oparg;         /* Current opcode argument, if any */
-    enum why_code why; /* Reason for block stack unwind */
     PyObject **fastlocals, **freevars;
     PyObject *retval = NULL;            /* Return value */
     PyThreadState *tstate = PyThreadState_GET();
@@ -914,8 +902,6 @@
     lltrace = _PyDict_GetItemId(f->f_globals, &PyId___ltrace__) != NULL;
 #endif
 
-    why = WHY_NOT;
-
     if (throwflag) /* support for generator.throw() */
         goto error;
 
@@ -926,6 +912,7 @@
     assert(!PyErr_Occurred());
 #endif
 
+main_loop:
     for (;;) {
         assert(stack_pointer >= f->f_valuestack); /* else underflow */
         assert(STACK_LEVEL() <= co->co_stacksize);  /* else overflow */
@@ -1056,9 +1043,8 @@
         switch (opcode) {
 
         /* BEWARE!
-           It is essential that any operation that fails sets either
-           x to NULL, err to nonzero, or why to anything but WHY_NOT,
-           and that no operation that succeeds does this! */
+           It is essential that any operation that fails must goto error
+           and that all operation that succeed call [FAST_]DISPATCH() ! */
 
         TARGET(NOP)
             FAST_DISPATCH();
@@ -1115,6 +1101,18 @@
             FAST_DISPATCH();
         }
 
+        TARGET(ROT_FOUR) {
+            PyObject *top = TOP();
+            PyObject *second = SECOND();
+            PyObject *third = THIRD();
+            PyObject *fourth = FOURTH();
+            SET_TOP(second);
+            SET_SECOND(third);
+            SET_THIRD(fourth);
+            SET_FOURTH(top);
+            FAST_DISPATCH();
+        }
+
         TARGET(DUP_TOP) {
             PyObject *top = TOP();
             Py_INCREF(top);
@@ -1618,8 +1616,7 @@
                 /* fall through */
             case 0:
                 if (do_raise(exc, cause)) {
-                    why = WHY_EXCEPTION;
-                    goto fast_block_end;
+                    goto exception_unwind;
                 }
                 break;
             default:
@@ -1632,8 +1629,8 @@
 
         TARGET(RETURN_VALUE) {
             retval = POP();
-            why = WHY_RETURN;
-            goto fast_block_end;
+            assert(f->f_iblock == 0);
+            goto return_or_yield;
         }
 
         TARGET(GET_AITER) {
@@ -1794,11 +1791,10 @@
             }
             /* receiver remains on stack, retval is value to be yielded */
             f->f_stacktop = stack_pointer;
-            why = WHY_YIELD;
             /* and repeat... */
             assert(f->f_lasti >= (int)sizeof(_Py_CODEUNIT));
             f->f_lasti -= sizeof(_Py_CODEUNIT);
-            goto fast_yield;
+            goto return_or_yield;
         }
 
         TARGET(YIELD_VALUE) {
@@ -1815,67 +1811,137 @@
             }
 
             f->f_stacktop = stack_pointer;
-            why = WHY_YIELD;
-            goto fast_yield;
+            goto return_or_yield;
         }
 
         TARGET(POP_EXCEPT) {
+            PyObject *type, *value, *traceback;
+            _PyErr_StackItem *exc_info;
             PyTryBlock *b = PyFrame_BlockPop(f);
             if (b->b_type != EXCEPT_HANDLER) {
                 PyErr_SetString(PyExc_SystemError,
                                 "popped block is not an except handler");
                 goto error;
             }
-            UNWIND_EXCEPT_HANDLER(b);
+            assert(STACK_LEVEL() >= (b)->b_level + 3 &&
+                   STACK_LEVEL() <= (b)->b_level + 4);
+            exc_info = tstate->exc_info;
+            type = exc_info->exc_type;
+            value = exc_info->exc_value;
+            traceback = exc_info->exc_traceback;
+            exc_info->exc_type = POP();
+            exc_info->exc_value = POP();
+            exc_info->exc_traceback = POP();
+            Py_XDECREF(type);
+            Py_XDECREF(value);
+            Py_XDECREF(traceback);
             DISPATCH();
         }
 
         PREDICTED(POP_BLOCK);
         TARGET(POP_BLOCK) {
-            PyTryBlock *b = PyFrame_BlockPop(f);
-            UNWIND_BLOCK(b);
+            PyFrame_BlockPop(f);
             DISPATCH();
         }
 
+        TARGET(POP_FINALLY) {
+            /* If oparg is 0 at the top of the stack are 1 or 6 values:
+               Either:
+                - TOP = NULL or an integer
+               or:
+                - (TOP, SECOND, THIRD) = exc_info()
+                - (FOURTH, FITH, SIXTH) = previous exception for EXCEPT_HANDLER
+
+               If oparg is 1 the value for 'return' was additionally pushed
+               at the top of the stack.
+            */
+            PyObject *res = NULL;
+            if (oparg) {
+                res = POP();
+            }
+            PyObject *exc = POP();
+            if (exc == NULL || PyLong_CheckExact(exc)) {
+                Py_XDECREF(exc);
+            }
+            else {
+                Py_DECREF(exc);
+                Py_DECREF(POP());
+                Py_DECREF(POP());
+
+                PyObject *type, *value, *traceback;
+                _PyErr_StackItem *exc_info;
+                PyTryBlock *b = PyFrame_BlockPop(f);
+                if (b->b_type != EXCEPT_HANDLER) {
+                    PyErr_SetString(PyExc_SystemError,
+                                    "popped block is not an except handler");
+                    Py_XDECREF(res);
+                    goto error;
+                }
+                assert(STACK_LEVEL() == (b)->b_level + 3);
+                exc_info = tstate->exc_info;
+                type = exc_info->exc_type;
+                value = exc_info->exc_value;
+                traceback = exc_info->exc_traceback;
+                exc_info->exc_type = POP();
+                exc_info->exc_value = POP();
+                exc_info->exc_traceback = POP();
+                Py_XDECREF(type);
+                Py_XDECREF(value);
+                Py_XDECREF(traceback);
+            }
+            if (oparg) {
+                PUSH(res);
+            }
+            DISPATCH();
+        }
+
+        TARGET(CALL_FINALLY) {
+            PyObject *ret = PyLong_FromLong(INSTR_OFFSET());
+            if (ret == NULL) {
+                goto error;
+            }
+            PUSH(ret);
+            JUMPBY(oparg);
+            FAST_DISPATCH();
+        }
+
+        TARGET(BEGIN_FINALLY) {
+            /* Push NULL onto the stack for using it in END_FINALLY,
+               POP_FINALLY, WITH_CLEANUP_START and WITH_CLEANUP_FINISH.
+             */
+            PUSH(NULL);
+            FAST_DISPATCH();
+        }
+
         PREDICTED(END_FINALLY);
         TARGET(END_FINALLY) {
-            PyObject *status = POP();
-            if (PyLong_Check(status)) {
-                why = (enum why_code) PyLong_AS_LONG(status);
-                assert(why != WHY_YIELD && why != WHY_EXCEPTION);
-                if (why == WHY_RETURN ||
-                    why == WHY_CONTINUE)
-                    retval = POP();
-                if (why == WHY_SILENCED) {
-                    /* An exception was silenced by 'with', we must
-                    manually unwind the EXCEPT_HANDLER block which was
-                    created when the exception was caught, otherwise
-                    the stack will be in an inconsistent state. */
-                    PyTryBlock *b = PyFrame_BlockPop(f);
-                    assert(b->b_type == EXCEPT_HANDLER);
-                    UNWIND_EXCEPT_HANDLER(b);
-                    why = WHY_NOT;
-                    Py_DECREF(status);
-                    DISPATCH();
+            /* At the top of the stack are 1 or 6 values:
+               Either:
+                - TOP = NULL or an integer
+               or:
+                - (TOP, SECOND, THIRD) = exc_info()
+                - (FOURTH, FITH, SIXTH) = previous exception for EXCEPT_HANDLER
+            */
+            PyObject *exc = POP();
+            if (exc == NULL) {
+                FAST_DISPATCH();
+            }
+            else if (PyLong_CheckExact(exc)) {
+                int ret = _PyLong_AsInt(exc);
+                Py_DECREF(exc);
+                if (ret == -1 && PyErr_Occurred()) {
+                    goto error;
                 }
-                Py_DECREF(status);
-                goto fast_block_end;
+                JUMPTO(ret);
+                FAST_DISPATCH();
             }
-            else if (PyExceptionClass_Check(status)) {
-                PyObject *exc = POP();
+            else {
+                assert(PyExceptionClass_Check(exc));
+                PyObject *val = POP();
                 PyObject *tb = POP();
-                PyErr_Restore(status, exc, tb);
-                why = WHY_EXCEPTION;
-                goto fast_block_end;
+                PyErr_Restore(exc, val, tb);
+                goto exception_unwind;
             }
-            else if (status != Py_None) {
-                PyErr_SetString(PyExc_SystemError,
-                    "'finally' pops bad exception");
-                Py_DECREF(status);
-                goto error;
-            }
-            Py_DECREF(status);
-            DISPATCH();
         }
 
         TARGET(LOAD_BUILD_CLASS) {
@@ -2815,28 +2881,13 @@
             DISPATCH();
         }
 
-        TARGET(BREAK_LOOP) {
-            why = WHY_BREAK;
-            goto fast_block_end;
-        }
-
-        TARGET(CONTINUE_LOOP) {
-            retval = PyLong_FromLong(oparg);
-            if (retval == NULL)
-                goto error;
-            why = WHY_CONTINUE;
-            goto fast_block_end;
-        }
-
-        TARGET(SETUP_LOOP)
-        TARGET(SETUP_EXCEPT)
         TARGET(SETUP_FINALLY) {
             /* NOTE: If you add any new block-setup opcodes that
                are not try/except/finally handlers, you may need
                to update the PyGen_NeedsFinalizing() function.
                */
 
-            PyFrame_BlockSetup(f, opcode, INSTR_OFFSET() + oparg,
+            PyFrame_BlockSetup(f, SETUP_FINALLY, INSTR_OFFSET() + oparg,
                                STACK_LEVEL());
             DISPATCH();
         }
@@ -2904,60 +2955,40 @@
         }
 
         TARGET(WITH_CLEANUP_START) {
-            /* At the top of the stack are 1-6 values indicating
+            /* At the top of the stack are 1 or 6 values indicating
                how/why we entered the finally clause:
-               - TOP = None
-               - (TOP, SECOND) = (WHY_{RETURN,CONTINUE}), retval
-               - TOP = WHY_*; no retval below it
+               - TOP = NULL
                - (TOP, SECOND, THIRD) = exc_info()
                  (FOURTH, FITH, SIXTH) = previous exception for EXCEPT_HANDLER
-               Below them is EXIT, the context.__exit__ bound method.
-               In the last case, we must call
-                 EXIT(TOP, SECOND, THIRD)
-               otherwise we must call
+               Below them is EXIT, the context.__exit__ or context.__aexit__
+               bound method.
+               In the first case, we must call
                  EXIT(None, None, None)
+               otherwise we must call
+                 EXIT(TOP, SECOND, THIRD)
 
-               In the first three cases, we remove EXIT from the
-               stack, leaving the rest in the same order.  In the
-               fourth case, we shift the bottom 3 values of the
-               stack down, and replace the empty spot with NULL.
+               In the first case, we remove EXIT from the
+               stack, leaving TOP, and push TOP on the stack.
+               Otherwise we shift the bottom 3 values of the
+               stack down, replace the empty spot with NULL, and push
+               None on the stack.
 
-               In addition, if the stack represents an exception,
-               *and* the function call returns a 'true' value, we
-               push WHY_SILENCED onto the stack.  END_FINALLY will
-               then not re-raise the exception.  (But non-local
-               gotos should still be resumed.)
+               Finally we push the result of the call.
             */
-
-            PyObject* stack[3];
+            PyObject *stack[3];
             PyObject *exit_func;
             PyObject *exc, *val, *tb, *res;
 
             val = tb = Py_None;
             exc = TOP();
-            if (exc == Py_None) {
-                (void)POP();
+            if (exc == NULL) {
+                STACKADJ(-1);
                 exit_func = TOP();
                 SET_TOP(exc);
-            }
-            else if (PyLong_Check(exc)) {
-                STACKADJ(-1);
-                switch (PyLong_AsLong(exc)) {
-                case WHY_RETURN:
-                case WHY_CONTINUE:
-                    /* Retval in TOP. */
-                    exit_func = SECOND();
-                    SET_SECOND(TOP());
-                    SET_TOP(exc);
-                    break;
-                default:
-                    exit_func = TOP();
-                    SET_TOP(exc);
-                    break;
-                }
                 exc = Py_None;
             }
             else {
+                assert(PyExceptionClass_Check(exc));
                 PyObject *tp2, *exc2, *tb2;
                 PyTryBlock *block;
                 val = SECOND();
@@ -2974,8 +3005,10 @@
                 /* We just shifted the stack down, so we have
                    to tell the except handler block that the
                    values are lower than it expects. */
+                assert(f->f_iblock > 0);
                 block = &f->f_blockstack[f->f_iblock - 1];
                 assert(block->b_type == EXCEPT_HANDLER);
+                assert(block->b_level > 0);
                 block->b_level--;
             }
 
@@ -2996,6 +3029,12 @@
 
         PREDICTED(WITH_CLEANUP_FINISH);
         TARGET(WITH_CLEANUP_FINISH) {
+            /* TOP = the result of calling the context.__exit__ bound method
+               SECOND = either None or exception type
+
+               If SECOND is None below is NULL or the return address,
+               otherwise below are 7 values representing an exception.
+            */
             PyObject *res = POP();
             PyObject *exc = POP();
             int err;
@@ -3011,8 +3050,15 @@
             if (err < 0)
                 goto error;
             else if (err > 0) {
-                /* There was an exception and a True return */
-                PUSH(PyLong_FromLong((long) WHY_SILENCED));
+                /* There was an exception and a True return.
+                 * We must manually unwind the EXCEPT_HANDLER block
+                 * which was created when the exception was caught,
+                 * otherwise the stack will be in an inconsisten state.
+                 */
+                PyTryBlock *b = PyFrame_BlockPop(f);
+                assert(b->b_type == EXCEPT_HANDLER);
+                UNWIND_EXCEPT_HANDLER(b);
+                PUSH(NULL);
             }
             PREDICT(END_FINALLY);
             DISPATCH();
@@ -3322,10 +3368,6 @@
         Py_UNREACHABLE();
 
 error:
-
-        assert(why == WHY_NOT);
-        why = WHY_EXCEPTION;
-
         /* Double-check exception status. */
 #ifdef NDEBUG
         if (!PyErr_Occurred())
@@ -3342,36 +3384,18 @@
             call_exc_trace(tstate->c_tracefunc, tstate->c_traceobj,
                            tstate, f);
 
-fast_block_end:
-        assert(why != WHY_NOT);
-
-        /* Unwind stacks if a (pseudo) exception occurred */
-        while (why != WHY_NOT && f->f_iblock > 0) {
-            /* Peek at the current block. */
-            PyTryBlock *b = &f->f_blockstack[f->f_iblock - 1];
-
-            assert(why != WHY_YIELD);
-            if (b->b_type == SETUP_LOOP && why == WHY_CONTINUE) {
-                why = WHY_NOT;
-                JUMPTO(PyLong_AS_LONG(retval));
-                Py_DECREF(retval);
-                break;
-            }
-            /* Now we have to pop the block. */
-            f->f_iblock--;
+exception_unwind:
+        /* Unwind stacks if an exception occurred */
+        while (f->f_iblock > 0) {
+            /* Pop the current block. */
+            PyTryBlock *b = &f->f_blockstack[--f->f_iblock];
 
             if (b->b_type == EXCEPT_HANDLER) {
                 UNWIND_EXCEPT_HANDLER(b);
                 continue;
             }
             UNWIND_BLOCK(b);
-            if (b->b_type == SETUP_LOOP && why == WHY_BREAK) {
-                why = WHY_NOT;
-                JUMPTO(b->b_handler);
-                break;
-            }
-            if (why == WHY_EXCEPTION && (b->b_type == SETUP_EXCEPT
-                || b->b_type == SETUP_FINALLY)) {
+            if (b->b_type == SETUP_FINALLY) {
                 PyObject *exc, *val, *tb;
                 int handler = b->b_handler;
                 _PyErr_StackItem *exc_info = tstate->exc_info;
@@ -3408,70 +3432,37 @@
                 PUSH(tb);
                 PUSH(val);
                 PUSH(exc);
-                why = WHY_NOT;
                 JUMPTO(handler);
-                break;
-            }
-            if (b->b_type == SETUP_FINALLY) {
-                if (why & (WHY_RETURN | WHY_CONTINUE))
-                    PUSH(retval);
-                PUSH(PyLong_FromLong((long)why));
-                why = WHY_NOT;
-                JUMPTO(b->b_handler);
-                break;
+                /* Resume normal execution */
+                goto main_loop;
             }
         } /* unwind stack */
 
-        /* End the loop if we still have an error (or return) */
-
-        if (why != WHY_NOT)
-            break;
-
-        assert(!PyErr_Occurred());
-
+        /* End the loop as we still have an error */
+        break;
     } /* main loop */
 
-    assert(why != WHY_YIELD);
     /* Pop remaining stack entries. */
     while (!EMPTY()) {
         PyObject *o = POP();
         Py_XDECREF(o);
     }
 
-    if (why != WHY_RETURN)
-        retval = NULL;
+    assert(retval == NULL);
+    assert(PyErr_Occurred());
 
-    assert((retval != NULL) ^ (PyErr_Occurred() != NULL));
-
-fast_yield:
-
+return_or_yield:
     if (tstate->use_tracing) {
         if (tstate->c_tracefunc) {
-            if (why == WHY_RETURN || why == WHY_YIELD) {
-                if (call_trace(tstate->c_tracefunc, tstate->c_traceobj,
-                               tstate, f,
-                               PyTrace_RETURN, retval)) {
-                    Py_CLEAR(retval);
-                    why = WHY_EXCEPTION;
-                }
-            }
-            else if (why == WHY_EXCEPTION) {
-                call_trace_protected(tstate->c_tracefunc, tstate->c_traceobj,
-                                     tstate, f,
-                                     PyTrace_RETURN, NULL);
+            if (call_trace_protected(tstate->c_tracefunc, tstate->c_traceobj,
+                                     tstate, f, PyTrace_RETURN, retval)) {
+                Py_CLEAR(retval);
             }
         }
         if (tstate->c_profilefunc) {
-            if (why == WHY_EXCEPTION)
-                call_trace_protected(tstate->c_profilefunc,
-                                     tstate->c_profileobj,
-                                     tstate, f,
-                                     PyTrace_RETURN, NULL);
-            else if (call_trace(tstate->c_profilefunc, tstate->c_profileobj,
-                                tstate, f,
-                                PyTrace_RETURN, retval)) {
+            if (call_trace_protected(tstate->c_profilefunc, tstate->c_profileobj,
+                                     tstate, f, PyTrace_RETURN, retval)) {
                 Py_CLEAR(retval);
-                /* why = WHY_EXCEPTION; useless yet but cause compiler warnings */
             }
         }
     }