Repository: python/cpython
Pull request: gh-124285: Fix bug where bool() is called multiple times for the same part of a boolean expression
Fixes #124285.



<!-- gh-issue-number: gh-124285 -->
* Issue: gh-124285
<!-- /gh-issue-number -->


<!-- readthedocs-preview cpython-previews start -->
----
📚 Documentation preview 📚: https://cpython-previews--124394.org.readthedocs.build/

<!-- readthedocs-preview cpython-previews end -->

Problem statement:
Behavior change for `foo and 1 or 2`: 3.12 newly converts `foo` to bool twice
# Bug report

### Bug description:

We noticed a behavior change between 3.11 and 3.12. The following code calls `Foo.__bool__` once in 3.11 and twice in 3.12. Consequently for this contrived example, the expression evaluates to different results in 3.11 and 3.12.

```python
class Foo:
    def __init__(self):
        self._a = True
    def __bool__(self):
        self._a = not self._a
        print(f"Foo.__bool__ -> {self._a}")
        return self._a

Foo() and "a string" or 42
```

In Python 3.11:

```
>>> Foo() and "a string" or 42
Foo.__bool__ -> False
42
```

In Python 3.12 (and 3.13.0b2):

```
>>> Foo() and "a string" or 42
Foo.__bool__ -> False
Foo.__bool__ -> True
<__main__.Foo object at 0x7f0ae554c1a0>
```

Is this change intentional?

Note that I'm not necessarily asking to change this. We should arguably change the code to `"a string" if Foo() else 42`, which evaluates the same in 3.11 and 3.12.

### CPython versions tested on:

3.12

### Operating systems tested on:

Linux

<!-- gh-linked-prs -->
### Linked PRs
* gh-124394
* gh-124723
<!-- /gh-linked-prs -->

Hints from issue discussion:
I haven't had a chance to look closer at this, but I will say that flipping the truthiness of something everytime it's used in `__bool__` is quite odd. How is this being used in practice?
We noticed this when upgrading pytype to support 3.12. Pytype evaluates the bytecode and reported that this expression may now now be `Union[Foo, str, int]`. See https://github.com/google/pytype/pull/1777.

After staring at the bytecode for a while we noticed this is really a runtime change and a specifically crafted `__bool__` method could show that. But I don't know of any real example that does that.

Apparently pyright also encodes that behavior somehow. But it does that independently of the Python version. [pyright playground](https://pyright-play.net/?pythonVersion=3.11&code=MYGwhgzhAEBiD28BcAoa7oBMCmAzaA%2BgQJYB2xALkQBQTYi4CUqGr0dDAdAWNALzQAKgCcArtjQYc%2BIgCNEIGhyYs26Zd14DS8Cu3q5NktcOwVRw0vq48UKMEnYVhAHzJ6BCeNUbQwpTGgAIl4IZzIAcyDoeGFoABYAJiA).

Edit: I was honestly wondering if this is worth filing an issue about. But ultimately I thought there is no harm in reporting.
Yeah, this code is quite odd but this is definitely regression, thanks for the report @frigus02. I'm bisecting the bad commit right know
Confirmed to be happening on current main as well
Bisected to 3468c768ce5e467799758ec70b840da08c3c1da9
cc @iritkatriel 
Documentation in 3.12 says: "The expression x and y first evaluates x; if x is false, its value is returned; otherwise, y is evaluated and the resulting value is returned.
The expression x or y first evaluates x; if x is true, its value is returned; otherwise, y is evaluated and the resulting value is returned.
Note that neither [and](https://docs.python.org/3.12/reference/expressions.html#and) nor [or](https://docs.python.org/3.12/reference/expressions.html#or) restrict the value and type they return to False and True, but rather return the last evaluated argument."

That's exactly how it works in 3.12+:
1. Compute Foo() and "a string":
2. Foo() evaluates to False => returns Foo() object
3. Compute Foo() or "42":
4. Foo() evaluated to False => returns Foo() object

```py
# a.py
class Foo:
    def __init__(self):
        self._a = True
    def __bool__(self):
        print(self)
        self._a = not self._a
        print(f"Foo.__bool__ -> {self._a}")
        return self._a
print(Foo() and "a string" or 42)
```

```
$ python3.12 a.py
<__main__.Foo object at 0x7f1b514d6120>
Foo.__bool__ -> False
<__main__.Foo object at 0x7f1b514d6120>
Foo.__bool__ -> True
<__main__.Foo object at 0x7f1b514d6120>
```

It looks, that before the result of evaluation Foo() in (2) was "cached".  That's ok, assuming that bool(obj) would have no side effects.  Which is not our case:
```pycon
>>> x = Foo()
>>> bool(x)
<a.Foo object at 0x7fb5d7bfbaa0>
Foo.__bool__ -> False
False
>>> bool(x)
<a.Foo object at 0x7fb5d7bfbaa0>
Foo.__bool__ -> True
True
```

@Eclips4, thanks for debugging.  But I don't think it's a bug.  Does documentation says somewhere that ``__bool__()`` should have no side effects?
It's too late to change something as big as that and say that `__bool__` should have no side effects. In fact, it's essential in Python to expect side effects somewhere.. This is not making `__bool__` an exclusive here. The fact that `__bool__` is called twice (which actually is a reason of the following) is less scary than the fact that the same code has different results on different versions, because the commit which introduced it didn't intend that.
> the commit which introduced it didn't intend that.

Maybe (let's wait a reply from core dev;)), but IMHO it looks like it fixed a bug (which was mentioned by OP pre-3.12 behaviour).

If we return rules as in 3.11 - we will have to adjust docs accordingly, because old behaviour doesn't match docs (in 3.11 - too).  And yes, ``__bool__()`` - will be a special beast!
If we do decide to fix this, it's probably going to be too complicated to backport :(
It's a bug.  Terms should only be evaluated once.

Commit message: gh-124285: Fix bug where bool() is called multiple times for the same part of a boolean expression

Patch to review:
diff --git a/Doc/library/dis.rst b/Doc/library/dis.rst
index cad73192f7cd43..662c890d996a24 100644
--- a/Doc/library/dis.rst
+++ b/Doc/library/dis.rst
@@ -1872,6 +1872,12 @@ but are replaced by real opcodes or removed before bytecode is generated.
    Undirected relative jump instructions which are replaced by their
    directed (forward/backward) counterparts by the assembler.
 
+.. opcode:: JUMP_IF_TRUE
+.. opcode:: JUMP_IF_FALSE
+
+   Conditional jumps which do not impact the stack. Replaced by the sequence
+   ``COPY 1``, ``TO_BOOL``, ``POP_JUMP_IF_TRUE/FALSE``.
+
 .. opcode:: LOAD_CLOSURE (i)
 
    Pushes a reference to the cell contained in slot ``i`` of the "fast locals"
diff --git a/Include/internal/pycore_opcode_metadata.h b/Include/internal/pycore_opcode_metadata.h
index 51479afae3833d..48f8ea10514832 100644
--- a/Include/internal/pycore_opcode_metadata.h
+++ b/Include/internal/pycore_opcode_metadata.h
@@ -22,6 +22,8 @@ extern "C" {
     ((OP) == STORE_FAST_MAYBE_NULL) || \
     ((OP) == JUMP) || \
     ((OP) == JUMP_NO_INTERRUPT) || \
+    ((OP) == JUMP_IF_FALSE) || \
+    ((OP) == JUMP_IF_TRUE) || \
     ((OP) == SETUP_FINALLY) || \
     ((OP) == SETUP_CLEANUP) || \
     ((OP) == SETUP_WITH) || \
@@ -269,6 +271,10 @@ int _PyOpcode_num_popped(int opcode, int oparg)  {
             return 0;
         case JUMP_FORWARD:
             return 0;
+        case JUMP_IF_FALSE:
+            return 1;
+        case JUMP_IF_TRUE:
+            return 1;
         case JUMP_NO_INTERRUPT:
             return 0;
         case LIST_APPEND:
@@ -726,6 +732,10 @@ int _PyOpcode_num_pushed(int opcode, int oparg)  {
             return 0;
         case JUMP_FORWARD:
             return 0;
+        case JUMP_IF_FALSE:
+            return 1;
+        case JUMP_IF_TRUE:
+            return 1;
         case JUMP_NO_INTERRUPT:
             return 0;
         case LIST_APPEND:
@@ -956,7 +966,7 @@ enum InstructionFormat {
 };
 
 #define IS_VALID_OPCODE(OP) \
-    (((OP) >= 0) && ((OP) < 264) && \
+    (((OP) >= 0) && ((OP) < 266) && \
      (_PyOpcode_opcode_metadata[(OP)].valid_entry))
 
 #define HAS_ARG_FLAG (1)
@@ -1005,9 +1015,9 @@ struct opcode_metadata {
     int16_t flags;
 };
 
-extern const struct opcode_metadata _PyOpcode_opcode_metadata[264];
+extern const struct opcode_metadata _PyOpcode_opcode_metadata[266];
 #ifdef NEED_OPCODE_METADATA
-const struct opcode_metadata _PyOpcode_opcode_metadata[264] = {
+const struct opcode_metadata _PyOpcode_opcode_metadata[266] = {
     [BINARY_OP] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG },
     [BINARY_OP_ADD_FLOAT] = { true, INSTR_FMT_IXC, HAS_EXIT_FLAG },
     [BINARY_OP_ADD_INT] = { true, INSTR_FMT_IXC, HAS_EXIT_FLAG | HAS_ERROR_FLAG },
@@ -1224,6 +1234,8 @@ const struct opcode_metadata _PyOpcode_opcode_metadata[264] = {
     [YIELD_VALUE] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_ESCAPES_FLAG },
     [_DO_CALL_FUNCTION_EX] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG },
     [JUMP] = { true, -1, HAS_ARG_FLAG | HAS_JUMP_FLAG | HAS_EVAL_BREAK_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG },
+    [JUMP_IF_FALSE] = { true, -1, HAS_ARG_FLAG | HAS_JUMP_FLAG | HAS_EVAL_BREAK_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG },
+    [JUMP_IF_TRUE] = { true, -1, HAS_ARG_FLAG | HAS_JUMP_FLAG | HAS_EVAL_BREAK_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG },
     [JUMP_NO_INTERRUPT] = { true, -1, HAS_ARG_FLAG | HAS_JUMP_FLAG },
     [LOAD_CLOSURE] = { true, -1, HAS_ARG_FLAG | HAS_LOCAL_FLAG | HAS_PURE_FLAG },
     [POP_BLOCK] = { true, -1, HAS_PURE_FLAG },
@@ -1422,9 +1434,9 @@ _PyOpcode_macro_expansion[256] = {
 };
 #endif // NEED_OPCODE_METADATA
 
-extern const char *_PyOpcode_OpName[264];
+extern const char *_PyOpcode_OpName[266];
 #ifdef NEED_OPCODE_METADATA
-const char *_PyOpcode_OpName[264] = {
+const char *_PyOpcode_OpName[266] = {
     [BINARY_OP] = "BINARY_OP",
     [BINARY_OP_ADD_FLOAT] = "BINARY_OP_ADD_FLOAT",
     [BINARY_OP_ADD_INT] = "BINARY_OP_ADD_INT",
@@ -1543,6 +1555,8 @@ const char *_PyOpcode_OpName[264] = {
     [JUMP_BACKWARD] = "JUMP_BACKWARD",
     [JUMP_BACKWARD_NO_INTERRUPT] = "JUMP_BACKWARD_NO_INTERRUPT",
     [JUMP_FORWARD] = "JUMP_FORWARD",
+    [JUMP_IF_FALSE] = "JUMP_IF_FALSE",
+    [JUMP_IF_TRUE] = "JUMP_IF_TRUE",
     [JUMP_NO_INTERRUPT] = "JUMP_NO_INTERRUPT",
     [LIST_APPEND] = "LIST_APPEND",
     [LIST_EXTEND] = "LIST_EXTEND",
@@ -1945,13 +1959,15 @@ const uint8_t _PyOpcode_Deopt[256] = {
 struct pseudo_targets {
     uint8_t targets[3];
 };
-extern const struct pseudo_targets _PyOpcode_PseudoTargets[8];
+extern const struct pseudo_targets _PyOpcode_PseudoTargets[10];
 #ifdef NEED_OPCODE_METADATA
-const struct pseudo_targets _PyOpcode_PseudoTargets[8] = {
+const struct pseudo_targets _PyOpcode_PseudoTargets[10] = {
     [LOAD_CLOSURE-256] = { { LOAD_FAST, 0, 0 } },
     [STORE_FAST_MAYBE_NULL-256] = { { STORE_FAST, 0, 0 } },
     [JUMP-256] = { { JUMP_FORWARD, JUMP_BACKWARD, 0 } },
     [JUMP_NO_INTERRUPT-256] = { { JUMP_FORWARD, JUMP_BACKWARD_NO_INTERRUPT, 0 } },
+    [JUMP_IF_FALSE-256] = { { JUMP_FORWARD, JUMP_BACKWARD, 0 } },
+    [JUMP_IF_TRUE-256] = { { JUMP_FORWARD, JUMP_BACKWARD, 0 } },
     [SETUP_FINALLY-256] = { { NOP, 0, 0 } },
     [SETUP_CLEANUP-256] = { { NOP, 0, 0 } },
     [SETUP_WITH-256] = { { NOP, 0, 0 } },
@@ -1961,7 +1977,7 @@ const struct pseudo_targets _PyOpcode_PseudoTargets[8] = {
 #endif // NEED_OPCODE_METADATA
 static inline bool
 is_pseudo_target(int pseudo, int target) {
-    if (pseudo < 256 || pseudo >= 264) {
+    if (pseudo < 256 || pseudo >= 266) {
         return false;
     }
     for (int i = 0; _PyOpcode_PseudoTargets[pseudo-256].targets[i]; i++) {
diff --git a/Include/opcode_ids.h b/Include/opcode_ids.h
index 5ded0b41b4830e..8ba1ab25a77770 100644
--- a/Include/opcode_ids.h
+++ b/Include/opcode_ids.h
@@ -226,13 +226,15 @@ extern "C" {
 #define INSTRUMENTED_LINE                      254
 #define ENTER_EXECUTOR                         255
 #define JUMP                                   256
-#define JUMP_NO_INTERRUPT                      257
-#define LOAD_CLOSURE                           258
-#define POP_BLOCK                              259
-#define SETUP_CLEANUP                          260
-#define SETUP_FINALLY                          261
-#define SETUP_WITH                             262
-#define STORE_FAST_MAYBE_NULL                  263
+#define JUMP_IF_FALSE                          257
+#define JUMP_IF_TRUE                           258
+#define JUMP_NO_INTERRUPT                      259
+#define LOAD_CLOSURE                           260
+#define POP_BLOCK                              261
+#define SETUP_CLEANUP                          262
+#define SETUP_FINALLY                          263
+#define SETUP_WITH                             264
+#define STORE_FAST_MAYBE_NULL                  265
 
 #define HAVE_ARGUMENT                           41
 #define MIN_SPECIALIZED_OPCODE                 150
diff --git a/Lib/_opcode_metadata.py b/Lib/_opcode_metadata.py
index 6e4b33921863cb..dd70c5250c0b1e 100644
--- a/Lib/_opcode_metadata.py
+++ b/Lib/_opcode_metadata.py
@@ -335,13 +335,15 @@
     'INSTRUMENTED_CALL': 252,
     'INSTRUMENTED_JUMP_BACKWARD': 253,
     'JUMP': 256,
-    'JUMP_NO_INTERRUPT': 257,
-    'LOAD_CLOSURE': 258,
-    'POP_BLOCK': 259,
-    'SETUP_CLEANUP': 260,
-    'SETUP_FINALLY': 261,
-    'SETUP_WITH': 262,
-    'STORE_FAST_MAYBE_NULL': 263,
+    'JUMP_IF_FALSE': 257,
+    'JUMP_IF_TRUE': 258,
+    'JUMP_NO_INTERRUPT': 259,
+    'LOAD_CLOSURE': 260,
+    'POP_BLOCK': 261,
+    'SETUP_CLEANUP': 262,
+    'SETUP_FINALLY': 263,
+    'SETUP_WITH': 264,
+    'STORE_FAST_MAYBE_NULL': 265,
 }
 
 HAVE_ARGUMENT = 41
diff --git a/Lib/test/test_compile.py b/Lib/test/test_compile.py
index 736eff35c1d5f2..b81d847c824273 100644
--- a/Lib/test/test_compile.py
+++ b/Lib/test/test_compile.py
@@ -1527,6 +1527,45 @@ async def name_4():
                     pass
             [[]]
 
+class TestBooleanExpression(unittest.TestCase):
+    class Value:
+        def __init__(self):
+            self.called = 0
+
+        def __bool__(self):
+            self.called += 1
+            return self.value
+
+    class Yes(Value):
+        value = True
+
+    class No(Value):
+        value = False
+
+    def test_short_circuit_and(self):
+        v = [self.Yes(), self.No(), self.Yes()]
+        res = v[0] and v[1] and v[0]
+        self.assertIs(res, v[1])
+        self.assertEqual([e.called for e in v], [1, 1, 0])
+
+    def test_short_circuit_or(self):
+        v = [self.No(), self.Yes(), self.No()]
+        res = v[0] or v[1] or v[0]
+        self.assertIs(res, v[1])
+        self.assertEqual([e.called for e in v], [1, 1, 0])
+
+    def test_compound(self):
+        # See gh-124285
+        v = [self.No(), self.Yes(), self.Yes(), self.Yes()]
+        res = v[0] and v[1] or v[2] or v[3]
+        self.assertIs(res, v[2])
+        self.assertEqual([e.called for e in v], [1, 0, 1, 0])
+
+        v = [self.No(), self.No(), self.Yes(), self.Yes(), self.No()]
+        res = v[0] or v[1] and v[2] or v[3] or v[4]
+        self.assertIs(res, v[3])
+        self.assertEqual([e.called for e in v], [1, 1, 0, 1, 0])
+
 @requires_debug_ranges()
 class TestSourcePositions(unittest.TestCase):
     # Ensure that compiled code snippets have correct line and column numbers
diff --git a/Misc/NEWS.d/next/Core and Builtins/2024-09-23-23-06-19.gh-issue-124285.mahGTg.rst b/Misc/NEWS.d/next/Core and Builtins/2024-09-23-23-06-19.gh-issue-124285.mahGTg.rst
new file mode 100644
index 00000000000000..a6dec66a743f92
--- /dev/null
+++ b/Misc/NEWS.d/next/Core and Builtins/2024-09-23-23-06-19.gh-issue-124285.mahGTg.rst	
@@ -0,0 +1,2 @@
+Fix bug where ``bool(a)`` can be invoked more than once during the
+evaluation of a compound boolean expression.
diff --git a/Python/bytecodes.c b/Python/bytecodes.c
index 846404e28bb18f..506c4136b9001e 100644
--- a/Python/bytecodes.c
+++ b/Python/bytecodes.c
@@ -2571,6 +2571,16 @@ dummy_func(
             JUMP_BACKWARD_NO_INTERRUPT,
         };
 
+        pseudo(JUMP_IF_FALSE, (cond -- cond)) = {
+            JUMP_FORWARD,
+            JUMP_BACKWARD,
+        };
+
+        pseudo(JUMP_IF_TRUE, (cond -- cond)) = {
+            JUMP_FORWARD,
+            JUMP_BACKWARD,
+        };
+
         tier1 inst(ENTER_EXECUTOR, (--)) {
             #ifdef _Py_TIER2
             PyCodeObject *code = _PyFrame_GetCode(frame);
diff --git a/Python/codegen.c b/Python/codegen.c
index 0305f4299aec56..896c30cc14952a 100644
--- a/Python/codegen.c
+++ b/Python/codegen.c
@@ -3140,17 +3140,15 @@ codegen_boolop(compiler *c, expr_ty e)
     location loc = LOC(e);
     assert(e->kind == BoolOp_kind);
     if (e->v.BoolOp.op == And)
-        jumpi = POP_JUMP_IF_FALSE;
+        jumpi = JUMP_IF_FALSE;
     else
-        jumpi = POP_JUMP_IF_TRUE;
+        jumpi = JUMP_IF_TRUE;
     NEW_JUMP_TARGET_LABEL(c, end);
     s = e->v.BoolOp.values;
     n = asdl_seq_LEN(s) - 1;
     assert(n >= 0);
     for (i = 0; i < n; ++i) {
         VISIT(c, expr, (expr_ty)asdl_seq_GET(s, i));
-        ADDOP_I(c, loc, COPY, 1);
-        ADDOP(c, loc, TO_BOOL);
         ADDOP_JUMP(c, loc, jumpi, end);
         ADDOP(c, loc, POP_TOP);
     }
diff --git a/Python/flowgraph.c b/Python/flowgraph.c
index f7d8efb28e21c4..10329e002dcf69 100644
--- a/Python/flowgraph.c
+++ b/Python/flowgraph.c
@@ -1589,6 +1589,8 @@ basicblock_optimize_load_const(PyObject *const_cache, basicblock *bb, PyObject *
         switch(nextop) {
             case POP_JUMP_IF_FALSE:
             case POP_JUMP_IF_TRUE:
+            case JUMP_IF_FALSE:
+            case JUMP_IF_TRUE:
             {
                 /* Remove LOAD_CONST const; conditional jump */
                 PyObject* cnt = get_const_value(opcode, oparg, consts);
@@ -1600,8 +1602,11 @@ basicblock_optimize_load_const(PyObject *const_cache, basicblock *bb, PyObject *
                 if (is_true == -1) {
                     return ERROR;
                 }
-                INSTR_SET_OP0(inst, NOP);
-                int jump_if_true = nextop == POP_JUMP_IF_TRUE;
+                if (PyCompile_OpcodeStackEffect(nextop, 0) == -1) {
+                    /* POP_JUMP_IF_FALSE or POP_JUMP_IF_TRUE */
+                    INSTR_SET_OP0(inst, NOP);
+                }
+                int jump_if_true = (nextop == POP_JUMP_IF_TRUE || nextop == JUMP_IF_TRUE);
                 if (is_true == jump_if_true) {
                     bb->b_instr[i+1].i_opcode = JUMP;
                 }
@@ -1761,6 +1766,34 @@ optimize_basic_block(PyObject *const_cache, basicblock *bb, PyObject *consts)
                         i -= jump_thread(bb, inst, target, POP_JUMP_IF_TRUE);
                 }
                 break;
+            case JUMP_IF_FALSE:
+                switch (target->i_opcode) {
+                    case JUMP:
+                    case JUMP_IF_FALSE:
+                        i -= jump_thread(bb, inst, target, JUMP_IF_FALSE);
+                        continue;
+                    case JUMP_IF_TRUE:
+                        // No need to check for loops here, a block's b_next
+                        // cannot point to itself.
+                        assert(inst->i_target != inst->i_target->b_next);
+                        inst->i_target = inst->i_target->b_next;
+                        continue;
+                }
+                break;
+            case JUMP_IF_TRUE:
+                switch (target->i_opcode) {
+                    case JUMP:
+                    case JUMP_IF_TRUE:
+                        i -= jump_thread(bb, inst, target, JUMP_IF_TRUE);
+                        continue;
+                    case JUMP_IF_FALSE:
+                        // No need to check for loops here, a block's b_next
+                        // cannot point to itself.
+                        assert(inst->i_target != inst->i_target->b_next);
+                        inst->i_target = inst->i_target->b_next;
+                        continue;
+                }
+                break;
             case JUMP:
             case JUMP_NO_INTERRUPT:
                 switch (target->i_opcode) {
@@ -2367,6 +2400,37 @@ push_cold_blocks_to_end(cfg_builder *g) {
     return SUCCESS;
 }
 
+static int
+convert_pseudo_conditional_jumps(cfg_builder *g)
+{
+    basicblock *entryblock = g->g_entryblock;
+    for (basicblock *b = entryblock; b != NULL; b = b->b_next) {
+        for (int i = 0; i < b->b_iused; i++) {
+            cfg_instr *instr = &b->b_instr[i];
+            if (instr->i_opcode == JUMP_IF_FALSE || instr->i_opcode == JUMP_IF_TRUE) {
+                assert(i == b->b_iused - 1);
+                instr->i_opcode = instr->i_opcode == JUMP_IF_FALSE ?
+                                          POP_JUMP_IF_FALSE : POP_JUMP_IF_TRUE;
+                cfg_instr copy = {
+                            .i_opcode = COPY,
+                            .i_oparg = 1,
+                            .i_loc = instr->i_loc,
+                            .i_target = NULL,
+                };
+                RETURN_IF_ERROR(basicblock_insert_instruction(b, i++, &copy));
+                cfg_instr to_bool = {
+                            .i_opcode = TO_BOOL,
+                            .i_oparg = 0,
+                            .i_loc = instr->i_loc,
+                            .i_target = NULL,
+                };
+                RETURN_IF_ERROR(basicblock_insert_instruction(b, i++, &to_bool));
+            }
+        }
+    }
+    return SUCCESS;
+}
+
 static int
 convert_pseudo_ops(cfg_builder *g)
 {
@@ -2826,6 +2890,8 @@ _PyCfg_OptimizedCfgToInstructionSequence(cfg_builder *g,
                                      int *stackdepth, int *nlocalsplus,
                                      _PyInstructionSequence *seq)
 {
+    RETURN_IF_ERROR(convert_pseudo_conditional_jumps(g));
+
     *stackdepth = calculate_stackdepth(g);
     if (*stackdepth < 0) {
         return ERROR;


Code review comments:
diff_hunk: @@ -2571,6 +2571,16 @@ dummy_func(
             JUMP_BACKWARD_NO_INTERRUPT,
         };
 
+        pseudo(JUMP_IF_FALSE, (cond -- cond)) = {
+            JUMP_FORWARD,
+            JUMP_BACKWARD,
path: Python/bytecodes.c
text: @author:
I'm cheating here, it's not the right definition. I need to think how to do this properly in the generator. I think we need a version of pseudo that takes a sequence of instructions rather than a set of options. I'll think about that tomorrow.