Skip to content

Commit 9af0e47

Browse files
authored
bpo-39156: Break up COMPARE_OP into four logically distinct opcodes. (GH-17754)
Break up COMPARE_OP into four logically distinct opcodes: * COMPARE_OP for rich comparisons * IS_OP for 'is' and 'is not' tests * CONTAINS_OP for 'in' and 'is not' tests * JUMP_IF_NOT_EXC_MATCH for checking exceptions in 'try-except' statements.
1 parent 62e3973 commit 9af0e47

File tree

17 files changed

+4901
-4847
lines changed

17 files changed

+4901
-4847
lines changed

Doc/library/dis.rst

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -927,6 +927,20 @@ All of the following opcodes use their arguments.
927927
``cmp_op[opname]``.
928928

929929

930+
.. opcode:: IS_OP (invert)
931+
932+
Performs ``is`` comparison, or ``is not`` if ``invert`` is 1.
933+
934+
.. versionadded:: 3.9
935+
936+
937+
.. opcode:: CONTAINS_OP (invert)
938+
939+
Performs ``in`` comparison, or ``not in`` if ``invert`` is 1.
940+
941+
.. versionadded:: 3.9
942+
943+
930944
.. opcode:: IMPORT_NAME (namei)
931945

932946
Imports the module ``co_names[namei]``. TOS and TOS1 are popped and provide
@@ -961,6 +975,13 @@ All of the following opcodes use their arguments.
961975

962976
.. versionadded:: 3.1
963977

978+
.. opcode:: JUMP_IF_NOT_EXC_MATCH (target)
979+
980+
Tests whether the second value on the stack is an exception matching TOS,
981+
and jumps if it is not. Pops two values from the stack.
982+
983+
.. versionadded:: 3.9
984+
964985

965986
.. opcode:: JUMP_IF_TRUE_OR_POP (target)
966987

Include/opcode.h

Lines changed: 3 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Lib/importlib/_bootstrap_external.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,7 @@ def _write_atomic(path, data, mode=0o666):
274274
# Python 3.9a0 3420 (add LOAD_ASSERTION_ERROR #34880)
275275
# Python 3.9a0 3421 (simplified bytecode for with blocks #32949)
276276
# Python 3.9a0 3422 (remove BEGIN_FINALLY, END_FINALLY, CALL_FINALLY, POP_FINALLY bytecodes #33387)
277+
# Python 3.9a2 3423 (add IS_OP, CONTAINS_OP and JUMP_IF_NOT_EXC_MATCH bytecodes #39156)
277278
#
278279
# MAGIC must change whenever the bytecode emitted by the compiler may no
279280
# longer be understood by older implementations of the eval loop (usually
@@ -282,7 +283,7 @@ def _write_atomic(path, data, mode=0o666):
282283
# Whenever MAGIC_NUMBER is changed, the ranges in the magic_values array
283284
# in PC/launcher.c must also be updated.
284285

285-
MAGIC_NUMBER = (3422).to_bytes(2, 'little') + b'\r\n'
286+
MAGIC_NUMBER = (3423).to_bytes(2, 'little') + b'\r\n'
286287
_RAW_MAGIC_NUMBER = int.from_bytes(MAGIC_NUMBER, 'little') # For import.c
287288

288289
_PYCACHE = '__pycache__'

Lib/opcode.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,7 @@
2121
except ImportError:
2222
pass
2323

24-
cmp_op = ('<', '<=', '==', '!=', '>', '>=', 'in', 'not in', 'is',
25-
'is not', 'exception match', 'BAD')
24+
cmp_op = ('<', '<=', '==', '!=', '>', '>=')
2625

2726
hasconst = []
2827
hasname = []
@@ -159,6 +158,10 @@ def jabs_op(name, op):
159158

160159
name_op('LOAD_GLOBAL', 116) # Index in name list
161160

161+
def_op('IS_OP', 117)
162+
def_op('CONTAINS_OP', 118)
163+
164+
jabs_op('JUMP_IF_NOT_EXC_MATCH', 121)
162165
jrel_op('SETUP_FINALLY', 122) # Distance to target address
163166

164167
def_op('LOAD_FAST', 124) # Local variable number

Lib/test/test_dis.py

Lines changed: 70 additions & 71 deletions
Large diffs are not rendered by default.

Lib/test/test_peepholer.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -65,14 +65,14 @@ def unot(x):
6565
self.check_lnotab(unot)
6666

6767
def test_elim_inversion_of_is_or_in(self):
68-
for line, cmp_op in (
69-
('not a is b', 'is not',),
70-
('not a in b', 'not in',),
71-
('not a is not b', 'is',),
72-
('not a not in b', 'in',),
68+
for line, cmp_op, invert in (
69+
('not a is b', 'IS_OP', 1,),
70+
('not a is not b', 'IS_OP', 0,),
71+
('not a in b', 'CONTAINS_OP', 1,),
72+
('not a not in b', 'CONTAINS_OP', 0,),
7373
):
7474
code = compile(line, '', 'single')
75-
self.assertInBytecode(code, 'COMPARE_OP', cmp_op)
75+
self.assertInBytecode(code, cmp_op, invert)
7676
self.check_lnotab(code)
7777

7878
def test_global_as_constant(self):

Lib/test/test_positional_only_arg.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -434,11 +434,11 @@ def g():
434434
def f(x: not (int is int), /): ...
435435

436436
# without constant folding we end up with
437-
# COMPARE_OP(is), UNARY_NOT
438-
# with constant folding we should expect a COMPARE_OP(is not)
437+
# COMPARE_OP(is), IS_OP (0)
438+
# with constant folding we should expect a IS_OP (1)
439439
codes = [(i.opname, i.argval) for i in dis.get_instructions(g)]
440440
self.assertNotIn(('UNARY_NOT', None), codes)
441-
self.assertIn(('COMPARE_OP', 'is not'), codes)
441+
self.assertIn(('IS_OP', 1), codes)
442442

443443

444444
if __name__ == "__main__":
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
Split the COMPARE_OP bytecode instruction into four distinct instructions.
2+
3+
* COMPARE_OP for rich comparisons
4+
* IS_OP for 'is' and 'is not' tests
5+
* CONTAINS_OP for 'in' and 'is not' tests
6+
* JUMP_IF_NOT_EXC_MATCH for checking exceptions in 'try-except' statements.
7+
8+
This improves the clarity of the interpreter and should provide a modest
9+
speedup.

PC/launcher.c

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1247,6 +1247,7 @@ static PYC_MAGIC magic_values[] = {
12471247
{ 3360, 3379, L"3.6" },
12481248
{ 3390, 3399, L"3.7" },
12491249
{ 3400, 3419, L"3.8" },
1250+
{ 3420, 3429, L"3.9" },
12501251
{ 0 }
12511252
};
12521253

@@ -1830,7 +1831,7 @@ process(int argc, wchar_t ** argv)
18301831

18311832
#if !defined(VENV_REDIRECT)
18321833
/* bpo-35811: The __PYVENV_LAUNCHER__ variable is used to
1833-
* override sys.executable and locate the original prefix path.
1834+
* override sys.executable and locate the original prefix path.
18341835
* However, if it is silently inherited by a non-venv Python
18351836
* process, that process will believe it is running in the venv
18361837
* still. This is the only place where *we* can clear it (that is,

Python/ceval.c

Lines changed: 78 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,6 @@ static void maybe_dtrace_line(PyFrameObject *, int *, int *, int *);
6767
static void dtrace_function_entry(PyFrameObject *);
6868
static void dtrace_function_return(PyFrameObject *);
6969

70-
static PyObject * cmp_outcome(PyThreadState *, int, PyObject *, PyObject *);
7170
static PyObject * import_name(PyThreadState *, PyFrameObject *,
7271
PyObject *, PyObject *, PyObject *);
7372
static PyObject * import_from(PyThreadState *, PyObject *, PyObject *);
@@ -2897,19 +2896,95 @@ _PyEval_EvalFrameDefault(PyFrameObject *f, int throwflag)
28972896
}
28982897

28992898
case TARGET(COMPARE_OP): {
2899+
assert(oparg <= Py_GE);
29002900
PyObject *right = POP();
29012901
PyObject *left = TOP();
2902-
PyObject *res = cmp_outcome(tstate, oparg, left, right);
2902+
PyObject *res = PyObject_RichCompare(left, right, oparg);
2903+
SET_TOP(res);
29032904
Py_DECREF(left);
29042905
Py_DECREF(right);
2905-
SET_TOP(res);
29062906
if (res == NULL)
29072907
goto error;
29082908
PREDICT(POP_JUMP_IF_FALSE);
29092909
PREDICT(POP_JUMP_IF_TRUE);
29102910
DISPATCH();
29112911
}
29122912

2913+
case TARGET(IS_OP): {
2914+
PyObject *right = POP();
2915+
PyObject *left = TOP();
2916+
int res = (left == right)^oparg;
2917+
PyObject *b = res ? Py_True : Py_False;
2918+
Py_INCREF(b);
2919+
SET_TOP(b);
2920+
Py_DECREF(left);
2921+
Py_DECREF(right);
2922+
PREDICT(POP_JUMP_IF_FALSE);
2923+
PREDICT(POP_JUMP_IF_TRUE);
2924+
FAST_DISPATCH();
2925+
}
2926+
2927+
case TARGET(CONTAINS_OP): {
2928+
PyObject *right = POP();
2929+
PyObject *left = POP();
2930+
int res = PySequence_Contains(right, left);
2931+
Py_DECREF(left);
2932+
Py_DECREF(right);
2933+
if (res < 0) {
2934+
goto error;
2935+
}
2936+
PyObject *b = (res^oparg) ? Py_True : Py_False;
2937+
Py_INCREF(b);
2938+
PUSH(b);
2939+
PREDICT(POP_JUMP_IF_FALSE);
2940+
PREDICT(POP_JUMP_IF_TRUE);
2941+
FAST_DISPATCH();
2942+
}
2943+
2944+
#define CANNOT_CATCH_MSG "catching classes that do not inherit from "\
2945+
"BaseException is not allowed"
2946+
2947+
case TARGET(JUMP_IF_NOT_EXC_MATCH): {
2948+
PyObject *right = POP();
2949+
PyObject *left = POP();
2950+
if (PyTuple_Check(right)) {
2951+
Py_ssize_t i, length;
2952+
length = PyTuple_GET_SIZE(right);
2953+
for (i = 0; i < length; i++) {
2954+
PyObject *exc = PyTuple_GET_ITEM(right, i);
2955+
if (!PyExceptionClass_Check(exc)) {
2956+
_PyErr_SetString(tstate, PyExc_TypeError,
2957+
CANNOT_CATCH_MSG);
2958+
Py_DECREF(left);
2959+
Py_DECREF(right);
2960+
goto error;
2961+
}
2962+
}
2963+
}
2964+
else {
2965+
if (!PyExceptionClass_Check(right)) {
2966+
_PyErr_SetString(tstate, PyExc_TypeError,
2967+
CANNOT_CATCH_MSG);
2968+
Py_DECREF(left);
2969+
Py_DECREF(right);
2970+
goto error;
2971+
}
2972+
}
2973+
int res = PyErr_GivenExceptionMatches(left, right);
2974+
Py_DECREF(left);
2975+
Py_DECREF(right);
2976+
if (res > 0) {
2977+
/* Exception matches -- Do nothing */;
2978+
}
2979+
else if (res == 0) {
2980+
JUMPTO(oparg);
2981+
}
2982+
else {
2983+
goto error;
2984+
}
2985+
DISPATCH();
2986+
}
2987+
29132988
case TARGET(IMPORT_NAME): {
29142989
PyObject *name = GETITEM(names, oparg);
29152990
PyObject *fromlist = POP();
@@ -4951,62 +5026,6 @@ _PyEval_SliceIndexNotNone(PyObject *v, Py_ssize_t *pi)
49515026
return 1;
49525027
}
49535028

4954-
4955-
#define CANNOT_CATCH_MSG "catching classes that do not inherit from "\
4956-
"BaseException is not allowed"
4957-
4958-
static PyObject *
4959-
cmp_outcome(PyThreadState *tstate, int op, PyObject *v, PyObject *w)
4960-
{
4961-
int res = 0;
4962-
switch (op) {
4963-
case PyCmp_IS:
4964-
res = (v == w);
4965-
break;
4966-
case PyCmp_IS_NOT:
4967-
res = (v != w);
4968-
break;
4969-
case PyCmp_IN:
4970-
res = PySequence_Contains(w, v);
4971-
if (res < 0)
4972-
return NULL;
4973-
break;
4974-
case PyCmp_NOT_IN:
4975-
res = PySequence_Contains(w, v);
4976-
if (res < 0)
4977-
return NULL;
4978-
res = !res;
4979-
break;
4980-
case PyCmp_EXC_MATCH:
4981-
if (PyTuple_Check(w)) {
4982-
Py_ssize_t i, length;
4983-
length = PyTuple_Size(w);
4984-
for (i = 0; i < length; i += 1) {
4985-
PyObject *exc = PyTuple_GET_ITEM(w, i);
4986-
if (!PyExceptionClass_Check(exc)) {
4987-
_PyErr_SetString(tstate, PyExc_TypeError,
4988-
CANNOT_CATCH_MSG);
4989-
return NULL;
4990-
}
4991-
}
4992-
}
4993-
else {
4994-
if (!PyExceptionClass_Check(w)) {
4995-
_PyErr_SetString(tstate, PyExc_TypeError,
4996-
CANNOT_CATCH_MSG);
4997-
return NULL;
4998-
}
4999-
}
5000-
res = PyErr_GivenExceptionMatches(v, w);
5001-
break;
5002-
default:
5003-
return PyObject_RichCompare(v, w, op);
5004-
}
5005-
v = res ? Py_True : Py_False;
5006-
Py_INCREF(v);
5007-
return v;
5008-
}
5009-
50105029
static PyObject *
50115030
import_name(PyThreadState *tstate, PyFrameObject *f,
50125031
PyObject *name, PyObject *fromlist, PyObject *level)

0 commit comments

Comments
 (0)