From 04b4a847f2c0b98f9fe477491e5695ff07ed3bb9 Mon Sep 17 00:00:00 2001 From: Yan Yanchii Date: Fri, 14 Feb 2025 22:53:30 +0100 Subject: [PATCH 01/34] move binop folding to cfg --- Python/ast_opt.c | 289 ++++++++++++++++++--------------------------- Python/flowgraph.c | 184 ++++++++++++++++++++++++++++- 2 files changed, 290 insertions(+), 183 deletions(-) diff --git a/Python/ast_opt.c b/Python/ast_opt.c index 0b58e8cd2a2ced..263c70fc94c946 100644 --- a/Python/ast_opt.c +++ b/Python/ast_opt.c @@ -136,129 +136,6 @@ fold_unaryop(expr_ty node, PyArena *arena, _PyASTOptimizeState *state) return make_const(node, newval, arena); } -/* Check whether a collection doesn't containing too much items (including - subcollections). This protects from creating a constant that needs - too much time for calculating a hash. - "limit" is the maximal number of items. - Returns the negative number if the total number of items exceeds the - limit. Otherwise returns the limit minus the total number of items. -*/ - -static Py_ssize_t -check_complexity(PyObject *obj, Py_ssize_t limit) -{ - if (PyTuple_Check(obj)) { - Py_ssize_t i; - limit -= PyTuple_GET_SIZE(obj); - for (i = 0; limit >= 0 && i < PyTuple_GET_SIZE(obj); i++) { - limit = check_complexity(PyTuple_GET_ITEM(obj, i), limit); - } - return limit; - } - return limit; -} - -#define MAX_INT_SIZE 128 /* bits */ -#define MAX_COLLECTION_SIZE 256 /* items */ -#define MAX_STR_SIZE 4096 /* characters */ -#define MAX_TOTAL_ITEMS 1024 /* including nested collections */ - -static PyObject * -safe_multiply(PyObject *v, PyObject *w) -{ - if (PyLong_Check(v) && PyLong_Check(w) && - !_PyLong_IsZero((PyLongObject *)v) && !_PyLong_IsZero((PyLongObject *)w) - ) { - int64_t vbits = _PyLong_NumBits(v); - int64_t wbits = _PyLong_NumBits(w); - assert(vbits >= 0); - assert(wbits >= 0); - if (vbits + wbits > MAX_INT_SIZE) { - return NULL; - } - } - else if (PyLong_Check(v) && PyTuple_Check(w)) { - Py_ssize_t size = PyTuple_GET_SIZE(w); - if (size) { - long n = PyLong_AsLong(v); - if (n < 0 || n > MAX_COLLECTION_SIZE / size) { - return NULL; - } - if (n && check_complexity(w, MAX_TOTAL_ITEMS / n) < 0) { - return NULL; - } - } - } - else if (PyLong_Check(v) && (PyUnicode_Check(w) || PyBytes_Check(w))) { - Py_ssize_t size = PyUnicode_Check(w) ? PyUnicode_GET_LENGTH(w) : - PyBytes_GET_SIZE(w); - if (size) { - long n = PyLong_AsLong(v); - if (n < 0 || n > MAX_STR_SIZE / size) { - return NULL; - } - } - } - else if (PyLong_Check(w) && - (PyTuple_Check(v) || PyUnicode_Check(v) || PyBytes_Check(v))) - { - return safe_multiply(w, v); - } - - return PyNumber_Multiply(v, w); -} - -static PyObject * -safe_power(PyObject *v, PyObject *w) -{ - if (PyLong_Check(v) && PyLong_Check(w) && - !_PyLong_IsZero((PyLongObject *)v) && _PyLong_IsPositive((PyLongObject *)w) - ) { - int64_t vbits = _PyLong_NumBits(v); - size_t wbits = PyLong_AsSize_t(w); - assert(vbits >= 0); - if (wbits == (size_t)-1) { - return NULL; - } - if ((uint64_t)vbits > MAX_INT_SIZE / wbits) { - return NULL; - } - } - - return PyNumber_Power(v, w, Py_None); -} - -static PyObject * -safe_lshift(PyObject *v, PyObject *w) -{ - if (PyLong_Check(v) && PyLong_Check(w) && - !_PyLong_IsZero((PyLongObject *)v) && !_PyLong_IsZero((PyLongObject *)w) - ) { - int64_t vbits = _PyLong_NumBits(v); - size_t wbits = PyLong_AsSize_t(w); - assert(vbits >= 0); - if (wbits == (size_t)-1) { - return NULL; - } - if (wbits > MAX_INT_SIZE || (uint64_t)vbits > MAX_INT_SIZE - wbits) { - return NULL; - } - } - - return PyNumber_Lshift(v, w); -} - -static PyObject * -safe_mod(PyObject *v, PyObject *w) -{ - if (PyUnicode_Check(v) || PyBytes_Check(v)) { - return NULL; - } - - return PyNumber_Remainder(v, w); -} - - static expr_ty parse_literal(PyObject *fmt, Py_ssize_t *ppos, PyArena *arena) { @@ -478,58 +355,7 @@ fold_binop(expr_ty node, PyArena *arena, _PyASTOptimizeState *state) return optimize_format(node, lv, rhs->v.Tuple.elts, arena); } - if (rhs->kind != Constant_kind) { - return 1; - } - - PyObject *rv = rhs->v.Constant.value; - PyObject *newval = NULL; - - switch (node->v.BinOp.op) { - case Add: - newval = PyNumber_Add(lv, rv); - break; - case Sub: - newval = PyNumber_Subtract(lv, rv); - break; - case Mult: - newval = safe_multiply(lv, rv); - break; - case Div: - newval = PyNumber_TrueDivide(lv, rv); - break; - case FloorDiv: - newval = PyNumber_FloorDivide(lv, rv); - break; - case Mod: - newval = safe_mod(lv, rv); - break; - case Pow: - newval = safe_power(lv, rv); - break; - case LShift: - newval = safe_lshift(lv, rv); - break; - case RShift: - newval = PyNumber_Rshift(lv, rv); - break; - case BitOr: - newval = PyNumber_Or(lv, rv); - break; - case BitXor: - newval = PyNumber_Xor(lv, rv); - break; - case BitAnd: - newval = PyNumber_And(lv, rv); - break; - // No builtin constants implement the following operators - case MatMult: - return 1; - // No default case, so the compiler will emit a warning if new binary - // operators are added without being handled here - } - - return make_const(node, newval, arena); + return 1; } static PyObject* @@ -971,6 +797,115 @@ astfold_withitem(withitem_ty node_, PyArena *ctx_, _PyASTOptimizeState *state) return 1; } +#define IS_CONST_EXPR(N) \ + ((N)->kind == Constant_kind) + +#define CONST_EXPR_VALUE(N) \ + ((N)->v.Constant.value) + +#define IS_COMPLEX_CONST_EXPR(N) \ + (IS_CONST_EXPR(N) && PyComplex_CheckExact(CONST_EXPR_VALUE(N))) + +#define IS_NUMERIC_CONST_EXPR(N) \ + (IS_CONST_EXPR(N) && (PyLong_CheckExact(CONST_EXPR_VALUE(N)) || PyFloat_CheckExact(CONST_EXPR_VALUE(N)))) + +#define IS_UNARY_EXPR(N) \ + ((N)->kind == UnaryOp_kind) + +#define UNARY_EXPR_OP(N) \ + ((N)->v.UnaryOp.op) + +#define UNARY_EXPR_OPERAND(N) \ + ((N)->v.UnaryOp.operand) + +#define UNARY_EXPR_OPERAND_CONST_VALUE(N) \ + (CONST_EXPR_VALUE(UNARY_EXPR_OPERAND(N))) + +#define IS_UNARY_SUB_EXPR(N) \ + (IS_UNARY_EXPR(N) && UNARY_EXPR_OP(N) == USub) + +#define IS_NUMERIC_UNARY_CONST_EXPR(N) \ + (IS_UNARY_SUB_EXPR(N) && IS_NUMERIC_CONST_EXPR(UNARY_EXPR_OPERAND(N))) + +#define IS_COMPLEX_UNARY_CONST_EXPR(N) \ + (IS_UNARY_SUB_EXPR(N) && IS_COMPLEX_CONST_EXPR(UNARY_EXPR_OPERAND(N))) + +#define BINARY_EXPR(N) \ + ((N)->v.BinOp) + +#define BINARY_EXPR_OP(N) \ + (BINARY_EXPR(N).op) + +#define BINARY_EXPR_LEFT(N) \ + (BINARY_EXPR(N).left) + +#define BINARY_EXPR_RIGHT(N) \ + (BINARY_EXPR(N).right) + +#define IS_BINARY_EXPR(N) \ + ((N)->kind == BinOp_kind) + +#define IS_BINARY_ADD_EXPR(N) \ + (IS_BINARY_EXPR(N) && BINARY_EXPR_OP(N) == Add) + +#define IS_BINARY_SUB_EXPR(N) \ + (IS_BINARY_EXPR(N) && BINARY_EXPR_OP(N) == Sub) + +#define IS_MATCH_NUMERIC_OR_COMPLEX_UNARY_CONST_EXPR(N) \ + (IS_NUMERIC_UNARY_CONST_EXPR(N) || IS_COMPLEX_UNARY_CONST_EXPR(N)) + +#define IS_MATCH_COMPLEX_BINARY_CONST_EXPR(N) \ + ( \ + (IS_BINARY_ADD_EXPR(N) || IS_BINARY_SUB_EXPR(N)) \ + && (IS_NUMERIC_UNARY_CONST_EXPR(BINARY_EXPR_LEFT(N)) || IS_CONST_EXPR(BINARY_EXPR_LEFT(N))) \ + && IS_COMPLEX_CONST_EXPR(BINARY_EXPR_RIGHT(N)) \ + ) + + +static int +fold_const_unary_or_complex_expr(expr_ty e, PyArena *arena) +{ + assert(IS_MATCH_NUMERIC_OR_COMPLEX_UNARY_CONST_EXPR(e)); + PyObject *constant = UNARY_EXPR_OPERAND_CONST_VALUE(e); + assert(UNARY_EXPR_OP(e) == USub); + PyObject* folded = PyNumber_Negative(constant); + return make_const(e, folded, arena); +} + +static int +fold_const_binary_complex_expr(expr_ty e, PyArena *arena) +{ + assert(IS_MATCH_COMPLEX_BINARY_CONST_EXPR(e)); + expr_ty left_expr = BINARY_EXPR_LEFT(e); + if (IS_NUMERIC_UNARY_CONST_EXPR(left_expr)) { + if (!fold_const_unary_or_complex_expr(left_expr, arena)) { + return 0; + } + } + assert(IS_CONST_EXPR(BINARY_EXPR_LEFT(e))); + operator_ty op = BINARY_EXPR_OP(e); + PyObject *left = CONST_EXPR_VALUE(BINARY_EXPR_LEFT(e)); + PyObject *right = CONST_EXPR_VALUE(BINARY_EXPR_RIGHT(e)); + assert(op == Add || op == Sub); + PyObject *folded = op == Add ? PyNumber_Add(left, right) : PyNumber_Subtract(left, right); + return make_const(e, folded, arena); +} + +static int +fold_pattern_match_value(expr_ty node, PyArena *arena, _PyASTOptimizeState *Py_UNUSED(state)) +{ + switch (node->kind) + { + case UnaryOp_kind: + return fold_const_unary_or_complex_expr(node, arena); + case BinOp_kind: + return fold_const_binary_complex_expr(node, arena); + default: + break; + } + return 1; +} + static int astfold_pattern(pattern_ty node_, PyArena *ctx_, _PyASTOptimizeState *state) { @@ -980,7 +915,7 @@ astfold_pattern(pattern_ty node_, PyArena *ctx_, _PyASTOptimizeState *state) ENTER_RECURSIVE(state); switch (node_->kind) { case MatchValue_kind: - CALL(astfold_expr, expr_ty, node_->v.MatchValue.value); + CALL(fold_pattern_match_value, expr_ty, node_->v.MatchValue.value); break; case MatchSingleton_kind: break; @@ -988,7 +923,7 @@ astfold_pattern(pattern_ty node_, PyArena *ctx_, _PyASTOptimizeState *state) CALL_SEQ(astfold_pattern, pattern, node_->v.MatchSequence.patterns); break; case MatchMapping_kind: - CALL_SEQ(astfold_expr, expr, node_->v.MatchMapping.keys); + CALL_SEQ(fold_pattern_match_value, expr, node_->v.MatchMapping.keys); CALL_SEQ(astfold_pattern, pattern, node_->v.MatchMapping.patterns); break; case MatchClass_kind: diff --git a/Python/flowgraph.c b/Python/flowgraph.c index 38fb40831f3735..4a96e100b35713 100644 --- a/Python/flowgraph.c +++ b/Python/flowgraph.c @@ -1517,15 +1517,188 @@ newop_from_folded(PyObject *newconst, PyObject *consts, return SUCCESS; } +/* Check whether a collection doesn't containing too much items (including + subcollections). This protects from creating a constant that needs + too much time for calculating a hash. + "limit" is the maximal number of items. + Returns the negative number if the total number of items exceeds the + limit. Otherwise returns the limit minus the total number of items. +*/ +static Py_ssize_t +check_complexity(PyObject *obj, Py_ssize_t limit) +{ + if (PyTuple_Check(obj)) { + Py_ssize_t i; + limit -= PyTuple_GET_SIZE(obj); + for (i = 0; limit >= 0 && i < PyTuple_GET_SIZE(obj); i++) { + limit = check_complexity(PyTuple_GET_ITEM(obj, i), limit); + } + return limit; + } + return limit; +} + +#define MAX_INT_SIZE 128 /* bits */ +#define MAX_COLLECTION_SIZE 256 /* items */ +#define MAX_STR_SIZE 4096 /* characters */ +#define MAX_TOTAL_ITEMS 1024 /* including nested collections */ + +static PyObject * +safe_multiply(PyObject *v, PyObject *w) +{ + if (PyLong_Check(v) && PyLong_Check(w) && + !_PyLong_IsZero((PyLongObject *)v) && !_PyLong_IsZero((PyLongObject *)w) + ) { + int64_t vbits = _PyLong_NumBits(v); + int64_t wbits = _PyLong_NumBits(w); + assert(vbits >= 0); + assert(wbits >= 0); + if (vbits + wbits > MAX_INT_SIZE) { + return NULL; + } + } + else if (PyLong_Check(v) && PyTuple_Check(w)) { + Py_ssize_t size = PyTuple_GET_SIZE(w); + if (size) { + long n = PyLong_AsLong(v); + if (n < 0 || n > MAX_COLLECTION_SIZE / size) { + return NULL; + } + if (n && check_complexity(w, MAX_TOTAL_ITEMS / n) < 0) { + return NULL; + } + } + } + else if (PyLong_Check(v) && (PyUnicode_Check(w) || PyBytes_Check(w))) { + Py_ssize_t size = PyUnicode_Check(w) ? PyUnicode_GET_LENGTH(w) : + PyBytes_GET_SIZE(w); + if (size) { + long n = PyLong_AsLong(v); + if (n < 0 || n > MAX_STR_SIZE / size) { + return NULL; + } + } + } + else if (PyLong_Check(w) && + (PyTuple_Check(v) || PyUnicode_Check(v) || PyBytes_Check(v))) + { + return safe_multiply(w, v); + } + + return PyNumber_Multiply(v, w); +} + +static PyObject * +safe_power(PyObject *v, PyObject *w) +{ + if (PyLong_Check(v) && PyLong_Check(w) && + !_PyLong_IsZero((PyLongObject *)v) && _PyLong_IsPositive((PyLongObject *)w) + ) { + int64_t vbits = _PyLong_NumBits(v); + size_t wbits = PyLong_AsSize_t(w); + assert(vbits >= 0); + if (wbits == (size_t)-1) { + return NULL; + } + if ((uint64_t)vbits > MAX_INT_SIZE / wbits) { + return NULL; + } + } + + return PyNumber_Power(v, w, Py_None); +} + +static PyObject * +safe_lshift(PyObject *v, PyObject *w) +{ + if (PyLong_Check(v) && PyLong_Check(w) && + !_PyLong_IsZero((PyLongObject *)v) && !_PyLong_IsZero((PyLongObject *)w) + ) { + int64_t vbits = _PyLong_NumBits(v); + size_t wbits = PyLong_AsSize_t(w); + assert(vbits >= 0); + if (wbits == (size_t)-1) { + return NULL; + } + if (wbits > MAX_INT_SIZE || (uint64_t)vbits > MAX_INT_SIZE - wbits) { + return NULL; + } + } + + return PyNumber_Lshift(v, w); +} + +static PyObject * +safe_mod(PyObject *v, PyObject *w) +{ + if (PyUnicode_Check(v) || PyBytes_Check(v)) { + return NULL; + } + + return PyNumber_Remainder(v, w); +} + +static PyObject * +eval_const_binop(PyObject *left, int op, PyObject *right) +{ + assert(left != NULL && right != NULL); + assert(op >= 0 && op <= NB_OPARG_LAST); + + PyObject *result = NULL; + switch (op) { + case NB_ADD: + result = PyNumber_Add(left, right); + break; + case NB_SUBTRACT: + result = PyNumber_Subtract(left, right); + break; + case NB_MULTIPLY: + result = safe_multiply(left, right); + break; + case NB_TRUE_DIVIDE: + result = PyNumber_TrueDivide(left, right); + break; + case NB_FLOOR_DIVIDE: + result = PyNumber_FloorDivide(left, right); + break; + case NB_REMAINDER: + result = safe_mod(left, right); + break; + case NB_POWER: + result = safe_power(left, right); + break; + case NB_LSHIFT: + result = safe_lshift(left, right); + break; + case NB_RSHIFT: + result = PyNumber_Rshift(left, right); + break; + case NB_OR: + result = PyNumber_Or(left, right); + break; + case NB_XOR: + result = PyNumber_Xor(left, right); + break; + case NB_AND: + result = PyNumber_And(left, right); + break; + case NB_SUBSCR: + result = PyObject_GetItem(left, right); + break; + case NB_MATRIX_MULTIPLY: + // No builtin constants implement matrix multiplication + break; + default: + Py_UNREACHABLE(); + } + return result; +} + static int optimize_if_const_binop(basicblock *bb, int i, PyObject *consts, PyObject *const_cache) { cfg_instr *binop = &bb->b_instr[i]; assert(binop->i_opcode == BINARY_OP); - if (binop->i_oparg != NB_SUBSCR) { - /* TODO: support other binary ops */ - return SUCCESS; - } PyObject *pair; RETURN_IF_ERROR(get_constant_sequence(bb, i-1, 2, consts, &pair)); if (pair == NULL) { @@ -1534,8 +1707,7 @@ optimize_if_const_binop(basicblock *bb, int i, PyObject *consts, PyObject *const assert(PyTuple_CheckExact(pair) && PyTuple_Size(pair) == 2); PyObject *left = PyTuple_GET_ITEM(pair, 0); PyObject *right = PyTuple_GET_ITEM(pair, 1); - assert(left != NULL && right != NULL); - PyObject *newconst = PyObject_GetItem(left, right); + PyObject *newconst = eval_const_binop(left, binop->i_oparg, right); Py_DECREF(pair); if (newconst == NULL) { if (PyErr_ExceptionMatches(PyExc_KeyboardInterrupt)) { From a6babab7964aec56f321d9e2127c843b74f20dc2 Mon Sep 17 00:00:00 2001 From: Yan Yanchii Date: Fri, 14 Feb 2025 22:53:39 +0100 Subject: [PATCH 02/34] update tests --- Lib/test/test_ast/test_ast.py | 65 ++++++++++------------------------- Lib/test/test_ast/utils.py | 2 +- Lib/test/test_builtin.py | 15 ++++---- Lib/test/test_peepholer.py | 9 ----- 4 files changed, 26 insertions(+), 65 deletions(-) diff --git a/Lib/test/test_ast/test_ast.py b/Lib/test/test_ast/test_ast.py index 434f291eebe51b..136bb128ce4977 100644 --- a/Lib/test/test_ast/test_ast.py +++ b/Lib/test/test_ast/test_ast.py @@ -147,18 +147,17 @@ def test_optimization_levels__debug__(self): self.assertEqual(res.body[0].value.id, expected) def test_optimization_levels_const_folding(self): - folded = ('Expr', (1, 0, 1, 5), ('Constant', (1, 0, 1, 5), 3, None)) - not_folded = ('Expr', (1, 0, 1, 5), - ('BinOp', (1, 0, 1, 5), - ('Constant', (1, 0, 1, 1), 1, None), - ('Add',), - ('Constant', (1, 4, 1, 5), 2, None))) + folded = ('Expr', (1, 0, 1, 6), ('Constant', (1, 0, 1, 6), (1, 2), None)) + not_folded = ('Expr', (1, 0, 1, 6), + ('Tuple', (1, 0, 1, 6), + [('Constant', (1, 1, 1, 2), 1, None), + ('Constant', (1, 4, 1, 5), 2, None)], ('Load',))) cases = [(-1, not_folded), (0, not_folded), (1, folded), (2, folded)] for (optval, expected) in cases: with self.subTest(optval=optval): - tree1 = ast.parse("1 + 2", optimize=optval) - tree2 = ast.parse(ast.parse("1 + 2"), optimize=optval) + tree1 = ast.parse("(1, 2)", optimize=optval) + tree2 = ast.parse(ast.parse("(1, 2)"), optimize=optval) for tree in [tree1, tree2]: res = to_tuple(tree.body[0]) self.assertEqual(res, expected) @@ -3134,32 +3133,6 @@ def assert_ast(self, code, non_optimized_target, optimized_target): f"{ast.dump(optimized_tree)}", ) - def create_binop(self, operand, left=ast.Constant(1), right=ast.Constant(1)): - return ast.BinOp(left=left, op=self.binop[operand], right=right) - - def test_folding_binop(self): - code = "1 %s 1" - operators = self.binop.keys() - - for op in operators: - result_code = code % op - non_optimized_target = self.wrap_expr(self.create_binop(op)) - optimized_target = self.wrap_expr(ast.Constant(value=eval(result_code))) - - with self.subTest( - result_code=result_code, - non_optimized_target=non_optimized_target, - optimized_target=optimized_target - ): - self.assert_ast(result_code, non_optimized_target, optimized_target) - - # Multiplication of constant tuples must be folded - code = "(1,) * 3" - non_optimized_target = self.wrap_expr(self.create_binop("*", ast.Tuple(elts=[ast.Constant(value=1)]), ast.Constant(value=3))) - optimized_target = self.wrap_expr(ast.Constant(eval(code))) - - self.assert_ast(code, non_optimized_target, optimized_target) - def test_folding_unaryop(self): code = "%s1" operators = self.unaryop.keys() @@ -3240,9 +3213,9 @@ def test_folding_tuple(self): self.assert_ast(code, non_optimized_target, optimized_target) def test_folding_type_param_in_function_def(self): - code = "def foo[%s = 1 + 1](): pass" + code = "def foo[%s = (1, 2)](): pass" - unoptimized_binop = self.create_binop("+") + unoptimized_tuple = ast.Tuple(elts=[ast.Constant(1), ast.Constant(2)]) unoptimized_type_params = [ ("T", "T", ast.TypeVar), ("**P", "P", ast.ParamSpec), @@ -3256,7 +3229,7 @@ def test_folding_type_param_in_function_def(self): name='foo', args=ast.arguments(), body=[ast.Pass()], - type_params=[type_param(name=name, default_value=ast.Constant(2))] + type_params=[type_param(name=name, default_value=ast.Constant((1, 2)))] ) ) non_optimized_target = self.wrap_statement( @@ -3264,15 +3237,15 @@ def test_folding_type_param_in_function_def(self): name='foo', args=ast.arguments(), body=[ast.Pass()], - type_params=[type_param(name=name, default_value=unoptimized_binop)] + type_params=[type_param(name=name, default_value=unoptimized_tuple)] ) ) self.assert_ast(result_code, non_optimized_target, optimized_target) def test_folding_type_param_in_class_def(self): - code = "class foo[%s = 1 + 1]: pass" + code = "class foo[%s = (1, 2)]: pass" - unoptimized_binop = self.create_binop("+") + unoptimized_tuple = ast.Tuple(elts=[ast.Constant(1), ast.Constant(2)]) unoptimized_type_params = [ ("T", "T", ast.TypeVar), ("**P", "P", ast.ParamSpec), @@ -3285,22 +3258,22 @@ def test_folding_type_param_in_class_def(self): ast.ClassDef( name='foo', body=[ast.Pass()], - type_params=[type_param(name=name, default_value=ast.Constant(2))] + type_params=[type_param(name=name, default_value=ast.Constant((1, 2)))] ) ) non_optimized_target = self.wrap_statement( ast.ClassDef( name='foo', body=[ast.Pass()], - type_params=[type_param(name=name, default_value=unoptimized_binop)] + type_params=[type_param(name=name, default_value=unoptimized_tuple)] ) ) self.assert_ast(result_code, non_optimized_target, optimized_target) def test_folding_type_param_in_type_alias(self): - code = "type foo[%s = 1 + 1] = 1" + code = "type foo[%s = (1, 2)] = 1" - unoptimized_binop = self.create_binop("+") + unoptimized_tuple = ast.Tuple(elts=[ast.Constant(1), ast.Constant(2)]) unoptimized_type_params = [ ("T", "T", ast.TypeVar), ("**P", "P", ast.ParamSpec), @@ -3312,14 +3285,14 @@ def test_folding_type_param_in_type_alias(self): optimized_target = self.wrap_statement( ast.TypeAlias( name=ast.Name(id='foo', ctx=ast.Store()), - type_params=[type_param(name=name, default_value=ast.Constant(2))], + type_params=[type_param(name=name, default_value=ast.Constant((1, 2)))], value=ast.Constant(value=1), ) ) non_optimized_target = self.wrap_statement( ast.TypeAlias( name=ast.Name(id='foo', ctx=ast.Store()), - type_params=[type_param(name=name, default_value=unoptimized_binop)], + type_params=[type_param(name=name, default_value=unoptimized_tuple)], value=ast.Constant(value=1), ) ) diff --git a/Lib/test/test_ast/utils.py b/Lib/test/test_ast/utils.py index 145e89ee94e935..e7054f3f710f1f 100644 --- a/Lib/test/test_ast/utils.py +++ b/Lib/test/test_ast/utils.py @@ -1,5 +1,5 @@ def to_tuple(t): - if t is None or isinstance(t, (str, int, complex, float, bytes)) or t is Ellipsis: + if t is None or isinstance(t, (str, int, complex, float, bytes, tuple)) or t is Ellipsis: return t elif isinstance(t, list): return [to_tuple(e) for e in t] diff --git a/Lib/test/test_builtin.py b/Lib/test/test_builtin.py index 913d007a126d72..d15964fe9dd88b 100644 --- a/Lib/test/test_builtin.py +++ b/Lib/test/test_builtin.py @@ -555,7 +555,7 @@ def test_compile_async_generator(self): self.assertEqual(type(glob['ticker']()), AsyncGeneratorType) def test_compile_ast(self): - args = ("a*(1+2)", "f.py", "exec") + args = ("a*(1,2)", "f.py", "exec") raw = compile(*args, flags = ast.PyCF_ONLY_AST).body[0] opt1 = compile(*args, flags = ast.PyCF_OPTIMIZED_AST).body[0] opt2 = compile(ast.parse(args[0]), *args[1:], flags = ast.PyCF_OPTIMIZED_AST).body[0] @@ -566,17 +566,14 @@ def test_compile_ast(self): self.assertIsInstance(tree.value.left, ast.Name) self.assertEqual(tree.value.left.id, 'a') - raw_right = raw.value.right # expect BinOp(1, '+', 2) - self.assertIsInstance(raw_right, ast.BinOp) - self.assertIsInstance(raw_right.left, ast.Constant) - self.assertEqual(raw_right.left.value, 1) - self.assertIsInstance(raw_right.right, ast.Constant) - self.assertEqual(raw_right.right.value, 2) + raw_right = raw.value.right # expect Tuple((1, 2)) + self.assertIsInstance(raw_right, ast.Tuple) + self.assertListEqual([elt.value for elt in raw_right.elts], [1, 2]) for opt in [opt1, opt2]: - opt_right = opt.value.right # expect Constant(3) + opt_right = opt.value.right # expect Constant((1,2)) self.assertIsInstance(opt_right, ast.Constant) - self.assertEqual(opt_right.value, 3) + self.assertEqual(opt_right.value, (1, 2)) def test_delattr(self): sys.spam = 1 diff --git a/Lib/test/test_peepholer.py b/Lib/test/test_peepholer.py index 4471c5129b96df..457a727b679adc 100644 --- a/Lib/test/test_peepholer.py +++ b/Lib/test/test_peepholer.py @@ -311,7 +311,6 @@ def test_folding_of_unaryops_on_constants(self): for line, elem in ( ('-0.5', -0.5), # unary negative ('-0.0', -0.0), # -0.0 - ('-(1.0-1.0)', -0.0), # -0.0 after folding ('-0', 0), # -0 ('~-2', 1), # unary invert ('+1', 1), # unary positive @@ -326,14 +325,6 @@ def test_folding_of_unaryops_on_constants(self): self.assertFalse(instr.opname.startswith('UNARY_')) self.check_lnotab(code) - # Check that -0.0 works after marshaling - def negzero(): - return -(1.0-1.0) - - for instr in dis.get_instructions(negzero): - self.assertFalse(instr.opname.startswith('UNARY_')) - self.check_lnotab(negzero) - # Verify that unfoldables are skipped for line, elem, opname in ( ('-"abc"', 'abc', 'UNARY_NEGATIVE'), From 0fa7c4cb92aedb8dfa2900d36445932c7cbaa26f Mon Sep 17 00:00:00 2001 From: Yan Yanchii Date: Fri, 14 Feb 2025 23:46:23 +0100 Subject: [PATCH 03/34] add match case folding tests --- Lib/test/test_ast/test_ast.py | 83 ++++++++++++++++++++++++++++------- 1 file changed, 68 insertions(+), 15 deletions(-) diff --git a/Lib/test/test_ast/test_ast.py b/Lib/test/test_ast/test_ast.py index 136bb128ce4977..cfdb336d9325f2 100644 --- a/Lib/test/test_ast/test_ast.py +++ b/Lib/test/test_ast/test_ast.py @@ -3081,21 +3081,6 @@ def test_cli_file_input(self): class ASTOptimiziationTests(unittest.TestCase): - binop = { - "+": ast.Add(), - "-": ast.Sub(), - "*": ast.Mult(), - "/": ast.Div(), - "%": ast.Mod(), - "<<": ast.LShift(), - ">>": ast.RShift(), - "|": ast.BitOr(), - "^": ast.BitXor(), - "&": ast.BitAnd(), - "//": ast.FloorDiv(), - "**": ast.Pow(), - } - unaryop = { "~": ast.Invert(), "+": ast.UAdd(), @@ -3298,6 +3283,74 @@ def test_folding_type_param_in_type_alias(self): ) self.assert_ast(result_code, non_optimized_target, optimized_target) + def test_folding_match_case_allowed_expressions(self): + source = textwrap.dedent(""" + match 0: + case -0: pass + case -0.1: pass + case -0j: pass + case 1 + 2j: pass + case 1 - 2j: pass + case 1.1 + 2.1j: pass + case 1.1 - 2.1j: pass + case -0 + 1j: pass + case -0 - 1j: pass + case -0.1 + 1.1j: pass + case -0.1 - 1.1j: pass + case {-0: 0}: pass + case {-0.1: 0}: pass + case {-0j: 0}: pass + case {1 + 2j: 0}: pass + case {1 - 2j: 0}: pass + case {1.1 + 2.1j: 0}: pass + case {1.1 - 2.1j: 0}: pass + case {-0 + 1j: 0}: pass + case {-0 - 1j: 0}: pass + case {-0.1 + 1.1j: 0}: pass + case {-0.1 - 1.1j: 0}: pass + case {-0: 0, 0 + 1j: 0, 0.1 + 1j: 0}: pass + """) + expected_constants = ( + 0, + -0.1, + complex(0, -0), + complex(1, 2), + complex(1, -2), + complex(1.1, 2.1), + complex(1.1, -2.1), + complex(-0, 1), + complex(-0, -1), + complex(-0.1, 1.1), + complex(-0.1, -1.1), + (0, ), + (-0.1, ), + (complex(0, -0), ), + (complex(1, 2), ), + (complex(1, -2), ), + (complex(1.1, 2.1), ), + (complex(1.1, -2.1), ), + (complex(-0, 1), ), + (complex(-0, -1), ), + (complex(-0.1, 1.1), ), + (complex(-0.1, -1.1), ), + (0, complex(0, 1), complex(0.1, 1)) + ) + consts = iter(expected_constants) + tree = ast.parse(source, optimize=1) + match_stmt = tree.body[0] + for case in match_stmt.cases: + pattern = case.pattern + if isinstance(pattern, ast.MatchValue): + self.assertIsInstance(pattern.value, ast.Constant) + self.assertEqual(pattern.value.value, next(consts)) + elif isinstance(pattern, ast.MatchMapping): + keys = iter(next(consts)) + for key in pattern.keys: + self.assertIsInstance(key, ast.Constant) + self.assertEqual(key.value, next(keys)) + else: + self.fail(f"Expected ast.MatchValue or ast.MatchMapping, found: {type(pattern)}") + if __name__ == '__main__': if len(sys.argv) > 1 and sys.argv[1] == '--snapshot-update': From 27b1a09ac732a220f48385010a6d6f2259a00fe4 Mon Sep 17 00:00:00 2001 From: Yan Yanchii Date: Sat, 15 Feb 2025 09:08:40 +0100 Subject: [PATCH 04/34] add peepholer tests --- Lib/test/test_peepholer.py | 116 +++++++++++++++++++++++++++++-------- 1 file changed, 91 insertions(+), 25 deletions(-) diff --git a/Lib/test/test_peepholer.py b/Lib/test/test_peepholer.py index 457a727b679adc..31732f89d0ba71 100644 --- a/Lib/test/test_peepholer.py +++ b/Lib/test/test_peepholer.py @@ -479,10 +479,13 @@ def test_constant_folding_small_int(self): ('(1 + 2, )[0]', 3), ('(2 + 2 * 2, )[0]', 6), ('(1, (1 + 2 + 3, ))[1][0]', 6), + ('1 + 2', 3), + ('2 + 2 * 2 // 2 - 2', 2), ('(255, )[0]', 255), ('(256, )[0]', None), ('(1000, )[0]', None), ('(1 - 2, )[0]', None), + ('255 + 1', None), ] for expr, oparg in tests: with self.subTest(expr=expr, oparg=oparg): @@ -493,37 +496,85 @@ def test_constant_folding_small_int(self): self.assertNotInBytecode(code, 'LOAD_SMALL_INT') self.check_lnotab(code) - def test_folding_subscript(self): + def test_folding_binop(self): + add = get_binop_argval('NB_ADD') + sub = get_binop_argval('NB_SUBTRACT') + mul = get_binop_argval('NB_MULTIPLY') + div = get_binop_argval('NB_TRUE_DIVIDE') + floor = get_binop_argval('NB_FLOOR_DIVIDE') + rem = get_binop_argval('NB_REMAINDER') + pow = get_binop_argval('NB_POWER') + lshift = get_binop_argval('NB_LSHIFT') + rshift = get_binop_argval('NB_RSHIFT') + or_ = get_binop_argval('NB_OR') + and_ = get_binop_argval('NB_AND') + xor = get_binop_argval('NB_XOR') + subscr = get_binop_argval('NB_SUBSCR') tests = [ - ('(1, )[0]', False), - ('(1, )[-1]', False), - ('(1 + 2, )[0]', False), - ('(1, (1, 2))[1][1]', False), - ('(1, 2)[2-1]', False), - ('(1, (1, 2))[1][2-1]', False), - ('(1, (1, 2))[1:6][0][2-1]', False), - ('"a"[0]', False), - ('("a" + "b")[1]', False), - ('("a" + "b", )[0][1]', False), - ('("a" * 10)[9]', False), - ('(1, )[1]', True), - ('(1, )[-2]', True), - ('"a"[1]', True), - ('"a"[-2]', True), - ('("a" + "b")[2]', True), - ('("a" + "b", )[0][2]', True), - ('("a" + "b", )[1][0]', True), - ('("a" * 10)[10]', True), - ('(1, (1, 2))[2:6][0][2-1]', True), + ('1 + 2', False, add), + ('1 + 2 + 3', False, add), + ('1 + ""', True, add), + ('1 - 2', False, sub), + ('1 - 2 - 3', False, sub), + ('1 - ""', True, sub), + ('2 * 2', False, mul), + ('2 * 2 * 2', False, mul), + ('2 / 2', False, div), + ('2 / 2 / 2', False, div), + ('2 / ""', True, div), + ('2 // 2', False, floor), + ('2 // 2 // 2', False, floor), + ('2 // ""', True, floor), + ('2 % 2', False, rem), + ('2 % 2 % 2', False, rem), + ('2 % ()', True, rem), + ('2 ** 2', False, pow), + ('2 ** 2 ** 2', False, pow), + ('2 ** ""', True, pow), + ('2 << 2', False, lshift), + ('2 << 2 << 2', False, lshift), + ('2 << ""', True, lshift), + ('2 >> 2', False, rshift), + ('2 >> 2 >> 2', False, rshift), + ('2 >> ""', True, rshift), + ('2 | 2', False, or_), + ('2 | 2 | 2', False, or_), + ('2 | ""', True, or_), + ('2 & 2', False, and_), + ('2 & 2 & 2', False, and_), + ('2 & ""', True, and_), + ('2 ^ 2', False, xor), + ('2 ^ 2 ^ 2', False, xor), + ('2 ^ ""', True, xor), + ('(1, )[0]', False, subscr), + ('(1, )[-1]', False, subscr), + ('(1 + 2, )[0]', False, subscr), + ('(1, (1, 2))[1][1]', False, subscr), + ('(1, 2)[2-1]', False, subscr), + ('(1, (1, 2))[1][2-1]', False, subscr), + ('(1, (1, 2))[1:6][0][2-1]', False, subscr), + ('"a"[0]', False, subscr), + ('("a" + "b")[1]', False, subscr), + ('("a" + "b", )[0][1]', False, subscr), + ('("a" * 10)[9]', False, subscr), + ('(1, )[1]', True, subscr), + ('(1, )[-2]', True, subscr), + ('"a"[1]', True, subscr), + ('"a"[-2]', True, subscr), + ('("a" + "b")[2]', True, subscr), + ('("a" + "b", )[0][2]', True, subscr), + ('("a" + "b", )[1][0]', True, subscr), + ('("a" * 10)[10]', True, subscr), + ('(1, (1, 2))[2:6][0][2-1]', True, subscr), + ] - subscr_argval = get_binop_argval('NB_SUBSCR') - for expr, has_error in tests: + for expr, has_error, nb_op in tests: with self.subTest(expr=expr, has_error=has_error): code = compile(expr, '', 'single') if not has_error: - self.assertNotInBytecode(code, 'BINARY_OP', argval=subscr_argval) + self.assertNotInBytecode(code, 'BINARY_OP', argval=nb_op) else: - self.assertInBytecode(code, 'BINARY_OP', argval=subscr_argval) + self.assertInBytecode(code, 'BINARY_OP', argval=nb_op) self.check_lnotab(code) def test_constant_folding_remove_nop_location(self): @@ -1165,9 +1216,24 @@ def f(): self.assertEqual(f(), frozenset(range(40))) def test_multiple_foldings(self): + # (1, (2 + 2 * 2 // 2 - 2, )[0], ) ==> (1, 2) before = [ ('LOAD_SMALL_INT', 1, 0), + ('NOP', None, 0), + ('LOAD_SMALL_INT', 2, 0), + ('NOP', None, 0), + ('NOP', None, 0), + ('LOAD_SMALL_INT', 2, 0), + ('BINARY_OP', get_binop_argval('NB_MULTIPLY')), + ('LOAD_SMALL_INT', 2, 0), + ('NOP', None, 0), + ('BINARY_OP', get_binop_argval('NB_FLOOR_DIVIDE')), + ('NOP', None, 0), + ('LOAD_SMALL_INT', 2, 0), + ('BINARY_OP', get_binop_argval('NB_ADD')), + ('NOP', None, 0), ('LOAD_SMALL_INT', 2, 0), + ('BINARY_OP', get_binop_argval('NB_SUBTRACT')), ('BUILD_TUPLE', 1, 0), ('LOAD_SMALL_INT', 0, 0), ('BINARY_OP', get_binop_argval('NB_SUBSCR'), 0), From d3df8c81468a537a5091e5041ca2292bf3d93dd6 Mon Sep 17 00:00:00 2001 From: Yan Yanchii Date: Sat, 15 Feb 2025 09:24:58 +0100 Subject: [PATCH 05/34] polish ast code --- Python/ast_opt.c | 52 +++++++++++++++++++++++++----------------------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/Python/ast_opt.c b/Python/ast_opt.c index 263c70fc94c946..5e5518d31e9e1e 100644 --- a/Python/ast_opt.c +++ b/Python/ast_opt.c @@ -824,10 +824,10 @@ astfold_withitem(withitem_ty node_, PyArena *ctx_, _PyASTOptimizeState *state) #define IS_UNARY_SUB_EXPR(N) \ (IS_UNARY_EXPR(N) && UNARY_EXPR_OP(N) == USub) -#define IS_NUMERIC_UNARY_CONST_EXPR(N) \ +#define IS_MATCH_NUMERIC_UNARY_CONST_EXPR(N) \ (IS_UNARY_SUB_EXPR(N) && IS_NUMERIC_CONST_EXPR(UNARY_EXPR_OPERAND(N))) -#define IS_COMPLEX_UNARY_CONST_EXPR(N) \ +#define IS_MATCH_COMPLEX_UNARY_CONST_EXPR(N) \ (IS_UNARY_SUB_EXPR(N) && IS_COMPLEX_CONST_EXPR(UNARY_EXPR_OPERAND(N))) #define BINARY_EXPR(N) \ @@ -852,54 +852,56 @@ astfold_withitem(withitem_ty node_, PyArena *ctx_, _PyASTOptimizeState *state) (IS_BINARY_EXPR(N) && BINARY_EXPR_OP(N) == Sub) #define IS_MATCH_NUMERIC_OR_COMPLEX_UNARY_CONST_EXPR(N) \ - (IS_NUMERIC_UNARY_CONST_EXPR(N) || IS_COMPLEX_UNARY_CONST_EXPR(N)) + (IS_MATCH_NUMERIC_UNARY_CONST_EXPR(N) || IS_MATCH_COMPLEX_UNARY_CONST_EXPR(N)) #define IS_MATCH_COMPLEX_BINARY_CONST_EXPR(N) \ ( \ (IS_BINARY_ADD_EXPR(N) || IS_BINARY_SUB_EXPR(N)) \ - && (IS_NUMERIC_UNARY_CONST_EXPR(BINARY_EXPR_LEFT(N)) || IS_CONST_EXPR(BINARY_EXPR_LEFT(N))) \ + && (IS_MATCH_NUMERIC_UNARY_CONST_EXPR(BINARY_EXPR_LEFT(N)) || IS_CONST_EXPR(BINARY_EXPR_LEFT(N))) \ && IS_COMPLEX_CONST_EXPR(BINARY_EXPR_RIGHT(N)) \ ) static int -fold_const_unary_or_complex_expr(expr_ty e, PyArena *arena) +fold_const_unary_or_complex_expr(expr_ty node, PyArena *ctx_, + _PyASTOptimizeState * Py_UNUSED(state)) { - assert(IS_MATCH_NUMERIC_OR_COMPLEX_UNARY_CONST_EXPR(e)); - PyObject *constant = UNARY_EXPR_OPERAND_CONST_VALUE(e); - assert(UNARY_EXPR_OP(e) == USub); + assert(IS_MATCH_NUMERIC_OR_COMPLEX_UNARY_CONST_EXPR(node)); + PyObject *constant = UNARY_EXPR_OPERAND_CONST_VALUE(node); + assert(UNARY_EXPR_OP(node) == USub); PyObject* folded = PyNumber_Negative(constant); - return make_const(e, folded, arena); + return make_const(node, folded, ctx_); } static int -fold_const_binary_complex_expr(expr_ty e, PyArena *arena) +fold_const_binary_complex_expr(expr_ty node, PyArena *ctx_, _PyASTOptimizeState *state) { - assert(IS_MATCH_COMPLEX_BINARY_CONST_EXPR(e)); - expr_ty left_expr = BINARY_EXPR_LEFT(e); - if (IS_NUMERIC_UNARY_CONST_EXPR(left_expr)) { - if (!fold_const_unary_or_complex_expr(left_expr, arena)) { - return 0; - } - } - assert(IS_CONST_EXPR(BINARY_EXPR_LEFT(e))); - operator_ty op = BINARY_EXPR_OP(e); - PyObject *left = CONST_EXPR_VALUE(BINARY_EXPR_LEFT(e)); - PyObject *right = CONST_EXPR_VALUE(BINARY_EXPR_RIGHT(e)); + assert(IS_MATCH_COMPLEX_BINARY_CONST_EXPR(node)); + expr_ty left_expr = BINARY_EXPR_LEFT(node); + if (IS_MATCH_NUMERIC_UNARY_CONST_EXPR(left_expr)) { + CALL(fold_const_unary_or_complex_expr, expr_ty, left_expr); + } + /* must have folded if left was IS_MATCH_NUMERIC_UNARY_CONST_EXPR */ + assert(IS_CONST_EXPR(BINARY_EXPR_LEFT(node))); + operator_ty op = BINARY_EXPR_OP(node); + PyObject *left = CONST_EXPR_VALUE(BINARY_EXPR_LEFT(node)); + PyObject *right = CONST_EXPR_VALUE(BINARY_EXPR_RIGHT(node)); assert(op == Add || op == Sub); PyObject *folded = op == Add ? PyNumber_Add(left, right) : PyNumber_Subtract(left, right); - return make_const(e, folded, arena); + return make_const(node, folded, ctx_); } static int -fold_pattern_match_value(expr_ty node, PyArena *arena, _PyASTOptimizeState *Py_UNUSED(state)) +fold_pattern_match_value(expr_ty node, PyArena *ctx_, _PyASTOptimizeState *state) { switch (node->kind) { case UnaryOp_kind: - return fold_const_unary_or_complex_expr(node, arena); + CALL(fold_const_unary_or_complex_expr, expr_ty, node); + break; case BinOp_kind: - return fold_const_binary_complex_expr(node, arena); + CALL(fold_const_binary_complex_expr, expr_ty, node); + break; default: break; } From e82841ad94ab8beda581841a133a84ece560808a Mon Sep 17 00:00:00 2001 From: Yan Yanchii Date: Sat, 15 Feb 2025 09:33:01 +0100 Subject: [PATCH 06/34] add assertions --- Python/ast_opt.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Python/ast_opt.c b/Python/ast_opt.c index 5e5518d31e9e1e..13b2f31f06dcc7 100644 --- a/Python/ast_opt.c +++ b/Python/ast_opt.c @@ -869,6 +869,7 @@ fold_const_unary_or_complex_expr(expr_ty node, PyArena *ctx_, assert(IS_MATCH_NUMERIC_OR_COMPLEX_UNARY_CONST_EXPR(node)); PyObject *constant = UNARY_EXPR_OPERAND_CONST_VALUE(node); assert(UNARY_EXPR_OP(node) == USub); + assert(constant != NULL); PyObject* folded = PyNumber_Negative(constant); return make_const(node, folded, ctx_); } @@ -887,6 +888,7 @@ fold_const_binary_complex_expr(expr_ty node, PyArena *ctx_, _PyASTOptimizeState PyObject *left = CONST_EXPR_VALUE(BINARY_EXPR_LEFT(node)); PyObject *right = CONST_EXPR_VALUE(BINARY_EXPR_RIGHT(node)); assert(op == Add || op == Sub); + assert(left != NULL && right != NULL); PyObject *folded = op == Add ? PyNumber_Add(left, right) : PyNumber_Subtract(left, right); return make_const(node, folded, ctx_); } From cd6fbd7babb90f44837eb9e14673342d4248febb Mon Sep 17 00:00:00 2001 From: Yan Yanchii Date: Sat, 15 Feb 2025 10:54:37 +0100 Subject: [PATCH 07/34] use IS_NUMERIC_CONST_EXPR --- Python/ast_opt.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Python/ast_opt.c b/Python/ast_opt.c index 13b2f31f06dcc7..cedf4fb95c4d3e 100644 --- a/Python/ast_opt.c +++ b/Python/ast_opt.c @@ -857,7 +857,7 @@ astfold_withitem(withitem_ty node_, PyArena *ctx_, _PyASTOptimizeState *state) #define IS_MATCH_COMPLEX_BINARY_CONST_EXPR(N) \ ( \ (IS_BINARY_ADD_EXPR(N) || IS_BINARY_SUB_EXPR(N)) \ - && (IS_MATCH_NUMERIC_UNARY_CONST_EXPR(BINARY_EXPR_LEFT(N)) || IS_CONST_EXPR(BINARY_EXPR_LEFT(N))) \ + && (IS_MATCH_NUMERIC_UNARY_CONST_EXPR(BINARY_EXPR_LEFT(N)) || IS_NUMERIC_CONST_EXPR(BINARY_EXPR_LEFT(N))) \ && IS_COMPLEX_CONST_EXPR(BINARY_EXPR_RIGHT(N)) \ ) From 300976e704af9ea5f7a9316eaa72ef0a377e4561 Mon Sep 17 00:00:00 2001 From: Yan Yanchii Date: Sat, 15 Feb 2025 20:05:18 +0100 Subject: [PATCH 08/34] bring back tests --- Lib/test/test_peepholer.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Lib/test/test_peepholer.py b/Lib/test/test_peepholer.py index 31732f89d0ba71..8040108dee9b8a 100644 --- a/Lib/test/test_peepholer.py +++ b/Lib/test/test_peepholer.py @@ -311,6 +311,7 @@ def test_folding_of_unaryops_on_constants(self): for line, elem in ( ('-0.5', -0.5), # unary negative ('-0.0', -0.0), # -0.0 + ('-(1.0-1.0)', -0.0), # -0.0 after folding ('-0', 0), # -0 ('~-2', 1), # unary invert ('+1', 1), # unary positive @@ -325,6 +326,14 @@ def test_folding_of_unaryops_on_constants(self): self.assertFalse(instr.opname.startswith('UNARY_')) self.check_lnotab(code) + # Check that -0.0 works after marshaling + def negzero(): + return -(1.0-1.0) + + for instr in dis.get_instructions(negzero): + self.assertFalse(instr.opname.startswith('UNARY_')) + self.check_lnotab(negzero) + # Verify that unfoldables are skipped for line, elem, opname in ( ('-"abc"', 'abc', 'UNARY_NEGATIVE'), From 3240d8e584270f62c295f458488e9ef1d0b41602 Mon Sep 17 00:00:00 2001 From: Yan Yanchii Date: Sat, 15 Feb 2025 21:07:32 +0100 Subject: [PATCH 09/34] move unaryop to cfg --- Lib/test/test_ast/test_ast.py | 57 -------- Python/ast_opt.c | 71 ---------- Python/flowgraph.c | 244 ++++++++++++++++++++++++++-------- 3 files changed, 186 insertions(+), 186 deletions(-) diff --git a/Lib/test/test_ast/test_ast.py b/Lib/test/test_ast/test_ast.py index cfdb336d9325f2..a145ef7fa63cbc 100644 --- a/Lib/test/test_ast/test_ast.py +++ b/Lib/test/test_ast/test_ast.py @@ -3081,12 +3081,6 @@ def test_cli_file_input(self): class ASTOptimiziationTests(unittest.TestCase): - unaryop = { - "~": ast.Invert(), - "+": ast.UAdd(), - "-": ast.USub(), - } - def wrap_expr(self, expr): return ast.Module(body=[ast.Expr(value=expr)]) @@ -3118,57 +3112,6 @@ def assert_ast(self, code, non_optimized_target, optimized_target): f"{ast.dump(optimized_tree)}", ) - def test_folding_unaryop(self): - code = "%s1" - operators = self.unaryop.keys() - - def create_unaryop(operand): - return ast.UnaryOp(op=self.unaryop[operand], operand=ast.Constant(1)) - - for op in operators: - result_code = code % op - non_optimized_target = self.wrap_expr(create_unaryop(op)) - optimized_target = self.wrap_expr(ast.Constant(eval(result_code))) - - with self.subTest( - result_code=result_code, - non_optimized_target=non_optimized_target, - optimized_target=optimized_target - ): - self.assert_ast(result_code, non_optimized_target, optimized_target) - - def test_folding_not(self): - code = "not (1 %s (1,))" - operators = { - "in": ast.In(), - "is": ast.Is(), - } - opt_operators = { - "is": ast.IsNot(), - "in": ast.NotIn(), - } - - def create_notop(operand): - return ast.UnaryOp(op=ast.Not(), operand=ast.Compare( - left=ast.Constant(value=1), - ops=[operators[operand]], - comparators=[ast.Tuple(elts=[ast.Constant(value=1)])] - )) - - for op in operators.keys(): - result_code = code % op - non_optimized_target = self.wrap_expr(create_notop(op)) - optimized_target = self.wrap_expr( - ast.Compare(left=ast.Constant(1), ops=[opt_operators[op]], comparators=[ast.Constant(value=(1,))]) - ) - - with self.subTest( - result_code=result_code, - non_optimized_target=non_optimized_target, - optimized_target=optimized_target - ): - self.assert_ast(result_code, non_optimized_target, optimized_target) - def test_folding_format(self): code = "'%s' % (a,)" diff --git a/Python/ast_opt.c b/Python/ast_opt.c index cedf4fb95c4d3e..8554c9d0571030 100644 --- a/Python/ast_opt.c +++ b/Python/ast_opt.c @@ -66,76 +66,6 @@ has_starred(asdl_expr_seq *elts) return 0; } - -static PyObject* -unary_not(PyObject *v) -{ - int r = PyObject_IsTrue(v); - if (r < 0) - return NULL; - return PyBool_FromLong(!r); -} - -static int -fold_unaryop(expr_ty node, PyArena *arena, _PyASTOptimizeState *state) -{ - expr_ty arg = node->v.UnaryOp.operand; - - if (arg->kind != Constant_kind) { - /* Fold not into comparison */ - if (node->v.UnaryOp.op == Not && arg->kind == Compare_kind && - asdl_seq_LEN(arg->v.Compare.ops) == 1) { - /* Eq and NotEq are often implemented in terms of one another, so - folding not (self == other) into self != other breaks implementation - of !=. Detecting such cases doesn't seem worthwhile. - Python uses for 'is subset'/'is superset' operations on sets. - They don't satisfy not folding laws. */ - cmpop_ty op = asdl_seq_GET(arg->v.Compare.ops, 0); - switch (op) { - case Is: - op = IsNot; - break; - case IsNot: - op = Is; - break; - case In: - op = NotIn; - break; - case NotIn: - op = In; - break; - // The remaining comparison operators can't be safely inverted - case Eq: - case NotEq: - case Lt: - case LtE: - case Gt: - case GtE: - op = 0; // The AST enums leave "0" free as an "unused" marker - break; - // No default case, so the compiler will emit a warning if new - // comparison operators are added without being handled here - } - if (op) { - asdl_seq_SET(arg->v.Compare.ops, 0, op); - COPY_NODE(node, arg); - return 1; - } - } - return 1; - } - - typedef PyObject *(*unary_op)(PyObject*); - static const unary_op ops[] = { - [Invert] = PyNumber_Invert, - [Not] = unary_not, - [UAdd] = PyNumber_Positive, - [USub] = PyNumber_Negative, - }; - PyObject *newval = ops[node->v.UnaryOp.op](arg->v.Constant.value); - return make_const(node, newval, arena); -} - static expr_ty parse_literal(PyObject *fmt, Py_ssize_t *ppos, PyArena *arena) { @@ -506,7 +436,6 @@ astfold_expr(expr_ty node_, PyArena *ctx_, _PyASTOptimizeState *state) break; case UnaryOp_kind: CALL(astfold_expr, expr_ty, node_->v.UnaryOp.operand); - CALL(fold_unaryop, expr_ty, node_); break; case Lambda_kind: CALL(astfold_arguments, arguments_ty, node_->v.Lambda.args); diff --git a/Python/flowgraph.c b/Python/flowgraph.c index 4a96e100b35713..d546bdbac8ef80 100644 --- a/Python/flowgraph.c +++ b/Python/flowgraph.c @@ -23,6 +23,11 @@ return ERROR; \ } +#define RETURN_IF_NOT_CONST_SEQ(X) \ + if ((X) == NULL) { \ + return SUCCESS; \ + } + #define DEFAULT_BLOCK_SIZE 16 typedef _Py_SourceLocation location; @@ -1406,6 +1411,47 @@ nop_out(basicblock *bb, int start, int count) } } +/* Determine opcode & oparg for freshly folded constant. + Steals reference to "newconst". +*/ +static int +newop_from_folded(PyObject *newconst, PyObject *consts, + PyObject *const_cache, int *newopcode, int *newoparg) +{ + if (PyLong_CheckExact(newconst)) { + int overflow; + long val = PyLong_AsLongAndOverflow(newconst, &overflow); + if (!overflow && _PY_IS_SMALL_INT(val)) { + *newopcode = LOAD_SMALL_INT; + *newoparg = val; + return SUCCESS; + } + } + *newopcode = LOAD_CONST; + *newoparg = add_const(newconst, consts, const_cache); + RETURN_IF_ERROR(*newoparg); + return SUCCESS; +} + +/* Steals reference to "newconst" */ +static int +make_const(PyObject *newconst, basicblock *bb, + int i, int nop, PyObject *consts, PyObject *const_cache) +{ + if (newconst == NULL) { + if (PyErr_ExceptionMatches(PyExc_KeyboardInterrupt)) { + return ERROR; + } + PyErr_Clear(); + return SUCCESS; + } + int newopcode, newoparg; + RETURN_IF_ERROR(newop_from_folded(newconst, consts, const_cache, &newopcode, &newoparg)); + nop_out(bb, i-1, nop); + INSTR_SET_OP1(&bb->b_instr[i], newopcode, newoparg); + return SUCCESS; +} + /* Replace LOAD_CONST c1, LOAD_CONST c2 ... LOAD_CONST cn, BUILD_TUPLE n with LOAD_CONST (c1, c2, ... cn). The consts table must still be in list form so that the @@ -1413,26 +1459,19 @@ nop_out(basicblock *bb, int start, int count) Called with codestr pointing to the first LOAD_CONST. */ static int -fold_tuple_of_constants(basicblock *bb, int n, PyObject *consts, PyObject *const_cache) +fold_tuple_of_constants(basicblock *bb, int i, PyObject *consts, PyObject *const_cache) { /* Pre-conditions */ assert(PyDict_CheckExact(const_cache)); assert(PyList_CheckExact(consts)); - cfg_instr *instr = &bb->b_instr[n]; + cfg_instr *instr = &bb->b_instr[i]; assert(instr->i_opcode == BUILD_TUPLE); int seq_size = instr->i_oparg; PyObject *newconst; - RETURN_IF_ERROR(get_constant_sequence(bb, n-1, seq_size, consts, &newconst)); - if (newconst == NULL) { - /* not a const sequence */ - return SUCCESS; - } - assert(PyTuple_CheckExact(newconst) && PyTuple_GET_SIZE(newconst) == seq_size); - int index = add_const(newconst, consts, const_cache); - RETURN_IF_ERROR(index); - nop_out(bb, n-1, seq_size); - INSTR_SET_OP1(instr, LOAD_CONST, index); - return SUCCESS; + RETURN_IF_ERROR(get_constant_sequence(bb, i-1, seq_size, consts, &newconst)); + RETURN_IF_NOT_CONST_SEQ(newconst); + assert(PyTuple_Size(newconst) == seq_size); + return make_const(newconst, bb, i, seq_size, consts, const_cache); } #define MIN_CONST_SEQUENCE_SIZE 3 @@ -1469,7 +1508,7 @@ optimize_lists_and_sets(basicblock *bb, int i, int nextop, } return SUCCESS; } - assert(PyTuple_CheckExact(newconst) && PyTuple_GET_SIZE(newconst) == seq_size); + assert(PyTuple_Size(newconst) == seq_size); if (instr->i_opcode == BUILD_SET) { PyObject *frozenset = PyFrozenSet_New(newconst); if (frozenset == NULL) { @@ -1497,26 +1536,6 @@ optimize_lists_and_sets(basicblock *bb, int i, int nextop, return SUCCESS; } -/* Determine opcode & oparg for freshly folded constant. */ -static int -newop_from_folded(PyObject *newconst, PyObject *consts, - PyObject *const_cache, int *newopcode, int *newoparg) -{ - if (PyLong_CheckExact(newconst)) { - int overflow; - long val = PyLong_AsLongAndOverflow(newconst, &overflow); - if (!overflow && _PY_IS_SMALL_INT(val)) { - *newopcode = LOAD_SMALL_INT; - *newoparg = val; - return SUCCESS; - } - } - *newopcode = LOAD_CONST; - *newoparg = add_const(newconst, consts, const_cache); - RETURN_IF_ERROR(*newoparg); - return SUCCESS; -} - /* Check whether a collection doesn't containing too much items (including subcollections). This protects from creating a constant that needs too much time for calculating a hash. @@ -1701,26 +1720,139 @@ optimize_if_const_binop(basicblock *bb, int i, PyObject *consts, PyObject *const assert(binop->i_opcode == BINARY_OP); PyObject *pair; RETURN_IF_ERROR(get_constant_sequence(bb, i-1, 2, consts, &pair)); - if (pair == NULL) { - return SUCCESS; - } - assert(PyTuple_CheckExact(pair) && PyTuple_Size(pair) == 2); + RETURN_IF_NOT_CONST_SEQ(pair); + assert(PyTuple_Size(pair) == 2); PyObject *left = PyTuple_GET_ITEM(pair, 0); PyObject *right = PyTuple_GET_ITEM(pair, 1); PyObject *newconst = eval_const_binop(left, binop->i_oparg, right); Py_DECREF(pair); - if (newconst == NULL) { - if (PyErr_ExceptionMatches(PyExc_KeyboardInterrupt)) { - return ERROR; + return make_const(newconst, bb, i, 2, consts, const_cache); +} + +static PyObject* +unary_not(PyObject *v) +{ + int r = PyObject_IsTrue(v); + if (r < 0) + return NULL; + return PyBool_FromLong(!r); +} + +static PyObject * +eval_const_unaryop(PyObject *operand, int op, bool unarypos) +{ + if (unarypos) { + return PyNumber_Positive(operand); + } + PyObject *result; + switch (op) { + case UNARY_NEGATIVE: + result = PyNumber_Negative(operand); + break; + case UNARY_INVERT: + result = PyNumber_Invert(operand); + break; + case UNARY_NOT: + result = unary_not(operand); + break; + default: + Py_UNREACHABLE(); + } + return result; +} + +static void +remove_redundant_to_bool(basicblock *bb, int start) +{ + assert(start >= 0); + for (;start < bb->b_iused; start++) { + cfg_instr *curr = &bb->b_instr[start]; + if (curr->i_opcode == TO_BOOL) { + INSTR_SET_OP0(curr, NOP); + continue; } - PyErr_Clear(); - return SUCCESS; + break; } - int newopcode, newoparg; - RETURN_IF_ERROR(newop_from_folded(newconst, consts, const_cache, &newopcode, &newoparg)); - nop_out(bb, i-1, 2); - INSTR_SET_OP1(binop, newopcode, newoparg); - return SUCCESS; +} + +cfg_instr * +find_unary_not_target(basicblock *bb, int start) +{ + assert(start < bb->b_iused); + for (; start >= 0; start--) { + cfg_instr *instr = &bb->b_instr[start]; + if (instr->i_opcode == NOP) { + continue; + } + if (instr->i_opcode == IS_OP || instr->i_opcode == CONTAINS_OP) { + return instr; + } + break; + } + return NULL; +} + +/* Replace: + IS_OP(is) + UNARY_NOT + with: + IS_OP(is not) + NOP + or vice versa ("is not" to "is"). + + And: + CONTAINS_OP(in) + UNARY_NOT + with: + CONTAINS_OP(not in) + NOP + or vice versa ("not in" to "in"). +*/ +static void +optimize_unary_not(basicblock *bb, int i, int nextop) +{ + cfg_instr *instr = &bb->b_instr[i]; + assert(instr->i_opcode == UNARY_NOT); + remove_redundant_to_bool(bb, i+1); + cfg_instr *target = find_unary_not_target(bb, i-1); + if (target == NULL) { + return; + } + int inverted = target->i_oparg ^ 1; + assert(inverted == 0 || inverted == 1); + INSTR_SET_OP1(target, target->i_opcode, inverted); + INSTR_SET_OP0(instr, NOP); +} + +static int +optimize_if_const_unaryop(basicblock *bb, int i, int nextop, + PyObject *consts, PyObject *const_cache, bool unarypos) +{ + cfg_instr *instr = &bb->b_instr[i]; + if (unarypos) { + assert( + instr->i_opcode == CALL_INTRINSIC_1 + && instr->i_oparg == INTRINSIC_UNARY_POSITIVE + ); + } + else { + assert( + instr->i_opcode == UNARY_NEGATIVE + || instr->i_opcode == UNARY_INVERT + || instr->i_opcode == UNARY_NOT + ); + } + if (instr->i_opcode == UNARY_NOT) { + optimize_unary_not(bb, i, nextop); + } + PyObject *seq; + RETURN_IF_ERROR(get_constant_sequence(bb, i-1, 1, consts, &seq)); + RETURN_IF_NOT_CONST_SEQ(seq); + assert(PyTuple_Size(seq) == 1); + PyObject *operand = PyTuple_GET_ITEM(seq, 0); + PyObject *newconst = eval_const_unaryop(operand, instr->i_opcode, unarypos); + Py_DECREF(seq); + return make_const(newconst, bb, i, 1, consts, const_cache); } #define VISITED (-1) @@ -2203,22 +2335,18 @@ optimize_basic_block(PyObject *const_cache, basicblock *bb, PyObject *consts) } break; case UNARY_NOT: - if (nextop == TO_BOOL) { - INSTR_SET_OP0(inst, NOP); - INSTR_SET_OP0(&bb->b_instr[i + 1], UNARY_NOT); - continue; - } - if (nextop == UNARY_NOT) { - INSTR_SET_OP0(inst, NOP); - INSTR_SET_OP0(&bb->b_instr[i + 1], NOP); - continue; - } + case UNARY_INVERT: + case UNARY_NEGATIVE: + RETURN_IF_ERROR(optimize_if_const_unaryop(bb, i, nextop, consts, const_cache, false)); break; case CALL_INTRINSIC_1: // for _ in (*foo, *bar) -> for _ in [*foo, *bar] if (oparg == INTRINSIC_LIST_TO_TUPLE && nextop == GET_ITER) { INSTR_SET_OP0(inst, NOP); } + else if (oparg == INTRINSIC_UNARY_POSITIVE) { + RETURN_IF_ERROR(optimize_if_const_unaryop(bb, i, nextop, consts, const_cache, true)); + } break; case BINARY_OP: RETURN_IF_ERROR(optimize_if_const_binop(bb, i, consts, const_cache)); From 74d22758e1e66bd41f3bae8f1594be39c0391901 Mon Sep 17 00:00:00 2001 From: Yan Yanchii Date: Sat, 15 Feb 2025 22:14:42 +0100 Subject: [PATCH 10/34] add tests --- Lib/test/test_peepholer.py | 127 +++++++++++++++++++++++++++++++++++-- 1 file changed, 123 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_peepholer.py b/Lib/test/test_peepholer.py index 8040108dee9b8a..44c1554d1221f2 100644 --- a/Lib/test/test_peepholer.py +++ b/Lib/test/test_peepholer.py @@ -483,7 +483,6 @@ def test_constant_folding(self): def test_constant_folding_small_int(self): tests = [ - # subscript ('(0, )[0]', 0), ('(1 + 2, )[0]', 3), ('(2 + 2 * 2, )[0]', 6), @@ -494,7 +493,18 @@ def test_constant_folding_small_int(self): ('(256, )[0]', None), ('(1000, )[0]', None), ('(1 - 2, )[0]', None), + ('255 + 0', 255), ('255 + 1', None), + ('-1', None), + ('--1', 1), + ('--255', 255), + ('--256', None), + ('~1', None), + ('~~1', 1), + ('~~255', 255), + ('~~256', None), + ('++255', 255), + ('++256', None), ] for expr, oparg in tests: with self.subTest(expr=expr, oparg=oparg): @@ -505,6 +515,9 @@ def test_constant_folding_small_int(self): self.assertNotInBytecode(code, 'LOAD_SMALL_INT') self.check_lnotab(code) + def test_folding_unaryop(self): + pass + def test_folding_binop(self): add = get_binop_argval('NB_ADD') sub = get_binop_argval('NB_SUBTRACT') @@ -1225,14 +1238,21 @@ def f(): self.assertEqual(f(), frozenset(range(40))) def test_multiple_foldings(self): - # (1, (2 + 2 * 2 // 2 - 2, )[0], ) ==> (1, 2) + # (1, (--2 + ++2 * 2 // 2 - 2, )[0], ~~3, not not True) ==> (1, 2, 3, True) + intrinsic_positive = 5 before = [ ('LOAD_SMALL_INT', 1, 0), ('NOP', None, 0), ('LOAD_SMALL_INT', 2, 0), + ('UNARY_NEGATIVE', None, 0), + ('NOP', None, 0), + ('UNARY_NEGATIVE', None, 0), ('NOP', None, 0), ('NOP', None, 0), ('LOAD_SMALL_INT', 2, 0), + ('CALL_INTRINSIC_1', intrinsic_positive, 0), + ('NOP', None, 0), + ('CALL_INTRINSIC_1', intrinsic_positive, 0), ('BINARY_OP', get_binop_argval('NB_MULTIPLY')), ('LOAD_SMALL_INT', 2, 0), ('NOP', None, 0), @@ -1242,18 +1262,34 @@ def test_multiple_foldings(self): ('BINARY_OP', get_binop_argval('NB_ADD')), ('NOP', None, 0), ('LOAD_SMALL_INT', 2, 0), + ('NOP', None, 0), ('BINARY_OP', get_binop_argval('NB_SUBTRACT')), + ('NOP', None, 0), ('BUILD_TUPLE', 1, 0), ('LOAD_SMALL_INT', 0, 0), ('BINARY_OP', get_binop_argval('NB_SUBSCR'), 0), - ('BUILD_TUPLE', 2, 0), + ('NOP', None, 0), + ('LOAD_SMALL_INT', 3, 0), + ('NOP', None, 0), + ('UNARY_INVERT', None, 0), + ('NOP', None, 0), + ('UNARY_INVERT', None, 0), + ('NOP', None, 0), + ('LOAD_SMALL_INT', 3, 0), + ('NOP', None, 0), + ('UNARY_NOT', None, 0), + ('NOP', None, 0), + ('UNARY_NOT', None, 0), + ('NOP', None, 0), + ('BUILD_TUPLE', 4, 0), + ('NOP', None, 0), ('RETURN_VALUE', None, 0) ] after = [ ('LOAD_CONST', 1, 0), ('RETURN_VALUE', None, 0) ] - self.cfg_optimization_test(before, after, consts=[], expected_consts=[(2,), (1, 2)]) + self.cfg_optimization_test(before, after, consts=[], expected_consts=[-2, (1, 2, 3, True)]) def test_build_empty_tuple(self): before = [ @@ -1601,6 +1637,89 @@ def test_optimize_literal_set_contains(self): ] self.cfg_optimization_test(same, same, consts=[None], expected_consts=[None]) + def test_optimize_unary_not_to_bool(self): + before = [ + ('LOAD_NAME', 0, 0), + ('UNARY_NOT', None, 0), + ('TO_BOOL', None, 0), + ('TO_BOOL', None, 0), + ('TO_BOOL', None, 0), + ('LOAD_CONST', 0, 0), + ('RETURN_VALUE', None, 0), + ] + after = [ + ('LOAD_NAME', 0, 0), + ('UNARY_NOT', None, 0), + ('LOAD_CONST', 0, 0), + ('RETURN_VALUE', None, 0), + ] + self.cfg_optimization_test(before, after, consts=[None], expected_consts=[None]) + + before = [ + ('LOAD_SMALL_INT', 1, 0), + ('UNARY_NOT', None, 0), + ('TO_BOOL', None, 0), + ('TO_BOOL', None, 0), + ('TO_BOOL', None, 0), + ('TO_BOOL', None, 0), + ('RETURN_VALUE', None, 0), + ] + after = [ + ('LOAD_CONST', 0, 0), + ('RETURN_VALUE', None, 0), + ] + self.cfg_optimization_test(before, after, consts=[], expected_consts=[False]) + + is_ = 0 + isnot = 1 + before = [ + ('LOAD_NAME', 0, 0), + ('LOAD_NAME', 1, 0), + ('IS_OP', is_, 0), + ('UNARY_NOT', None, 0), + ('TO_BOOL', None, 0), + ('TO_BOOL', None, 0), + ('TO_BOOL', None, 0), + ('LOAD_CONST', 0, 0), + ('RETURN_VALUE', None, 0), + ] + after = [ + ('LOAD_NAME', 0, 0), + ('LOAD_NAME', 1, 0), + ('IS_OP', isnot, 0), + ('LOAD_CONST', 0, 0), + ('RETURN_VALUE', None, 0), + ] + self.cfg_optimization_test(before, after, consts=[None], expected_consts=[None]) + + in_ = 0 + notin = 1 + before = [ + ('LOAD_NAME', 0, 0), + ('LOAD_NAME', 1, 0), + ('CONTAINS_OP', in_, 0), + ('UNARY_NOT', None, 0), + ('TO_BOOL', None, 0), + ('TO_BOOL', None, 0), + ('TO_BOOL', None, 0), + ('LOAD_CONST', 0, 0), + ('RETURN_VALUE', None, 0), + ] + after = [ + ('LOAD_NAME', 0, 0), + ('LOAD_NAME', 1, 0), + ('CONTAINS_OP', notin, 0), + ('LOAD_CONST', 0, 0), + ('RETURN_VALUE', None, 0), + ] + self.cfg_optimization_test(before, after, consts=[None], expected_consts=[None]) + + def test_optimize_if_const_unaryop(self): + pass + + def test_optimize_if_const_binop(self): + pass + def test_conditional_jump_forward_const_condition(self): # The unreachable branch of the jump is removed, the jump # becomes redundant and is replaced by a NOP (for the lineno) From 8358e28a7469c52636b787c9b30d2ac77dd080eb Mon Sep 17 00:00:00 2001 From: Yan Yanchii Date: Sat, 15 Feb 2025 22:38:51 +0100 Subject: [PATCH 11/34] cancel out unary not --- Python/flowgraph.c | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/Python/flowgraph.c b/Python/flowgraph.c index d546bdbac8ef80..f804751e809542 100644 --- a/Python/flowgraph.c +++ b/Python/flowgraph.c @@ -1792,6 +1792,22 @@ find_unary_not_target(basicblock *bb, int start) return NULL; } +static bool +maybe_unary_not_cancel_out(basicblock *bb, int i, int nextop) +{ + cfg_instr *instr = &bb->b_instr[i]; + assert(instr->i_opcode == UNARY_NOT); + if (nextop == UNARY_NOT) { + INSTR_SET_OP0(instr, NOP); + assert(i < bb->b_iused); + cfg_instr *next = &bb->b_instr[i+1]; + assert(next->i_opcode == nextop); + INSTR_SET_OP0(next, NOP); + return true; + } + return false; +} + /* Replace: IS_OP(is) UNARY_NOT @@ -1808,20 +1824,21 @@ find_unary_not_target(basicblock *bb, int start) NOP or vice versa ("not in" to "in"). */ -static void -optimize_unary_not(basicblock *bb, int i, int nextop) +static bool +optimize_unary_not_is_contains(basicblock *bb, int i, int nextop) { cfg_instr *instr = &bb->b_instr[i]; assert(instr->i_opcode == UNARY_NOT); remove_redundant_to_bool(bb, i+1); cfg_instr *target = find_unary_not_target(bb, i-1); if (target == NULL) { - return; + return false; } int inverted = target->i_oparg ^ 1; assert(inverted == 0 || inverted == 1); INSTR_SET_OP1(target, target->i_opcode, inverted); INSTR_SET_OP0(instr, NOP); + return true; } static int @@ -1843,7 +1860,14 @@ optimize_if_const_unaryop(basicblock *bb, int i, int nextop, ); } if (instr->i_opcode == UNARY_NOT) { - optimize_unary_not(bb, i, nextop); + if (maybe_unary_not_cancel_out(bb, i, nextop)) { + /* nothing more to do here */ + return SUCCESS; + } + if (optimize_unary_not_is_contains(bb, i, nextop)) { + /* if optimized, no need to continue */ + return SUCCESS; + } } PyObject *seq; RETURN_IF_ERROR(get_constant_sequence(bb, i-1, 1, consts, &seq)); From c1a1be5bfa4aa73658cbbe1854f90d3044133993 Mon Sep 17 00:00:00 2001 From: Yan Yanchii Date: Sat, 15 Feb 2025 23:35:07 +0100 Subject: [PATCH 12/34] fix optimize_if_const_unaryop --- Python/flowgraph.c | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/Python/flowgraph.c b/Python/flowgraph.c index f804751e809542..2aa38c3edfafdd 100644 --- a/Python/flowgraph.c +++ b/Python/flowgraph.c @@ -1762,11 +1762,15 @@ eval_const_unaryop(PyObject *operand, int op, bool unarypos) } static void -remove_redundant_to_bool(basicblock *bb, int start) +remove_to_bool_sequence(basicblock *bb, int start) { assert(start >= 0); for (;start < bb->b_iused; start++) { cfg_instr *curr = &bb->b_instr[start]; + if (curr->i_opcode == NOP) { + /* should not happen, but just in case */ + continue; + } if (curr->i_opcode == TO_BOOL) { INSTR_SET_OP0(curr, NOP); continue; @@ -1799,8 +1803,9 @@ maybe_unary_not_cancel_out(basicblock *bb, int i, int nextop) assert(instr->i_opcode == UNARY_NOT); if (nextop == UNARY_NOT) { INSTR_SET_OP0(instr, NOP); - assert(i < bb->b_iused); - cfg_instr *next = &bb->b_instr[i+1]; + int nexi = i + 1; + assert(nexi >= 0 && nexi < bb->b_iused); + cfg_instr *next = &bb->b_instr[nexi]; assert(next->i_opcode == nextop); INSTR_SET_OP0(next, NOP); return true; @@ -1829,7 +1834,6 @@ optimize_unary_not_is_contains(basicblock *bb, int i, int nextop) { cfg_instr *instr = &bb->b_instr[i]; assert(instr->i_opcode == UNARY_NOT); - remove_redundant_to_bool(bb, i+1); cfg_instr *target = find_unary_not_target(bb, i-1); if (target == NULL) { return false; @@ -1864,6 +1868,7 @@ optimize_if_const_unaryop(basicblock *bb, int i, int nextop, /* nothing more to do here */ return SUCCESS; } + remove_to_bool_sequence(bb, i+1); if (optimize_unary_not_is_contains(bb, i, nextop)) { /* if optimized, no need to continue */ return SUCCESS; @@ -1875,6 +1880,10 @@ optimize_if_const_unaryop(basicblock *bb, int i, int nextop, assert(PyTuple_Size(seq) == 1); PyObject *operand = PyTuple_GET_ITEM(seq, 0); PyObject *newconst = eval_const_unaryop(operand, instr->i_opcode, unarypos); + if (instr->i_opcode == UNARY_NOT) { + /* we eliminated TO_BOOL's so we must return boolean */ + assert(PyBool_Check(newconst)); + } Py_DECREF(seq); return make_const(newconst, bb, i, 1, consts, const_cache); } From ef9221a0dc58d13a3da20e559d82aa1b118ce9e7 Mon Sep 17 00:00:00 2001 From: Yan Yanchii Date: Sat, 15 Feb 2025 23:35:31 +0100 Subject: [PATCH 13/34] add test_optimize_unary_not --- Lib/test/test_peepholer.py | 173 +++++++++++++++++++++++++++++++++---- 1 file changed, 158 insertions(+), 15 deletions(-) diff --git a/Lib/test/test_peepholer.py b/Lib/test/test_peepholer.py index 44c1554d1221f2..a13283dd13bc2a 100644 --- a/Lib/test/test_peepholer.py +++ b/Lib/test/test_peepholer.py @@ -1637,31 +1637,57 @@ def test_optimize_literal_set_contains(self): ] self.cfg_optimization_test(same, same, consts=[None], expected_consts=[None]) - def test_optimize_unary_not_to_bool(self): + def test_optimize_unary_not(self): + # test folding + before = [ + ('LOAD_SMALL_INT', 1, 0), + ('UNARY_NOT', None, 0), + ('RETURN_VALUE', None, 0), + ] + after = [ + ('LOAD_CONST', 0, 0), + ('RETURN_VALUE', None, 0), + ] + self.cfg_optimization_test(before, after, consts=[], expected_consts=[False]) + + # test cancel out + before = [ + ('LOAD_SMALL_INT', 1, 0), + ('UNARY_NOT', None, 0), + ('UNARY_NOT', None, 0), + ('UNARY_NOT', None, 0), + ('UNARY_NOT', None, 0), + ('RETURN_VALUE', None, 0), + ] + after = [ + ('LOAD_SMALL_INT', 1, 0), + ('RETURN_VALUE', None, 0), + ] + self.cfg_optimization_test(before, after, consts=[], expected_consts=[]) + + # test eliminate to bool before = [ ('LOAD_NAME', 0, 0), ('UNARY_NOT', None, 0), ('TO_BOOL', None, 0), ('TO_BOOL', None, 0), ('TO_BOOL', None, 0), - ('LOAD_CONST', 0, 0), ('RETURN_VALUE', None, 0), ] after = [ ('LOAD_NAME', 0, 0), ('UNARY_NOT', None, 0), - ('LOAD_CONST', 0, 0), ('RETURN_VALUE', None, 0), ] - self.cfg_optimization_test(before, after, consts=[None], expected_consts=[None]) + self.cfg_optimization_test(before, after, consts=[], expected_consts=[]) + # test folding & elimitnate to bool before = [ ('LOAD_SMALL_INT', 1, 0), ('UNARY_NOT', None, 0), ('TO_BOOL', None, 0), ('TO_BOOL', None, 0), ('TO_BOOL', None, 0), - ('TO_BOOL', None, 0), ('RETURN_VALUE', None, 0), ] after = [ @@ -1670,8 +1696,43 @@ def test_optimize_unary_not_to_bool(self): ] self.cfg_optimization_test(before, after, consts=[], expected_consts=[False]) - is_ = 0 - isnot = 1 + # test cancel out & eliminate to bool (to bool stays) + before = [ + ('LOAD_SMALL_INT', 1, 0), + ('UNARY_NOT', None, 0), + ('UNARY_NOT', None, 0), + ('UNARY_NOT', None, 0), + ('UNARY_NOT', None, 0), + ('TO_BOOL', None, 0), + ('RETURN_VALUE', None, 0), + ] + after = [ + ('LOAD_SMALL_INT', 1, 0), + ('TO_BOOL', None, 0), + ('RETURN_VALUE', None, 0), + ] + self.cfg_optimization_test(before, after, consts=[], expected_consts=[]) + + is_ = in_ = 0 + isnot = notin = 1 + + # test is/isnot + before = [ + ('LOAD_NAME', 0, 0), + ('LOAD_NAME', 1, 0), + ('IS_OP', is_, 0), + ('UNARY_NOT', None, 0), + ('RETURN_VALUE', None, 0), + ] + after = [ + ('LOAD_NAME', 0, 0), + ('LOAD_NAME', 1, 0), + ('IS_OP', isnot, 0), + ('RETURN_VALUE', None, 0), + ] + self.cfg_optimization_test(before, after, consts=[], expected_consts=[]) + + # test is/isnot eliminate to bool before = [ ('LOAD_NAME', 0, 0), ('LOAD_NAME', 1, 0), @@ -1680,20 +1741,85 @@ def test_optimize_unary_not_to_bool(self): ('TO_BOOL', None, 0), ('TO_BOOL', None, 0), ('TO_BOOL', None, 0), - ('LOAD_CONST', 0, 0), ('RETURN_VALUE', None, 0), ] after = [ ('LOAD_NAME', 0, 0), ('LOAD_NAME', 1, 0), ('IS_OP', isnot, 0), - ('LOAD_CONST', 0, 0), ('RETURN_VALUE', None, 0), ] - self.cfg_optimization_test(before, after, consts=[None], expected_consts=[None]) + self.cfg_optimization_test(before, after, consts=[], expected_consts=[]) - in_ = 0 - notin = 1 + # test in/notin + before = [ + ('LOAD_NAME', 0, 0), + ('LOAD_NAME', 1, 0), + ('CONTAINS_OP', in_, 0), + ('UNARY_NOT', None, 0), + ('RETURN_VALUE', None, 0), + ] + after = [ + ('LOAD_NAME', 0, 0), + ('LOAD_NAME', 1, 0), + ('CONTAINS_OP', notin, 0), + ('RETURN_VALUE', None, 0), + ] + self.cfg_optimization_test(before, after, consts=[], expected_consts=[]) + + # test in/notin cancel out + before = [ + ('LOAD_NAME', 0, 0), + ('LOAD_NAME', 1, 0), + ('CONTAINS_OP', in_, 0), + ('UNARY_NOT', None, 0), + ('UNARY_NOT', None, 0), + ('RETURN_VALUE', None, 0), + ] + after = [ + ('LOAD_NAME', 0, 0), + ('LOAD_NAME', 1, 0), + ('CONTAINS_OP', in_, 0), + ('RETURN_VALUE', None, 0), + ] + self.cfg_optimization_test(before, after, consts=[], expected_consts=[]) + + # test in/notin + before = [ + ('LOAD_NAME', 0, 0), + ('LOAD_NAME', 1, 0), + ('CONTAINS_OP', in_, 0), + ('UNARY_NOT', None, 0), + ('RETURN_VALUE', None, 0), + ] + after = [ + ('LOAD_NAME', 0, 0), + ('LOAD_NAME', 1, 0), + ('CONTAINS_OP', notin, 0), + ('RETURN_VALUE', None, 0), + ] + self.cfg_optimization_test(before, after, consts=[], expected_consts=[]) + + # test in/notin cancel out eliminate to bool (to bool stays) (maybe optimize later ???) + before = [ + ('LOAD_NAME', 0, 0), + ('LOAD_NAME', 1, 0), + ('CONTAINS_OP', in_, 0), + ('UNARY_NOT', None, 0), + ('UNARY_NOT', None, 0), + ('TO_BOOL', None, 0), + ('RETURN_VALUE', None, 0), + ] + after = [ + ('LOAD_NAME', 0, 0), + ('LOAD_NAME', 1, 0), + ('CONTAINS_OP', in_, 0), + ('TO_BOOL', None, 0), + ('RETURN_VALUE', None, 0), + ] + self.cfg_optimization_test(before, after, consts=[], expected_consts=[]) + + # test in/notin eliminate to bool before = [ ('LOAD_NAME', 0, 0), ('LOAD_NAME', 1, 0), @@ -1702,17 +1828,34 @@ def test_optimize_unary_not_to_bool(self): ('TO_BOOL', None, 0), ('TO_BOOL', None, 0), ('TO_BOOL', None, 0), - ('LOAD_CONST', 0, 0), ('RETURN_VALUE', None, 0), ] after = [ ('LOAD_NAME', 0, 0), ('LOAD_NAME', 1, 0), ('CONTAINS_OP', notin, 0), - ('LOAD_CONST', 0, 0), ('RETURN_VALUE', None, 0), ] - self.cfg_optimization_test(before, after, consts=[None], expected_consts=[None]) + self.cfg_optimization_test(before, after, consts=[], expected_consts=[]) + + # test in/notin cancel out eliminate to bool (to bool stays) (maybe optimize later ???) + before = [ + ('LOAD_NAME', 0, 0), + ('LOAD_NAME', 1, 0), + ('CONTAINS_OP', in_, 0), + ('UNARY_NOT', None, 0), + ('UNARY_NOT', None, 0), + ('TO_BOOL', None, 0), + ('RETURN_VALUE', None, 0), + ] + after = [ + ('LOAD_NAME', 0, 0), + ('LOAD_NAME', 1, 0), + ('CONTAINS_OP', in_, 0), + ('TO_BOOL', None, 0), + ('RETURN_VALUE', None, 0), + ] + self.cfg_optimization_test(before, after, consts=[], expected_consts=[]) def test_optimize_if_const_unaryop(self): pass From 598adced181e1b5e195f0d998f4c0903e198ce13 Mon Sep 17 00:00:00 2001 From: Yan Yanchii Date: Sun, 16 Feb 2025 09:32:18 +0100 Subject: [PATCH 14/34] add optimize_unary_not_non_const --- Python/flowgraph.c | 54 +++++++++++++++++++++------------------------- 1 file changed, 25 insertions(+), 29 deletions(-) diff --git a/Python/flowgraph.c b/Python/flowgraph.c index 2aa38c3edfafdd..69bd5e76a547ef 100644 --- a/Python/flowgraph.c +++ b/Python/flowgraph.c @@ -1768,7 +1768,6 @@ remove_to_bool_sequence(basicblock *bb, int start) for (;start < bb->b_iused; start++) { cfg_instr *curr = &bb->b_instr[start]; if (curr->i_opcode == NOP) { - /* should not happen, but just in case */ continue; } if (curr->i_opcode == TO_BOOL) { @@ -1796,23 +1795,6 @@ find_unary_not_target(basicblock *bb, int start) return NULL; } -static bool -maybe_unary_not_cancel_out(basicblock *bb, int i, int nextop) -{ - cfg_instr *instr = &bb->b_instr[i]; - assert(instr->i_opcode == UNARY_NOT); - if (nextop == UNARY_NOT) { - INSTR_SET_OP0(instr, NOP); - int nexi = i + 1; - assert(nexi >= 0 && nexi < bb->b_iused); - cfg_instr *next = &bb->b_instr[nexi]; - assert(next->i_opcode == nextop); - INSTR_SET_OP0(next, NOP); - return true; - } - return false; -} - /* Replace: IS_OP(is) UNARY_NOT @@ -1845,6 +1827,29 @@ optimize_unary_not_is_contains(basicblock *bb, int i, int nextop) return true; } +static bool +optimize_unary_not_non_const(basicblock *bb, int i, int nextop) +{ + assert(i >= 0 && i < bb->b_iused); + cfg_instr *instr = &bb->b_instr[i]; + assert(instr->i_opcode == UNARY_NOT); + if (nextop == UNARY_NOT) { /* subsequent UNARY_NOT is no-op */ + INSTR_SET_OP0(instr, NOP); + int nexti = i + 1; + assert(nexti >= 1 && nexti < bb->b_iused); + cfg_instr *next = &bb->b_instr[nexti]; + assert(next->i_opcode == nextop); + INSTR_SET_OP0(next, NOP); + if (find_unary_not_target(bb, i-1) != NULL) { + /* IS_OP & CONTAINS_OP don't need TO_BOOL conversion */ + remove_to_bool_sequence(bb, i+1); + } + return true; + } + remove_to_bool_sequence(bb, i+1); + return optimize_unary_not_is_contains(bb, i, nextop); +} + static int optimize_if_const_unaryop(basicblock *bb, int i, int nextop, PyObject *consts, PyObject *const_cache, bool unarypos) @@ -1863,16 +1868,8 @@ optimize_if_const_unaryop(basicblock *bb, int i, int nextop, || instr->i_opcode == UNARY_NOT ); } - if (instr->i_opcode == UNARY_NOT) { - if (maybe_unary_not_cancel_out(bb, i, nextop)) { - /* nothing more to do here */ - return SUCCESS; - } - remove_to_bool_sequence(bb, i+1); - if (optimize_unary_not_is_contains(bb, i, nextop)) { - /* if optimized, no need to continue */ - return SUCCESS; - } + if (instr->i_opcode == UNARY_NOT && optimize_unary_not_non_const(bb, i, nextop)) { + return SUCCESS; } PyObject *seq; RETURN_IF_ERROR(get_constant_sequence(bb, i-1, 1, consts, &seq)); @@ -1881,7 +1878,6 @@ optimize_if_const_unaryop(basicblock *bb, int i, int nextop, PyObject *operand = PyTuple_GET_ITEM(seq, 0); PyObject *newconst = eval_const_unaryop(operand, instr->i_opcode, unarypos); if (instr->i_opcode == UNARY_NOT) { - /* we eliminated TO_BOOL's so we must return boolean */ assert(PyBool_Check(newconst)); } Py_DECREF(seq); From bb60d4cf23d6ffc36ca7c672a5ed244e1daa9abc Mon Sep 17 00:00:00 2001 From: Yan Yanchii Date: Sun, 16 Feb 2025 09:58:05 +0100 Subject: [PATCH 15/34] add tests --- Lib/test/test_peepholer.py | 245 ++++++++++++++++++++++++++++++++++--- 1 file changed, 227 insertions(+), 18 deletions(-) diff --git a/Lib/test/test_peepholer.py b/Lib/test/test_peepholer.py index a13283dd13bc2a..a2001f65b453f1 100644 --- a/Lib/test/test_peepholer.py +++ b/Lib/test/test_peepholer.py @@ -1732,54 +1732,58 @@ def test_optimize_unary_not(self): ] self.cfg_optimization_test(before, after, consts=[], expected_consts=[]) - # test is/isnot eliminate to bool + # test is/isnot cancel out before = [ ('LOAD_NAME', 0, 0), ('LOAD_NAME', 1, 0), ('IS_OP', is_, 0), ('UNARY_NOT', None, 0), - ('TO_BOOL', None, 0), - ('TO_BOOL', None, 0), - ('TO_BOOL', None, 0), + ('UNARY_NOT', None, 0), ('RETURN_VALUE', None, 0), ] after = [ ('LOAD_NAME', 0, 0), ('LOAD_NAME', 1, 0), - ('IS_OP', isnot, 0), + ('IS_OP', is_, 0), ('RETURN_VALUE', None, 0), ] self.cfg_optimization_test(before, after, consts=[], expected_consts=[]) - # test in/notin + # test is/isnot eliminate to bool before = [ ('LOAD_NAME', 0, 0), ('LOAD_NAME', 1, 0), - ('CONTAINS_OP', in_, 0), + ('IS_OP', is_, 0), ('UNARY_NOT', None, 0), + ('TO_BOOL', None, 0), + ('TO_BOOL', None, 0), + ('TO_BOOL', None, 0), ('RETURN_VALUE', None, 0), ] after = [ ('LOAD_NAME', 0, 0), ('LOAD_NAME', 1, 0), - ('CONTAINS_OP', notin, 0), + ('IS_OP', isnot, 0), ('RETURN_VALUE', None, 0), ] self.cfg_optimization_test(before, after, consts=[], expected_consts=[]) - # test in/notin cancel out + # test is/isnot cancel out eliminate to bool before = [ ('LOAD_NAME', 0, 0), ('LOAD_NAME', 1, 0), - ('CONTAINS_OP', in_, 0), + ('IS_OP', is_, 0), ('UNARY_NOT', None, 0), ('UNARY_NOT', None, 0), + ('TO_BOOL', None, 0), + ('TO_BOOL', None, 0), + ('TO_BOOL', None, 0), ('RETURN_VALUE', None, 0), ] after = [ ('LOAD_NAME', 0, 0), ('LOAD_NAME', 1, 0), - ('CONTAINS_OP', in_, 0), + ('IS_OP', is_, 0), ('RETURN_VALUE', None, 0), ] self.cfg_optimization_test(before, after, consts=[], expected_consts=[]) @@ -1800,26 +1804,24 @@ def test_optimize_unary_not(self): ] self.cfg_optimization_test(before, after, consts=[], expected_consts=[]) - # test in/notin cancel out eliminate to bool (to bool stays) (maybe optimize later ???) + # test in/notin cancel out before = [ ('LOAD_NAME', 0, 0), ('LOAD_NAME', 1, 0), ('CONTAINS_OP', in_, 0), ('UNARY_NOT', None, 0), ('UNARY_NOT', None, 0), - ('TO_BOOL', None, 0), ('RETURN_VALUE', None, 0), ] after = [ ('LOAD_NAME', 0, 0), ('LOAD_NAME', 1, 0), ('CONTAINS_OP', in_, 0), - ('TO_BOOL', None, 0), ('RETURN_VALUE', None, 0), ] self.cfg_optimization_test(before, after, consts=[], expected_consts=[]) - # test in/notin eliminate to bool + # test is/isnot eliminate to bool before = [ ('LOAD_NAME', 0, 0), ('LOAD_NAME', 1, 0), @@ -1838,7 +1840,7 @@ def test_optimize_unary_not(self): ] self.cfg_optimization_test(before, after, consts=[], expected_consts=[]) - # test in/notin cancel out eliminate to bool (to bool stays) (maybe optimize later ???) + # test in/notin cancel out eliminate to bool before = [ ('LOAD_NAME', 0, 0), ('LOAD_NAME', 1, 0), @@ -1852,7 +1854,6 @@ def test_optimize_unary_not(self): ('LOAD_NAME', 0, 0), ('LOAD_NAME', 1, 0), ('CONTAINS_OP', in_, 0), - ('TO_BOOL', None, 0), ('RETURN_VALUE', None, 0), ] self.cfg_optimization_test(before, after, consts=[], expected_consts=[]) @@ -1861,7 +1862,215 @@ def test_optimize_if_const_unaryop(self): pass def test_optimize_if_const_binop(self): - pass + add = get_binop_argval('NB_ADD') + sub = get_binop_argval('NB_SUBTRACT') + mul = get_binop_argval('NB_MULTIPLY') + div = get_binop_argval('NB_TRUE_DIVIDE') + floor = get_binop_argval('NB_FLOOR_DIVIDE') + rem = get_binop_argval('NB_REMAINDER') + pow = get_binop_argval('NB_POWER') + lshift = get_binop_argval('NB_LSHIFT') + rshift = get_binop_argval('NB_RSHIFT') + or_ = get_binop_argval('NB_OR') + and_ = get_binop_argval('NB_AND') + xor = get_binop_argval('NB_XOR') + subscr = get_binop_argval('NB_SUBSCR') + + # test add + before = [ + ('LOAD_SMALL_INT', 2, 0), + ('LOAD_SMALL_INT', 2, 0), + ('BINARY_OP', add, 0), + ('LOAD_SMALL_INT', 2, 0), + ('BINARY_OP', add, 0), + ('RETURN_VALUE', None, 0) + ] + after = [ + ('LOAD_SMALL_INT', 6, 0), + ('RETURN_VALUE', None, 0) + ] + self.cfg_optimization_test(before, after, consts=[], expected_consts=[]) + + # test sub + before = [ + ('LOAD_SMALL_INT', 2, 0), + ('LOAD_SMALL_INT', 2, 0), + ('BINARY_OP', sub, 0), + ('LOAD_SMALL_INT', 2, 0), + ('BINARY_OP', sub, 0), + ('RETURN_VALUE', None, 0) + ] + after = [ + ('LOAD_CONST', 0, 0), + ('RETURN_VALUE', None, 0) + ] + self.cfg_optimization_test(before, after, consts=[], expected_consts=[-2]) + + # test mul + before = [ + ('LOAD_SMALL_INT', 2, 0), + ('LOAD_SMALL_INT', 2, 0), + ('BINARY_OP', mul, 0), + ('LOAD_SMALL_INT', 2, 0), + ('BINARY_OP', mul, 0), + ('RETURN_VALUE', None, 0) + ] + after = [ + ('LOAD_SMALL_INT', 8, 0), + ('RETURN_VALUE', None, 0) + ] + self.cfg_optimization_test(before, after, consts=[], expected_consts=[]) + + # test div + before = [ + ('LOAD_SMALL_INT', 2, 0), + ('LOAD_SMALL_INT', 2, 0), + ('BINARY_OP', div, 0), + ('LOAD_SMALL_INT', 2, 0), + ('BINARY_OP', div, 0), + ('RETURN_VALUE', None, 0) + ] + after = [ + ('LOAD_CONST', 1, 0), + ('RETURN_VALUE', None, 0) + ] + self.cfg_optimization_test(before, after, consts=[], expected_consts=[1.0, 0.5]) + + # test floor + before = [ + ('LOAD_SMALL_INT', 2, 0), + ('LOAD_SMALL_INT', 2, 0), + ('BINARY_OP', floor, 0), + ('LOAD_SMALL_INT', 2, 0), + ('BINARY_OP', floor, 0), + ('RETURN_VALUE', None, 0) + ] + after = [ + ('LOAD_SMALL_INT', 0, 0), + ('RETURN_VALUE', None, 0) + ] + self.cfg_optimization_test(before, after, consts=[], expected_consts=[]) + + # test rem + before = [ + ('LOAD_SMALL_INT', 2, 0), + ('LOAD_SMALL_INT', 2, 0), + ('BINARY_OP', rem, 0), + ('LOAD_SMALL_INT', 2, 0), + ('BINARY_OP', rem, 0), + ('RETURN_VALUE', None, 0) + ] + after = [ + ('LOAD_SMALL_INT', 0, 0), + ('RETURN_VALUE', None, 0) + ] + self.cfg_optimization_test(before, after, consts=[], expected_consts=[]) + + # test pow + before = [ + ('LOAD_SMALL_INT', 2, 0), + ('LOAD_SMALL_INT', 2, 0), + ('BINARY_OP', pow, 0), + ('LOAD_SMALL_INT', 2, 0), + ('BINARY_OP', pow, 0), + ('RETURN_VALUE', None, 0) + ] + after = [ + ('LOAD_SMALL_INT', 16, 0), + ('RETURN_VALUE', None, 0) + ] + self.cfg_optimization_test(before, after, consts=[], expected_consts=[]) + + # test lshift + before = [ + ('LOAD_SMALL_INT', 1, 0), + ('LOAD_SMALL_INT', 1, 0), + ('BINARY_OP', lshift, 0), + ('LOAD_SMALL_INT', 1, 0), + ('BINARY_OP', lshift, 0), + ('RETURN_VALUE', None, 0) + ] + after = [ + ('LOAD_SMALL_INT', 4, 0), + ('RETURN_VALUE', None, 0) + ] + self.cfg_optimization_test(before, after, consts=[], expected_consts=[]) + + # test rshift + before = [ + ('LOAD_SMALL_INT', 4, 0), + ('LOAD_SMALL_INT', 1, 0), + ('BINARY_OP', rshift, 0), + ('LOAD_SMALL_INT', 1, 0), + ('BINARY_OP', rshift, 0), + ('RETURN_VALUE', None, 0) + ] + after = [ + ('LOAD_SMALL_INT', 1, 0), + ('RETURN_VALUE', None, 0) + ] + self.cfg_optimization_test(before, after, consts=[], expected_consts=[]) + + # test or + before = [ + ('LOAD_SMALL_INT', 1, 0), + ('LOAD_SMALL_INT', 2, 0), + ('BINARY_OP', or_, 0), + ('LOAD_SMALL_INT', 4, 0), + ('BINARY_OP', or_, 0), + ('RETURN_VALUE', None, 0) + ] + after = [ + ('LOAD_SMALL_INT', 7, 0), + ('RETURN_VALUE', None, 0) + ] + self.cfg_optimization_test(before, after, consts=[], expected_consts=[]) + + # test and + before = [ + ('LOAD_SMALL_INT', 1, 0), + ('LOAD_SMALL_INT', 1, 0), + ('BINARY_OP', and_, 0), + ('LOAD_SMALL_INT', 1, 0), + ('BINARY_OP', and_, 0), + ('RETURN_VALUE', None, 0) + ] + after = [ + ('LOAD_SMALL_INT', 1, 0), + ('RETURN_VALUE', None, 0) + ] + self.cfg_optimization_test(before, after, consts=[], expected_consts=[]) + + # test xor + before = [ + ('LOAD_SMALL_INT', 2, 0), + ('LOAD_SMALL_INT', 2, 0), + ('BINARY_OP', xor, 0), + ('LOAD_SMALL_INT', 2, 0), + ('BINARY_OP', xor, 0), + ('RETURN_VALUE', None, 0) + ] + after = [ + ('LOAD_SMALL_INT', 2, 0), + ('RETURN_VALUE', None, 0) + ] + self.cfg_optimization_test(before, after, consts=[], expected_consts=[]) + + # test subscr + before = [ + ('LOAD_CONST', 0, 0), + ('LOAD_SMALL_INT', 1, 0), + ('BINARY_OP', subscr, 0), + ('LOAD_SMALL_INT', 2, 0), + ('BINARY_OP', subscr, 0), + ('RETURN_VALUE', None, 0) + ] + after = [ + ('LOAD_SMALL_INT', 3, 0), + ('RETURN_VALUE', None, 0) + ] + self.cfg_optimization_test(before, after, consts=[(1, (1, 2, 3))], expected_consts=[(1, (1, 2, 3))]) + def test_conditional_jump_forward_const_condition(self): # The unreachable branch of the jump is removed, the jump From eb998702010734ec55a7517e4a1c5448120398bb Mon Sep 17 00:00:00 2001 From: Yan Yanchii Date: Sun, 16 Feb 2025 10:08:52 +0100 Subject: [PATCH 16/34] add tests --- Lib/test/test_peepholer.py | 39 +++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_peepholer.py b/Lib/test/test_peepholer.py index a2001f65b453f1..4f9468b61937de 100644 --- a/Lib/test/test_peepholer.py +++ b/Lib/test/test_peepholer.py @@ -1859,7 +1859,44 @@ def test_optimize_unary_not(self): self.cfg_optimization_test(before, after, consts=[], expected_consts=[]) def test_optimize_if_const_unaryop(self): - pass + # test unary negative + before = [ + ('LOAD_SMALL_INT', 2, 0), + ('UNARY_NEGATIVE', None, 0), + ('UNARY_NEGATIVE', None, 0), + ('RETURN_VALUE', None, 0) + ] + after = [ + ('LOAD_SMALL_INT', 2, 0), + ('RETURN_VALUE', None, 0), + ] + self.cfg_optimization_test(before, after, consts=[], expected_consts=[-2]) + + # test unary invert + before = [ + ('LOAD_SMALL_INT', 2, 0), + ('UNARY_INVERT', None, 0), + ('UNARY_INVERT', None, 0), + ('RETURN_VALUE', None, 0) + ] + after = [ + ('LOAD_SMALL_INT', 2, 0), + ('RETURN_VALUE', None, 0), + ] + self.cfg_optimization_test(before, after, consts=[], expected_consts=[-3]) + + # test unary positive + before = [ + ('LOAD_SMALL_INT', 2, 0), + ('CALL_INTRINSIC_1', 5, 0), + ('CALL_INTRINSIC_1', 5, 0), + ('RETURN_VALUE', None, 0) + ] + after = [ + ('LOAD_SMALL_INT', 2, 0), + ('RETURN_VALUE', None, 0), + ] + self.cfg_optimization_test(before, after, consts=[], expected_consts=[]) def test_optimize_if_const_binop(self): add = get_binop_argval('NB_ADD') From c93627e556fb855269296b13fcdc1bab043c9592 Mon Sep 17 00:00:00 2001 From: Yan Yanchii Date: Sun, 16 Feb 2025 10:15:36 +0100 Subject: [PATCH 17/34] add static --- Python/flowgraph.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Python/flowgraph.c b/Python/flowgraph.c index 69bd5e76a547ef..ea46c9af0afb83 100644 --- a/Python/flowgraph.c +++ b/Python/flowgraph.c @@ -1778,7 +1778,7 @@ remove_to_bool_sequence(basicblock *bb, int start) } } -cfg_instr * +static cfg_instr * find_unary_not_target(basicblock *bb, int start) { assert(start < bb->b_iused); From f0f044a484e1342ccda7c68cd69c0f0fa8a768f9 Mon Sep 17 00:00:00 2001 From: Yan Yanchii Date: Sun, 16 Feb 2025 12:17:32 +0100 Subject: [PATCH 18/34] polish --- Python/flowgraph.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Python/flowgraph.c b/Python/flowgraph.c index ea46c9af0afb83..0cc0b2c69b96ec 100644 --- a/Python/flowgraph.c +++ b/Python/flowgraph.c @@ -1741,6 +1741,7 @@ unary_not(PyObject *v) static PyObject * eval_const_unaryop(PyObject *operand, int op, bool unarypos) { + assert(operand != NULL); if (unarypos) { return PyNumber_Positive(operand); } @@ -1846,6 +1847,7 @@ optimize_unary_not_non_const(basicblock *bb, int i, int nextop) } return true; } + /* single UNARY_NOT doesn't need TO_BOOL conversion */ remove_to_bool_sequence(bb, i+1); return optimize_unary_not_is_contains(bb, i, nextop); } From a02ac6631262ad48ae9ba42962fdc8faa2cd7311 Mon Sep 17 00:00:00 2001 From: Yan Yanchii Date: Sun, 16 Feb 2025 13:06:43 +0100 Subject: [PATCH 19/34] add unaryop folding tests --- Lib/test/test_peepholer.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_peepholer.py b/Lib/test/test_peepholer.py index 4f9468b61937de..ac21381588279d 100644 --- a/Lib/test/test_peepholer.py +++ b/Lib/test/test_peepholer.py @@ -516,7 +516,26 @@ def test_constant_folding_small_int(self): self.check_lnotab(code) def test_folding_unaryop(self): - pass + intrinsic_positive = 5 + tests = [ + ('---1', 'UNARY_NEGATIVE', None, True), + ('---""', 'UNARY_NEGATIVE', None, False), + ('~~~1', 'UNARY_INVERT', None, True), + ('~~~""', 'UNARY_INVERT', None, False), + ('not not True', 'UNARY_NOT', None, True), + ('not not x', 'UNARY_NOT', None, False), + ('+++1', 'CALL_INTRINSIC_1', intrinsic_positive, True), + ('+++x', 'CALL_INTRINSIC_1', intrinsic_positive, False), + ] + + for expr, opcode, oparg, optimized in tests: + with self.subTest(expr=expr, optimized=optimized): + code = compile(expr, '', 'single') + if optimized: + self.assertNotInBytecode(code, opcode, argval=oparg) + else: + self.assertInBytecode(code, opcode, argval=oparg) + self.check_lnotab(code) def test_folding_binop(self): add = get_binop_argval('NB_ADD') From 2739fa0414e4ffd5e76ce5b165bd6a15690d08a3 Mon Sep 17 00:00:00 2001 From: Yan Yanchii Date: Mon, 17 Feb 2025 10:08:59 +0100 Subject: [PATCH 20/34] add not not test to catch misoptimized case --- Lib/test/test_peepholer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_peepholer.py b/Lib/test/test_peepholer.py index ac21381588279d..d837eb556bfab0 100644 --- a/Lib/test/test_peepholer.py +++ b/Lib/test/test_peepholer.py @@ -523,7 +523,7 @@ def test_folding_unaryop(self): ('~~~1', 'UNARY_INVERT', None, True), ('~~~""', 'UNARY_INVERT', None, False), ('not not True', 'UNARY_NOT', None, True), - ('not not x', 'UNARY_NOT', None, False), + ('not not x', 'UNARY_NOT', None, True), # this should be optimized regardless of constant or not ('+++1', 'CALL_INTRINSIC_1', intrinsic_positive, True), ('+++x', 'CALL_INTRINSIC_1', intrinsic_positive, False), ] From 258a5b6bc457c002f24d04cbf1c3f40733b5b092 Mon Sep 17 00:00:00 2001 From: Yan Yanchii Date: Mon, 17 Feb 2025 10:47:32 +0100 Subject: [PATCH 21/34] revert old unarynot handing, add contains/is + unarynot folding --- Python/flowgraph.c | 112 ++++++++------------------------------------- 1 file changed, 19 insertions(+), 93 deletions(-) diff --git a/Python/flowgraph.c b/Python/flowgraph.c index 0cc0b2c69b96ec..2437c4a59ec105 100644 --- a/Python/flowgraph.c +++ b/Python/flowgraph.c @@ -1762,96 +1762,6 @@ eval_const_unaryop(PyObject *operand, int op, bool unarypos) return result; } -static void -remove_to_bool_sequence(basicblock *bb, int start) -{ - assert(start >= 0); - for (;start < bb->b_iused; start++) { - cfg_instr *curr = &bb->b_instr[start]; - if (curr->i_opcode == NOP) { - continue; - } - if (curr->i_opcode == TO_BOOL) { - INSTR_SET_OP0(curr, NOP); - continue; - } - break; - } -} - -static cfg_instr * -find_unary_not_target(basicblock *bb, int start) -{ - assert(start < bb->b_iused); - for (; start >= 0; start--) { - cfg_instr *instr = &bb->b_instr[start]; - if (instr->i_opcode == NOP) { - continue; - } - if (instr->i_opcode == IS_OP || instr->i_opcode == CONTAINS_OP) { - return instr; - } - break; - } - return NULL; -} - -/* Replace: - IS_OP(is) - UNARY_NOT - with: - IS_OP(is not) - NOP - or vice versa ("is not" to "is"). - - And: - CONTAINS_OP(in) - UNARY_NOT - with: - CONTAINS_OP(not in) - NOP - or vice versa ("not in" to "in"). -*/ -static bool -optimize_unary_not_is_contains(basicblock *bb, int i, int nextop) -{ - cfg_instr *instr = &bb->b_instr[i]; - assert(instr->i_opcode == UNARY_NOT); - cfg_instr *target = find_unary_not_target(bb, i-1); - if (target == NULL) { - return false; - } - int inverted = target->i_oparg ^ 1; - assert(inverted == 0 || inverted == 1); - INSTR_SET_OP1(target, target->i_opcode, inverted); - INSTR_SET_OP0(instr, NOP); - return true; -} - -static bool -optimize_unary_not_non_const(basicblock *bb, int i, int nextop) -{ - assert(i >= 0 && i < bb->b_iused); - cfg_instr *instr = &bb->b_instr[i]; - assert(instr->i_opcode == UNARY_NOT); - if (nextop == UNARY_NOT) { /* subsequent UNARY_NOT is no-op */ - INSTR_SET_OP0(instr, NOP); - int nexti = i + 1; - assert(nexti >= 1 && nexti < bb->b_iused); - cfg_instr *next = &bb->b_instr[nexti]; - assert(next->i_opcode == nextop); - INSTR_SET_OP0(next, NOP); - if (find_unary_not_target(bb, i-1) != NULL) { - /* IS_OP & CONTAINS_OP don't need TO_BOOL conversion */ - remove_to_bool_sequence(bb, i+1); - } - return true; - } - /* single UNARY_NOT doesn't need TO_BOOL conversion */ - remove_to_bool_sequence(bb, i+1); - return optimize_unary_not_is_contains(bb, i, nextop); -} - static int optimize_if_const_unaryop(basicblock *bb, int i, int nextop, PyObject *consts, PyObject *const_cache, bool unarypos) @@ -1870,9 +1780,6 @@ optimize_if_const_unaryop(basicblock *bb, int i, int nextop, || instr->i_opcode == UNARY_NOT ); } - if (instr->i_opcode == UNARY_NOT && optimize_unary_not_non_const(bb, i, nextop)) { - return SUCCESS; - } PyObject *seq; RETURN_IF_ERROR(get_constant_sequence(bb, i-1, 1, consts, &seq)); RETURN_IF_NOT_CONST_SEQ(seq); @@ -2358,6 +2265,13 @@ optimize_basic_block(PyObject *const_cache, basicblock *bb, PyObject *consts) INSTR_SET_OP1(&bb->b_instr[i + 1], opcode, oparg); continue; } + if (nextop == UNARY_NOT) { + INSTR_SET_OP0(inst, NOP); + int inverted = oparg ^ 1; + assert(inverted == 0 || inverted == 1); + INSTR_SET_OP1(&bb->b_instr[i + 1], opcode, inverted); + continue; + } break; case TO_BOOL: if (nextop == TO_BOOL) { @@ -2368,6 +2282,18 @@ optimize_basic_block(PyObject *const_cache, basicblock *bb, PyObject *consts) case UNARY_NOT: case UNARY_INVERT: case UNARY_NEGATIVE: + if (opcode == UNARY_NOT) { + if (nextop == TO_BOOL) { + INSTR_SET_OP0(inst, NOP); + INSTR_SET_OP0(&bb->b_instr[i + 1], UNARY_NOT); + continue; + } + if (nextop == UNARY_NOT) { + INSTR_SET_OP0(inst, NOP); + INSTR_SET_OP0(&bb->b_instr[i + 1], NOP); + continue; + } + } RETURN_IF_ERROR(optimize_if_const_unaryop(bb, i, nextop, consts, const_cache, false)); break; case CALL_INTRINSIC_1: From 59ee8972ce36d5c94b1e19572eab2754c50971a4 Mon Sep 17 00:00:00 2001 From: Yan Yanchii Date: Mon, 17 Feb 2025 23:36:04 +0100 Subject: [PATCH 22/34] address reviews --- Lib/test/test_ast/test_ast.py | 44 +++++++- Python/ast_opt.c | 6 +- Python/flowgraph.c | 186 +++++++++++++++++----------------- 3 files changed, 139 insertions(+), 97 deletions(-) diff --git a/Lib/test/test_ast/test_ast.py b/Lib/test/test_ast/test_ast.py index a145ef7fa63cbc..0150b0d3b1c55a 100644 --- a/Lib/test/test_ast/test_ast.py +++ b/Lib/test/test_ast/test_ast.py @@ -3232,6 +3232,7 @@ def test_folding_match_case_allowed_expressions(self): case -0: pass case -0.1: pass case -0j: pass + case -0.1j: pass case 1 + 2j: pass case 1 - 2j: pass case 1.1 + 2.1j: pass @@ -3243,6 +3244,7 @@ def test_folding_match_case_allowed_expressions(self): case {-0: 0}: pass case {-0.1: 0}: pass case {-0j: 0}: pass + case {-0.1j: 0}: pass case {1 + 2j: 0}: pass case {1 - 2j: 0}: pass case {1.1 + 2.1j: 0}: pass @@ -3252,11 +3254,15 @@ def test_folding_match_case_allowed_expressions(self): case {-0.1 + 1.1j: 0}: pass case {-0.1 - 1.1j: 0}: pass case {-0: 0, 0 + 1j: 0, 0.1 + 1j: 0}: pass + case [-0, -0.1, -0j, -0.1j]: pass + case [[-0, -0.1], [-0j, -0.1j]]: pass + case ((-0, -0.1), (-0j, -0.1j)): pass """) expected_constants = ( 0, -0.1, complex(0, -0), + complex(0, -0.1), complex(1, 2), complex(1, -2), complex(1.1, 2.1), @@ -3268,6 +3274,7 @@ def test_folding_match_case_allowed_expressions(self): (0, ), (-0.1, ), (complex(0, -0), ), + (complex(0, -0.1), ), (complex(1, 2), ), (complex(1, -2), ), (complex(1.1, 2.1), ), @@ -3276,7 +3283,31 @@ def test_folding_match_case_allowed_expressions(self): (complex(-0, -1), ), (complex(-0.1, 1.1), ), (complex(-0.1, -1.1), ), - (0, complex(0, 1), complex(0.1, 1)) + (0, complex(0, 1), complex(0.1, 1)), + ( + 0, + -0.1, + complex(0, -0), + complex(0, -0.1), + ), + ( + 0, + -0.1, + complex(0, -0), + complex(0, -0.1), + ), + ( + 0, + -0.1, + complex(0, -0), + complex(0, -0.1), + ), + ( + 0, + -0.1, + complex(0, -0), + complex(0, -0.1), + ) ) consts = iter(expected_constants) tree = ast.parse(source, optimize=1) @@ -3291,6 +3322,17 @@ def test_folding_match_case_allowed_expressions(self): for key in pattern.keys: self.assertIsInstance(key, ast.Constant) self.assertEqual(key.value, next(keys)) + elif isinstance(pattern, ast.MatchSequence): + values = iter(next(consts)) + for pat in pattern.patterns: + if isinstance(pat, ast.MatchValue): + self.assertEqual(pat.value.value, next(values)) + elif isinstance(pat, ast.MatchSequence): + for p in pat.patterns: + self.assertIsInstance(p, ast.MatchValue) + self.assertEqual(p.value.value, next(values)) + else: + self.fail(f"Expected ast.MatchValue or ast.MatchSequence, found: {type(pat)}") else: self.fail(f"Expected ast.MatchValue or ast.MatchMapping, found: {type(pattern)}") diff --git a/Python/ast_opt.c b/Python/ast_opt.c index 8554c9d0571030..34f54d4309e186 100644 --- a/Python/ast_opt.c +++ b/Python/ast_opt.c @@ -732,7 +732,7 @@ astfold_withitem(withitem_ty node_, PyArena *ctx_, _PyASTOptimizeState *state) #define CONST_EXPR_VALUE(N) \ ((N)->v.Constant.value) -#define IS_COMPLEX_CONST_EXPR(N) \ +#define IS_CONST_COMPLEX_EXPR(N) \ (IS_CONST_EXPR(N) && PyComplex_CheckExact(CONST_EXPR_VALUE(N))) #define IS_NUMERIC_CONST_EXPR(N) \ @@ -757,7 +757,7 @@ astfold_withitem(withitem_ty node_, PyArena *ctx_, _PyASTOptimizeState *state) (IS_UNARY_SUB_EXPR(N) && IS_NUMERIC_CONST_EXPR(UNARY_EXPR_OPERAND(N))) #define IS_MATCH_COMPLEX_UNARY_CONST_EXPR(N) \ - (IS_UNARY_SUB_EXPR(N) && IS_COMPLEX_CONST_EXPR(UNARY_EXPR_OPERAND(N))) + (IS_UNARY_SUB_EXPR(N) && IS_CONST_COMPLEX_EXPR(UNARY_EXPR_OPERAND(N))) #define BINARY_EXPR(N) \ ((N)->v.BinOp) @@ -787,7 +787,7 @@ astfold_withitem(withitem_ty node_, PyArena *ctx_, _PyASTOptimizeState *state) ( \ (IS_BINARY_ADD_EXPR(N) || IS_BINARY_SUB_EXPR(N)) \ && (IS_MATCH_NUMERIC_UNARY_CONST_EXPR(BINARY_EXPR_LEFT(N)) || IS_NUMERIC_CONST_EXPR(BINARY_EXPR_LEFT(N))) \ - && IS_COMPLEX_CONST_EXPR(BINARY_EXPR_RIGHT(N)) \ + && IS_CONST_COMPLEX_EXPR(BINARY_EXPR_RIGHT(N)) \ ) diff --git a/Python/flowgraph.c b/Python/flowgraph.c index 2437c4a59ec105..f2b9cba605782d 100644 --- a/Python/flowgraph.c +++ b/Python/flowgraph.c @@ -23,9 +23,9 @@ return ERROR; \ } -#define RETURN_IF_NOT_CONST_SEQ(X) \ - if ((X) == NULL) { \ - return SUCCESS; \ +#define RETURN_SUCCESS_IF_NULL(X) \ + if ((X) == NULL) { \ + return SUCCESS; \ } #define DEFAULT_BLOCK_SIZE 16 @@ -1411,44 +1411,27 @@ nop_out(basicblock *bb, int start, int count) } } -/* Determine opcode & oparg for freshly folded constant. - Steals reference to "newconst". -*/ +/* Steals reference to "newconst" */ static int -newop_from_folded(PyObject *newconst, PyObject *consts, - PyObject *const_cache, int *newopcode, int *newoparg) +instr_make_load_const(cfg_instr *instr, PyObject *newconst, + PyObject *consts, PyObject *const_cache) { + int opcode, oparg; if (PyLong_CheckExact(newconst)) { int overflow; long val = PyLong_AsLongAndOverflow(newconst, &overflow); if (!overflow && _PY_IS_SMALL_INT(val)) { - *newopcode = LOAD_SMALL_INT; - *newoparg = val; - return SUCCESS; + assert(_Py_IsImmortal(newconst)); + opcode = LOAD_SMALL_INT; + oparg = (int)val; + goto exit; } } - *newopcode = LOAD_CONST; - *newoparg = add_const(newconst, consts, const_cache); - RETURN_IF_ERROR(*newoparg); - return SUCCESS; -} - -/* Steals reference to "newconst" */ -static int -make_const(PyObject *newconst, basicblock *bb, - int i, int nop, PyObject *consts, PyObject *const_cache) -{ - if (newconst == NULL) { - if (PyErr_ExceptionMatches(PyExc_KeyboardInterrupt)) { - return ERROR; - } - PyErr_Clear(); - return SUCCESS; - } - int newopcode, newoparg; - RETURN_IF_ERROR(newop_from_folded(newconst, consts, const_cache, &newopcode, &newoparg)); - nop_out(bb, i-1, nop); - INSTR_SET_OP1(&bb->b_instr[i], newopcode, newoparg); + opcode = LOAD_CONST; + oparg = add_const(newconst, consts, const_cache); + RETURN_IF_ERROR(oparg); +exit: + INSTR_SET_OP1(instr, opcode, oparg); return SUCCESS; } @@ -1469,9 +1452,11 @@ fold_tuple_of_constants(basicblock *bb, int i, PyObject *consts, PyObject *const int seq_size = instr->i_oparg; PyObject *newconst; RETURN_IF_ERROR(get_constant_sequence(bb, i-1, seq_size, consts, &newconst)); - RETURN_IF_NOT_CONST_SEQ(newconst); + RETURN_SUCCESS_IF_NULL(newconst); assert(PyTuple_Size(newconst) == seq_size); - return make_const(newconst, bb, i, seq_size, consts, const_cache); + RETURN_IF_ERROR(instr_make_load_const(instr, newconst, consts, const_cache)); + nop_out(bb, i-1, seq_size); + return SUCCESS; } #define MIN_CONST_SEQUENCE_SIZE 3 @@ -1536,7 +1521,7 @@ optimize_lists_and_sets(basicblock *bb, int i, int nextop, return SUCCESS; } -/* Check whether a collection doesn't containing too much items (including +/* Check whether a collection doesn't contain too many items (including subcollections). This protects from creating a constant that needs too much time for calculating a hash. "limit" is the maximal number of items. @@ -1544,15 +1529,17 @@ optimize_lists_and_sets(basicblock *bb, int i, int nextop, limit. Otherwise returns the limit minus the total number of items. */ static Py_ssize_t -check_complexity(PyObject *obj, Py_ssize_t limit) +const_folding_check_complexity(PyObject *obj, Py_ssize_t limit) { if (PyTuple_Check(obj)) { Py_ssize_t i; limit -= PyTuple_GET_SIZE(obj); for (i = 0; limit >= 0 && i < PyTuple_GET_SIZE(obj); i++) { - limit = check_complexity(PyTuple_GET_ITEM(obj, i), limit); + limit = const_folding_check_complexity(PyTuple_GET_ITEM(obj, i), limit); + if (limit < 0) { + break; + } } - return limit; } return limit; } @@ -1563,7 +1550,7 @@ check_complexity(PyObject *obj, Py_ssize_t limit) #define MAX_TOTAL_ITEMS 1024 /* including nested collections */ static PyObject * -safe_multiply(PyObject *v, PyObject *w) +const_folding_safe_multiply(PyObject *v, PyObject *w) { if (PyLong_Check(v) && PyLong_Check(w) && !_PyLong_IsZero((PyLongObject *)v) && !_PyLong_IsZero((PyLongObject *)w) @@ -1583,7 +1570,7 @@ safe_multiply(PyObject *v, PyObject *w) if (n < 0 || n > MAX_COLLECTION_SIZE / size) { return NULL; } - if (n && check_complexity(w, MAX_TOTAL_ITEMS / n) < 0) { + if (n && const_folding_check_complexity(w, MAX_TOTAL_ITEMS / n) < 0) { return NULL; } } @@ -1601,14 +1588,14 @@ safe_multiply(PyObject *v, PyObject *w) else if (PyLong_Check(w) && (PyTuple_Check(v) || PyUnicode_Check(v) || PyBytes_Check(v))) { - return safe_multiply(w, v); + return const_folding_safe_multiply(w, v); } return PyNumber_Multiply(v, w); } static PyObject * -safe_power(PyObject *v, PyObject *w) +const_folding_safe_power(PyObject *v, PyObject *w) { if (PyLong_Check(v) && PyLong_Check(w) && !_PyLong_IsZero((PyLongObject *)v) && _PyLong_IsPositive((PyLongObject *)w) @@ -1628,7 +1615,7 @@ safe_power(PyObject *v, PyObject *w) } static PyObject * -safe_lshift(PyObject *v, PyObject *w) +const_folding_safe_lshift(PyObject *v, PyObject *w) { if (PyLong_Check(v) && PyLong_Check(w) && !_PyLong_IsZero((PyLongObject *)v) && !_PyLong_IsZero((PyLongObject *)w) @@ -1648,7 +1635,7 @@ safe_lshift(PyObject *v, PyObject *w) } static PyObject * -safe_mod(PyObject *v, PyObject *w) +const_folding_safe_mod(PyObject *v, PyObject *w) { if (PyUnicode_Check(v) || PyBytes_Check(v)) { return NULL; @@ -1672,7 +1659,7 @@ eval_const_binop(PyObject *left, int op, PyObject *right) result = PyNumber_Subtract(left, right); break; case NB_MULTIPLY: - result = safe_multiply(left, right); + result = const_folding_safe_multiply(left, right); break; case NB_TRUE_DIVIDE: result = PyNumber_TrueDivide(left, right); @@ -1681,13 +1668,13 @@ eval_const_binop(PyObject *left, int op, PyObject *right) result = PyNumber_FloorDivide(left, right); break; case NB_REMAINDER: - result = safe_mod(left, right); + result = const_folding_safe_mod(left, right); break; case NB_POWER: - result = safe_power(left, right); + result = const_folding_safe_power(left, right); break; case NB_LSHIFT: - result = safe_lshift(left, right); + result = const_folding_safe_lshift(left, right); break; case NB_RSHIFT: result = PyNumber_Rshift(left, right); @@ -1716,21 +1703,31 @@ eval_const_binop(PyObject *left, int op, PyObject *right) static int optimize_if_const_binop(basicblock *bb, int i, PyObject *consts, PyObject *const_cache) { + #define BINOP_OPERAND_COUNT 2 cfg_instr *binop = &bb->b_instr[i]; assert(binop->i_opcode == BINARY_OP); PyObject *pair; - RETURN_IF_ERROR(get_constant_sequence(bb, i-1, 2, consts, &pair)); - RETURN_IF_NOT_CONST_SEQ(pair); - assert(PyTuple_Size(pair) == 2); + RETURN_IF_ERROR(get_constant_sequence(bb, i-1, BINOP_OPERAND_COUNT, consts, &pair)); + RETURN_SUCCESS_IF_NULL(pair); + assert(PyTuple_Size(pair) == BINOP_OPERAND_COUNT); PyObject *left = PyTuple_GET_ITEM(pair, 0); PyObject *right = PyTuple_GET_ITEM(pair, 1); PyObject *newconst = eval_const_binop(left, binop->i_oparg, right); Py_DECREF(pair); - return make_const(newconst, bb, i, 2, consts, const_cache); + if (newconst == NULL) { + if (PyErr_ExceptionMatches(PyExc_KeyboardInterrupt)) { + return ERROR; + } + PyErr_Clear(); + return SUCCESS; + } + RETURN_IF_ERROR(instr_make_load_const(binop, newconst, consts, const_cache)); + nop_out(bb, i-1, BINOP_OPERAND_COUNT); + return SUCCESS; } static PyObject* -unary_not(PyObject *v) +const_folding_unary_not(PyObject *v) { int r = PyObject_IsTrue(v); if (r < 0) @@ -1739,12 +1736,9 @@ unary_not(PyObject *v) } static PyObject * -eval_const_unaryop(PyObject *operand, int op, bool unarypos) +eval_const_unaryop(PyObject *operand, int op) { assert(operand != NULL); - if (unarypos) { - return PyNumber_Positive(operand); - } PyObject *result; switch (op) { case UNARY_NEGATIVE: @@ -1754,7 +1748,7 @@ eval_const_unaryop(PyObject *operand, int op, bool unarypos) result = PyNumber_Invert(operand); break; case UNARY_NOT: - result = unary_not(operand); + result = const_folding_unary_not(operand); break; default: Py_UNREACHABLE(); @@ -1763,34 +1757,41 @@ eval_const_unaryop(PyObject *operand, int op, bool unarypos) } static int -optimize_if_const_unaryop(basicblock *bb, int i, int nextop, - PyObject *consts, PyObject *const_cache, bool unarypos) +optimize_if_const_unaryop(basicblock *bb, int i, + PyObject *consts, PyObject *const_cache) { + #define UNARYOP_OPERAND_COUNT 1 cfg_instr *instr = &bb->b_instr[i]; - if (unarypos) { - assert( - instr->i_opcode == CALL_INTRINSIC_1 - && instr->i_oparg == INTRINSIC_UNARY_POSITIVE - ); - } - else { - assert( - instr->i_opcode == UNARY_NEGATIVE - || instr->i_opcode == UNARY_INVERT - || instr->i_opcode == UNARY_NOT - ); + assert( + instr->i_opcode == UNARY_NEGATIVE + || instr->i_opcode == UNARY_INVERT + || instr->i_opcode == UNARY_NOT + || instr->i_opcode == CALL_INTRINSIC_1 + ); + if (instr->i_opcode == CALL_INTRINSIC_1) { + assert(instr->i_oparg == INTRINSIC_UNARY_POSITIVE); } PyObject *seq; - RETURN_IF_ERROR(get_constant_sequence(bb, i-1, 1, consts, &seq)); - RETURN_IF_NOT_CONST_SEQ(seq); - assert(PyTuple_Size(seq) == 1); + RETURN_IF_ERROR(get_constant_sequence(bb, i-1, UNARYOP_OPERAND_COUNT, consts, &seq)); + RETURN_SUCCESS_IF_NULL(seq); + assert(PyTuple_Size(seq) == UNARYOP_OPERAND_COUNT); PyObject *operand = PyTuple_GET_ITEM(seq, 0); - PyObject *newconst = eval_const_unaryop(operand, instr->i_opcode, unarypos); + bool unarypos = instr->i_opcode == CALL_INTRINSIC_1; + PyObject *newconst = unarypos ? PyNumber_Positive(operand) : eval_const_unaryop(operand, instr->i_opcode); + Py_DECREF(seq); + if (newconst == NULL) { + if (PyErr_ExceptionMatches(PyExc_KeyboardInterrupt)) { + return ERROR; + } + PyErr_Clear(); + return SUCCESS; + } if (instr->i_opcode == UNARY_NOT) { assert(PyBool_Check(newconst)); } - Py_DECREF(seq); - return make_const(newconst, bb, i, 1, consts, const_cache); + RETURN_IF_ERROR(instr_make_load_const(instr, newconst, consts, const_cache)); + nop_out(bb, i-1, UNARYOP_OPERAND_COUNT); + return SUCCESS; } #define VISITED (-1) @@ -2280,21 +2281,20 @@ optimize_basic_block(PyObject *const_cache, basicblock *bb, PyObject *consts) } break; case UNARY_NOT: + if (nextop == TO_BOOL) { + INSTR_SET_OP0(inst, NOP); + INSTR_SET_OP0(&bb->b_instr[i + 1], UNARY_NOT); + continue; + } + if (nextop == UNARY_NOT) { + INSTR_SET_OP0(inst, NOP); + INSTR_SET_OP0(&bb->b_instr[i + 1], NOP); + continue; + } + _Py_FALLTHROUGH; case UNARY_INVERT: case UNARY_NEGATIVE: - if (opcode == UNARY_NOT) { - if (nextop == TO_BOOL) { - INSTR_SET_OP0(inst, NOP); - INSTR_SET_OP0(&bb->b_instr[i + 1], UNARY_NOT); - continue; - } - if (nextop == UNARY_NOT) { - INSTR_SET_OP0(inst, NOP); - INSTR_SET_OP0(&bb->b_instr[i + 1], NOP); - continue; - } - } - RETURN_IF_ERROR(optimize_if_const_unaryop(bb, i, nextop, consts, const_cache, false)); + RETURN_IF_ERROR(optimize_if_const_unaryop(bb, i, consts, const_cache)); break; case CALL_INTRINSIC_1: // for _ in (*foo, *bar) -> for _ in [*foo, *bar] @@ -2302,7 +2302,7 @@ optimize_basic_block(PyObject *const_cache, basicblock *bb, PyObject *consts) INSTR_SET_OP0(inst, NOP); } else if (oparg == INTRINSIC_UNARY_POSITIVE) { - RETURN_IF_ERROR(optimize_if_const_unaryop(bb, i, nextop, consts, const_cache, true)); + RETURN_IF_ERROR(optimize_if_const_unaryop(bb, i, consts, const_cache)); } break; case BINARY_OP: From 91ea2fa6c3648299c29cd95cface9e2ff68f2bec Mon Sep 17 00:00:00 2001 From: Yan Yanchii Date: Mon, 17 Feb 2025 23:37:37 +0100 Subject: [PATCH 23/34] address reviews --- Lib/test/test_ast/test_ast.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_ast/test_ast.py b/Lib/test/test_ast/test_ast.py index 0150b0d3b1c55a..24a86b891f3bd5 100644 --- a/Lib/test/test_ast/test_ast.py +++ b/Lib/test/test_ast/test_ast.py @@ -3255,8 +3255,9 @@ def test_folding_match_case_allowed_expressions(self): case {-0.1 - 1.1j: 0}: pass case {-0: 0, 0 + 1j: 0, 0.1 + 1j: 0}: pass case [-0, -0.1, -0j, -0.1j]: pass - case [[-0, -0.1], [-0j, -0.1j]]: pass - case ((-0, -0.1), (-0j, -0.1j)): pass + case (-0, -0.1, -0j, -0.1j): pass + case [[-0, -0.1], [-0j, -0.1j]]: pass + case ((-0, -0.1), (-0j, -0.1j)): pass """) expected_constants = ( 0, From 2c5ee8619144bb6b5412238b65c7eb7bdbd95134 Mon Sep 17 00:00:00 2001 From: Yan Yanchii Date: Mon, 17 Feb 2025 23:52:21 +0100 Subject: [PATCH 24/34] simplify optimize_if_const_unaryop --- Python/flowgraph.c | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Python/flowgraph.c b/Python/flowgraph.c index f2b9cba605782d..cfddd8d52c81a2 100644 --- a/Python/flowgraph.c +++ b/Python/flowgraph.c @@ -1766,11 +1766,8 @@ optimize_if_const_unaryop(basicblock *bb, int i, instr->i_opcode == UNARY_NEGATIVE || instr->i_opcode == UNARY_INVERT || instr->i_opcode == UNARY_NOT - || instr->i_opcode == CALL_INTRINSIC_1 + || (instr->i_opcode == CALL_INTRINSIC_1 && instr->i_oparg == INTRINSIC_UNARY_POSITIVE) ); - if (instr->i_opcode == CALL_INTRINSIC_1) { - assert(instr->i_oparg == INTRINSIC_UNARY_POSITIVE); - } PyObject *seq; RETURN_IF_ERROR(get_constant_sequence(bb, i-1, UNARYOP_OPERAND_COUNT, consts, &seq)); RETURN_SUCCESS_IF_NULL(seq); From 0399fce74a855be4d71a9ab348fc653585cb3f6f Mon Sep 17 00:00:00 2001 From: Yan Yanchii Date: Wed, 19 Feb 2025 19:34:16 +0100 Subject: [PATCH 25/34] address reviews --- Lib/test/test_ast/test_ast.py | 166 ++++++++++++---------------------- Lib/test/test_peepholer.py | 10 +- 2 files changed, 64 insertions(+), 112 deletions(-) diff --git a/Lib/test/test_ast/test_ast.py b/Lib/test/test_ast/test_ast.py index 24a86b891f3bd5..8f461a8ed9a56f 100644 --- a/Lib/test/test_ast/test_ast.py +++ b/Lib/test/test_ast/test_ast.py @@ -3227,115 +3227,65 @@ def test_folding_type_param_in_type_alias(self): self.assert_ast(result_code, non_optimized_target, optimized_target) def test_folding_match_case_allowed_expressions(self): - source = textwrap.dedent(""" - match 0: - case -0: pass - case -0.1: pass - case -0j: pass - case -0.1j: pass - case 1 + 2j: pass - case 1 - 2j: pass - case 1.1 + 2.1j: pass - case 1.1 - 2.1j: pass - case -0 + 1j: pass - case -0 - 1j: pass - case -0.1 + 1.1j: pass - case -0.1 - 1.1j: pass - case {-0: 0}: pass - case {-0.1: 0}: pass - case {-0j: 0}: pass - case {-0.1j: 0}: pass - case {1 + 2j: 0}: pass - case {1 - 2j: 0}: pass - case {1.1 + 2.1j: 0}: pass - case {1.1 - 2.1j: 0}: pass - case {-0 + 1j: 0}: pass - case {-0 - 1j: 0}: pass - case {-0.1 + 1.1j: 0}: pass - case {-0.1 - 1.1j: 0}: pass - case {-0: 0, 0 + 1j: 0, 0.1 + 1j: 0}: pass - case [-0, -0.1, -0j, -0.1j]: pass - case (-0, -0.1, -0j, -0.1j): pass - case [[-0, -0.1], [-0j, -0.1j]]: pass - case ((-0, -0.1), (-0j, -0.1j)): pass - """) - expected_constants = ( - 0, - -0.1, - complex(0, -0), - complex(0, -0.1), - complex(1, 2), - complex(1, -2), - complex(1.1, 2.1), - complex(1.1, -2.1), - complex(-0, 1), - complex(-0, -1), - complex(-0.1, 1.1), - complex(-0.1, -1.1), - (0, ), - (-0.1, ), - (complex(0, -0), ), - (complex(0, -0.1), ), - (complex(1, 2), ), - (complex(1, -2), ), - (complex(1.1, 2.1), ), - (complex(1.1, -2.1), ), - (complex(-0, 1), ), - (complex(-0, -1), ), - (complex(-0.1, 1.1), ), - (complex(-0.1, -1.1), ), - (0, complex(0, 1), complex(0.1, 1)), - ( - 0, - -0.1, - complex(0, -0), - complex(0, -0.1), - ), - ( - 0, - -0.1, - complex(0, -0), - complex(0, -0.1), - ), - ( - 0, - -0.1, - complex(0, -0), - complex(0, -0.1), - ), - ( - 0, - -0.1, - complex(0, -0), - complex(0, -0.1), - ) - ) - consts = iter(expected_constants) - tree = ast.parse(source, optimize=1) - match_stmt = tree.body[0] - for case in match_stmt.cases: - pattern = case.pattern - if isinstance(pattern, ast.MatchValue): - self.assertIsInstance(pattern.value, ast.Constant) - self.assertEqual(pattern.value.value, next(consts)) - elif isinstance(pattern, ast.MatchMapping): - keys = iter(next(consts)) - for key in pattern.keys: - self.assertIsInstance(key, ast.Constant) - self.assertEqual(key.value, next(keys)) - elif isinstance(pattern, ast.MatchSequence): - values = iter(next(consts)) - for pat in pattern.patterns: - if isinstance(pat, ast.MatchValue): - self.assertEqual(pat.value.value, next(values)) - elif isinstance(pat, ast.MatchSequence): - for p in pat.patterns: - self.assertIsInstance(p, ast.MatchValue) - self.assertEqual(p.value.value, next(values)) - else: - self.fail(f"Expected ast.MatchValue or ast.MatchSequence, found: {type(pat)}") + def get_match_case_values(node): + result = [] + if isinstance(node, ast.Constant): + result.append(node.value) + elif isinstance(node, ast.MatchValue): + result.extend(get_match_case_values(node.value)) + elif isinstance(node, ast.MatchMapping): + for key in node.keys: + result.extend(get_match_case_values(key)) + elif isinstance(node, ast.MatchSequence): + for pat in node.patterns: + result.extend(get_match_case_values(pat)) else: - self.fail(f"Expected ast.MatchValue or ast.MatchMapping, found: {type(pattern)}") + self.fail(f"Unexpected node {node}") + return result + + tests = [ + ("-0", [0]), + ("-0.1", [-0.1]), + ("-0j", [complex(0, 0)]), + ("-0.1j", [complex(0, -0.1)]), + ("1 + 2j", [complex(1, 2)]), + ("1 - 2j", [complex(1, -2)]), + ("1.1 + 2.1j", [complex(1.1, 2.1)]), + ("1.1 - 2.1j", [complex(1.1, -2.1)]), + ("-0 + 1j", [complex(0, 1)]), + ("-0 - 1j", [complex(0, -1)]), + ("-0.1 + 1.1j", [complex(-0.1, 1.1)]), + ("-0.1 - 1.1j", [complex(-0.1, -1.1)]), + ("{-0: 0}", [0]), + ("{-0.1: 0}", [-0.1]), + ("{-0j: 0}", [complex(0, 0)]), + ("{-0.1j: 0}", [complex(0, -0.1)]), + ("{1 + 2j: 0}", [complex(1, 2)]), + ("{1 - 2j: 0}", [complex(1, -2)]), + ("{1.1 + 2.1j: 0}", [complex(1.1, 2.1)]), + ("{1.1 - 2.1j: 0}", [complex(1.1, -2.1)]), + ("{-0 + 1j: 0}", [complex(0, 1)]), + ("{-0 - 1j: 0}", [complex(0, -1)]), + ("{-0.1 + 1.1j: 0}", [complex(-0.1, 1.1)]), + ("{-0.1 - 1.1j: 0}", [complex(-0.1, -1.1)]), + ("{-0: 0, 0 + 1j: 0, 0.1 + 1j: 0}", [0, complex(0, 1), complex(0.1, 1)]), + ("[-0, -0.1, -0j, -0.1j]", [0, -0.1, complex(0, 0), complex(0, -0.1)]), + ("[[[[-0, -0.1, -0j, -0.1j]]]]", [0, -0.1, complex(0, 0), complex(0, -0.1)]), + ("[[-0, -0.1], -0j, -0.1j]", [0, -0.1, complex(0, 0), complex(0, -0.1)]), + ("[[-0, -0.1], [-0j, -0.1j]]", [0, -0.1, complex(0, 0), complex(0, -0.1)]), + ("(-0, -0.1, -0j, -0.1j)", [0, -0.1, complex(0, 0), complex(0, -0.1)]), + ("((((-0, -0.1, -0j, -0.1j))))", [0, -0.1, complex(0, 0), complex(0, -0.1)]), + ("((-0, -0.1), -0j, -0.1j)", [0, -0.1, complex(0, 0), complex(0, -0.1)]), + ("((-0, -0.1), (-0j, -0.1j))", [0, -0.1, complex(0, 0), complex(0, -0.1)]), + ] + for match_expr, constants in tests: + with self.subTest(match_expr): + src = f"match 0:\n\t case {match_expr}: pass" + tree = ast.parse(src, optimize=1) + match_stmt = tree.body[0] + case = match_stmt.cases[0] + values = get_match_case_values(case.pattern) + self.assertListEqual(constants, values) if __name__ == '__main__': diff --git a/Lib/test/test_peepholer.py b/Lib/test/test_peepholer.py index d837eb556bfab0..4f770b40c885b0 100644 --- a/Lib/test/test_peepholer.py +++ b/Lib/test/test_peepholer.py @@ -525,6 +525,8 @@ def test_folding_unaryop(self): ('not not True', 'UNARY_NOT', None, True), ('not not x', 'UNARY_NOT', None, True), # this should be optimized regardless of constant or not ('+++1', 'CALL_INTRINSIC_1', intrinsic_positive, True), + ('---x', 'UNARY_NEGATIVE', None, False), + ('~~~x', 'UNARY_INVERT', None, False), ('+++x', 'CALL_INTRINSIC_1', intrinsic_positive, False), ] @@ -1700,7 +1702,7 @@ def test_optimize_unary_not(self): ] self.cfg_optimization_test(before, after, consts=[], expected_consts=[]) - # test folding & elimitnate to bool + # test folding & eliminate to bool before = [ ('LOAD_SMALL_INT', 1, 0), ('UNARY_NOT', None, 0), @@ -1787,7 +1789,7 @@ def test_optimize_unary_not(self): ] self.cfg_optimization_test(before, after, consts=[], expected_consts=[]) - # test is/isnot cancel out eliminate to bool + # test is/isnot cancel out & eliminate to bool before = [ ('LOAD_NAME', 0, 0), ('LOAD_NAME', 1, 0), @@ -1840,7 +1842,7 @@ def test_optimize_unary_not(self): ] self.cfg_optimization_test(before, after, consts=[], expected_consts=[]) - # test is/isnot eliminate to bool + # test is/isnot & eliminate to bool before = [ ('LOAD_NAME', 0, 0), ('LOAD_NAME', 1, 0), @@ -1859,7 +1861,7 @@ def test_optimize_unary_not(self): ] self.cfg_optimization_test(before, after, consts=[], expected_consts=[]) - # test in/notin cancel out eliminate to bool + # test in/notin cancel out & eliminate to bool before = [ ('LOAD_NAME', 0, 0), ('LOAD_NAME', 1, 0), From 3c5392318c067001932545943b15f4189c3b9d9e Mon Sep 17 00:00:00 2001 From: Yan Yanchii Date: Wed, 19 Feb 2025 22:03:24 +0100 Subject: [PATCH 26/34] simplify instr_make_load_const --- Python/flowgraph.c | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/Python/flowgraph.c b/Python/flowgraph.c index cfddd8d52c81a2..57d4ce8e42a23f 100644 --- a/Python/flowgraph.c +++ b/Python/flowgraph.c @@ -1416,22 +1416,18 @@ static int instr_make_load_const(cfg_instr *instr, PyObject *newconst, PyObject *consts, PyObject *const_cache) { - int opcode, oparg; if (PyLong_CheckExact(newconst)) { int overflow; long val = PyLong_AsLongAndOverflow(newconst, &overflow); if (!overflow && _PY_IS_SMALL_INT(val)) { assert(_Py_IsImmortal(newconst)); - opcode = LOAD_SMALL_INT; - oparg = (int)val; - goto exit; + INSTR_SET_OP1(instr, LOAD_SMALL_INT, (int)val); + return SUCCESS; } } - opcode = LOAD_CONST; - oparg = add_const(newconst, consts, const_cache); + int oparg = add_const(newconst, consts, const_cache); RETURN_IF_ERROR(oparg); -exit: - INSTR_SET_OP1(instr, opcode, oparg); + INSTR_SET_OP1(instr, LOAD_CONST, oparg); return SUCCESS; } From a6a06c80c4fabb8c31663b7a708555a38ad5fc40 Mon Sep 17 00:00:00 2001 From: Yan Yanchii Date: Thu, 20 Feb 2025 13:50:47 +0100 Subject: [PATCH 27/34] address review for tests --- Lib/test/test_peepholer.py | 130 +++++++++++++++++-------------------- 1 file changed, 59 insertions(+), 71 deletions(-) diff --git a/Lib/test/test_peepholer.py b/Lib/test/test_peepholer.py index 4f770b40c885b0..39e7b10837a585 100644 --- a/Lib/test/test_peepholer.py +++ b/Lib/test/test_peepholer.py @@ -540,84 +540,72 @@ def test_folding_unaryop(self): self.check_lnotab(code) def test_folding_binop(self): - add = get_binop_argval('NB_ADD') - sub = get_binop_argval('NB_SUBTRACT') - mul = get_binop_argval('NB_MULTIPLY') - div = get_binop_argval('NB_TRUE_DIVIDE') - floor = get_binop_argval('NB_FLOOR_DIVIDE') - rem = get_binop_argval('NB_REMAINDER') - pow = get_binop_argval('NB_POWER') - lshift = get_binop_argval('NB_LSHIFT') - rshift = get_binop_argval('NB_RSHIFT') - or_ = get_binop_argval('NB_OR') - and_ = get_binop_argval('NB_AND') - xor = get_binop_argval('NB_XOR') - subscr = get_binop_argval('NB_SUBSCR') tests = [ - ('1 + 2', False, add), - ('1 + 2 + 3', False, add), - ('1 + ""', True, add), - ('1 - 2', False, sub), - ('1 - 2 - 3', False, sub), - ('1 - ""', True, sub), - ('2 * 2', False, mul), - ('2 * 2 * 2', False, mul), - ('2 / 2', False, div), - ('2 / 2 / 2', False, div), - ('2 / ""', True, div), - ('2 // 2', False, floor), - ('2 // 2 // 2', False, floor), - ('2 // ""', True, floor), - ('2 % 2', False, rem), - ('2 % 2 % 2', False, rem), - ('2 % ()', True, rem), - ('2 ** 2', False, pow), - ('2 ** 2 ** 2', False, pow), - ('2 ** ""', True, pow), - ('2 << 2', False, lshift), - ('2 << 2 << 2', False, lshift), - ('2 << ""', True, lshift), - ('2 >> 2', False, rshift), - ('2 >> 2 >> 2', False, rshift), - ('2 >> ""', True, rshift), - ('2 | 2', False, or_), - ('2 | 2 | 2', False, or_), - ('2 | ""', True, or_), - ('2 & 2', False, and_), - ('2 & 2 & 2', False, and_), - ('2 & ""', True, and_), - ('2 ^ 2', False, xor), - ('2 ^ 2 ^ 2', False, xor), - ('2 ^ ""', True, xor), - ('(1, )[0]', False, subscr), - ('(1, )[-1]', False, subscr), - ('(1 + 2, )[0]', False, subscr), - ('(1, (1, 2))[1][1]', False, subscr), - ('(1, 2)[2-1]', False, subscr), - ('(1, (1, 2))[1][2-1]', False, subscr), - ('(1, (1, 2))[1:6][0][2-1]', False, subscr), - ('"a"[0]', False, subscr), - ('("a" + "b")[1]', False, subscr), - ('("a" + "b", )[0][1]', False, subscr), - ('("a" * 10)[9]', False, subscr), - ('(1, )[1]', True, subscr), - ('(1, )[-2]', True, subscr), - ('"a"[1]', True, subscr), - ('"a"[-2]', True, subscr), - ('("a" + "b")[2]', True, subscr), - ('("a" + "b", )[0][2]', True, subscr), - ('("a" + "b", )[1][0]', True, subscr), - ('("a" * 10)[10]', True, subscr), - ('(1, (1, 2))[2:6][0][2-1]', True, subscr), + ('1 + 2', False, 'NB_ADD'), + ('1 + 2 + 3', False, 'NB_ADD'), + ('1 + ""', True, 'NB_ADD'), + ('1 - 2', False, 'NB_SUBTRACT'), + ('1 - 2 - 3', False, 'NB_SUBTRACT'), + ('1 - ""', True, 'NB_SUBTRACT'), + ('2 * 2', False, 'NB_MULTIPLY'), + ('2 * 2 * 2', False, 'NB_MULTIPLY'), + ('2 / 2', False, 'NB_TRUE_DIVIDE'), + ('2 / 2 / 2', False, 'NB_TRUE_DIVIDE'), + ('2 / ""', True, 'NB_TRUE_DIVIDE'), + ('2 // 2', False, 'NB_FLOOR_DIVIDE'), + ('2 // 2 // 2', False, 'NB_FLOOR_DIVIDE'), + ('2 // ""', True, 'NB_FLOOR_DIVIDE'), + ('2 % 2', False, 'NB_REMAINDER'), + ('2 % 2 % 2', False, 'NB_REMAINDER'), + ('2 % ()', True, 'NB_REMAINDER'), + ('2 ** 2', False, 'NB_POWER'), + ('2 ** 2 ** 2', False, 'NB_POWER'), + ('2 ** ""', True, 'NB_POWER'), + ('2 << 2', False, 'NB_LSHIFT'), + ('2 << 2 << 2', False, 'NB_LSHIFT'), + ('2 << ""', True, 'NB_LSHIFT'), + ('2 >> 2', False, 'NB_RSHIFT'), + ('2 >> 2 >> 2', False, 'NB_RSHIFT'), + ('2 >> ""', True, 'NB_RSHIFT'), + ('2 | 2', False, 'NB_OR'), + ('2 | 2 | 2', False, 'NB_OR'), + ('2 | ""', True, 'NB_OR'), + ('2 & 2', False, 'NB_AND'), + ('2 & 2 & 2', False, 'NB_AND'), + ('2 & ""', True, 'NB_AND'), + ('2 ^ 2', False, 'NB_XOR'), + ('2 ^ 2 ^ 2', False, 'NB_XOR'), + ('2 ^ ""', True, 'NB_XOR'), + ('(1, )[0]', False, 'NB_SUBSCR'), + ('(1, )[-1]', False, 'NB_SUBSCR'), + ('(1 + 2, )[0]', False, 'NB_SUBSCR'), + ('(1, (1, 2))[1][1]', False, 'NB_SUBSCR'), + ('(1, 2)[2-1]', False, 'NB_SUBSCR'), + ('(1, (1, 2))[1][2-1]', False, 'NB_SUBSCR'), + ('(1, (1, 2))[1:6][0][2-1]', False, 'NB_SUBSCR'), + ('"a"[0]', False, 'NB_SUBSCR'), + ('("a" + "b")[1]', False, 'NB_SUBSCR'), + ('("a" + "b", )[0][1]', False, 'NB_SUBSCR'), + ('("a" * 10)[9]', False, 'NB_SUBSCR'), + ('(1, )[1]', True, 'NB_SUBSCR'), + ('(1, )[-2]', True, 'NB_SUBSCR'), + ('"a"[1]', True, 'NB_SUBSCR'), + ('"a"[-2]', True, 'NB_SUBSCR'), + ('("a" + "b")[2]', True, 'NB_SUBSCR'), + ('("a" + "b", )[0][2]', True, 'NB_SUBSCR'), + ('("a" + "b", )[1][0]', True, 'NB_SUBSCR'), + ('("a" * 10)[10]', True, 'NB_SUBSCR'), + ('(1, (1, 2))[2:6][0][2-1]', True, 'NB_SUBSCR'), ] for expr, has_error, nb_op in tests: with self.subTest(expr=expr, has_error=has_error): code = compile(expr, '', 'single') + nb_op_val = get_binop_argval(nb_op) if not has_error: - self.assertNotInBytecode(code, 'BINARY_OP', argval=nb_op) + self.assertNotInBytecode(code, 'BINARY_OP', argval=nb_op_val) else: - self.assertInBytecode(code, 'BINARY_OP', argval=nb_op) + self.assertInBytecode(code, 'BINARY_OP', argval=nb_op_val) self.check_lnotab(code) def test_constant_folding_remove_nop_location(self): @@ -1258,7 +1246,7 @@ def f(): } self.assertEqual(f(), frozenset(range(40))) - def test_multiple_foldings(self): + def test_nested_const_foldings(self): # (1, (--2 + ++2 * 2 // 2 - 2, )[0], ~~3, not not True) ==> (1, 2, 3, True) intrinsic_positive = 5 before = [ From 2ea8425bb5e14cf111599691c75a861d413d1f92 Mon Sep 17 00:00:00 2001 From: Yan Yanchii Date: Thu, 20 Feb 2025 15:17:14 +0100 Subject: [PATCH 28/34] replace macros --- Python/ast_opt.c | 132 +++++++++++++++++++++-------------------------- 1 file changed, 58 insertions(+), 74 deletions(-) diff --git a/Python/ast_opt.c b/Python/ast_opt.c index 34f54d4309e186..66984422e02f33 100644 --- a/Python/ast_opt.c +++ b/Python/ast_opt.c @@ -726,79 +726,63 @@ astfold_withitem(withitem_ty node_, PyArena *ctx_, _PyASTOptimizeState *state) return 1; } -#define IS_CONST_EXPR(N) \ - ((N)->kind == Constant_kind) - -#define CONST_EXPR_VALUE(N) \ - ((N)->v.Constant.value) - -#define IS_CONST_COMPLEX_EXPR(N) \ - (IS_CONST_EXPR(N) && PyComplex_CheckExact(CONST_EXPR_VALUE(N))) - -#define IS_NUMERIC_CONST_EXPR(N) \ - (IS_CONST_EXPR(N) && (PyLong_CheckExact(CONST_EXPR_VALUE(N)) || PyFloat_CheckExact(CONST_EXPR_VALUE(N)))) - -#define IS_UNARY_EXPR(N) \ - ((N)->kind == UnaryOp_kind) - -#define UNARY_EXPR_OP(N) \ - ((N)->v.UnaryOp.op) - -#define UNARY_EXPR_OPERAND(N) \ - ((N)->v.UnaryOp.operand) - -#define UNARY_EXPR_OPERAND_CONST_VALUE(N) \ - (CONST_EXPR_VALUE(UNARY_EXPR_OPERAND(N))) - -#define IS_UNARY_SUB_EXPR(N) \ - (IS_UNARY_EXPR(N) && UNARY_EXPR_OP(N) == USub) - -#define IS_MATCH_NUMERIC_UNARY_CONST_EXPR(N) \ - (IS_UNARY_SUB_EXPR(N) && IS_NUMERIC_CONST_EXPR(UNARY_EXPR_OPERAND(N))) - -#define IS_MATCH_COMPLEX_UNARY_CONST_EXPR(N) \ - (IS_UNARY_SUB_EXPR(N) && IS_CONST_COMPLEX_EXPR(UNARY_EXPR_OPERAND(N))) - -#define BINARY_EXPR(N) \ - ((N)->v.BinOp) - -#define BINARY_EXPR_OP(N) \ - (BINARY_EXPR(N).op) - -#define BINARY_EXPR_LEFT(N) \ - (BINARY_EXPR(N).left) - -#define BINARY_EXPR_RIGHT(N) \ - (BINARY_EXPR(N).right) - -#define IS_BINARY_EXPR(N) \ - ((N)->kind == BinOp_kind) - -#define IS_BINARY_ADD_EXPR(N) \ - (IS_BINARY_EXPR(N) && BINARY_EXPR_OP(N) == Add) - -#define IS_BINARY_SUB_EXPR(N) \ - (IS_BINARY_EXPR(N) && BINARY_EXPR_OP(N) == Sub) - -#define IS_MATCH_NUMERIC_OR_COMPLEX_UNARY_CONST_EXPR(N) \ - (IS_MATCH_NUMERIC_UNARY_CONST_EXPR(N) || IS_MATCH_COMPLEX_UNARY_CONST_EXPR(N)) +static bool +is_unarynegative_const_numeric_expr(expr_ty node) +{ + return ( + node->kind == UnaryOp_kind + && node->v.UnaryOp.op == USub + && node->v.UnaryOp.operand->kind == Constant_kind + && ( + PyLong_CheckExact(node->v.UnaryOp.operand->v.Constant.value) + || PyFloat_CheckExact(node->v.UnaryOp.operand->v.Constant.value) + ) + ); +} -#define IS_MATCH_COMPLEX_BINARY_CONST_EXPR(N) \ - ( \ - (IS_BINARY_ADD_EXPR(N) || IS_BINARY_SUB_EXPR(N)) \ - && (IS_MATCH_NUMERIC_UNARY_CONST_EXPR(BINARY_EXPR_LEFT(N)) || IS_NUMERIC_CONST_EXPR(BINARY_EXPR_LEFT(N))) \ - && IS_CONST_COMPLEX_EXPR(BINARY_EXPR_RIGHT(N)) \ - ) +static bool +is_unarynegative_const_complex_expr(expr_ty node) +{ + return ( + node->kind == UnaryOp_kind + && node->v.UnaryOp.op == USub + && node->v.UnaryOp.operand->kind == Constant_kind + && PyComplex_CheckExact(node->v.UnaryOp.operand->v.Constant.value) + ); +} +static bool +is_allowed_match_case_binary_expr(expr_ty node) +{ + return ( + node->kind == BinOp_kind + && (node->v.BinOp.op == Add || node->v.BinOp.op == Sub) + && node->v.BinOp.right->kind == Constant_kind + && PyComplex_CheckExact(node->v.BinOp.right->v.Constant.value) + && ( + is_unarynegative_const_numeric_expr(node->v.BinOp.left) + || ( + node->v.BinOp.left->kind == Constant_kind + && ( + PyLong_CheckExact(node->v.BinOp.left->v.Constant.value) + || PyFloat_CheckExact(node->v.BinOp.left->v.Constant.value) + ) + ) + ) + ); +} static int fold_const_unary_or_complex_expr(expr_ty node, PyArena *ctx_, _PyASTOptimizeState * Py_UNUSED(state)) { - assert(IS_MATCH_NUMERIC_OR_COMPLEX_UNARY_CONST_EXPR(node)); - PyObject *constant = UNARY_EXPR_OPERAND_CONST_VALUE(node); - assert(UNARY_EXPR_OP(node) == USub); + assert( + is_unarynegative_const_numeric_expr(node) + || is_unarynegative_const_complex_expr(node) + ); + PyObject *constant = node->v.UnaryOp.operand->v.Constant.value; assert(constant != NULL); + assert(node->v.UnaryOp.op == USub); PyObject* folded = PyNumber_Negative(constant); return make_const(node, folded, ctx_); } @@ -806,18 +790,18 @@ fold_const_unary_or_complex_expr(expr_ty node, PyArena *ctx_, static int fold_const_binary_complex_expr(expr_ty node, PyArena *ctx_, _PyASTOptimizeState *state) { - assert(IS_MATCH_COMPLEX_BINARY_CONST_EXPR(node)); - expr_ty left_expr = BINARY_EXPR_LEFT(node); - if (IS_MATCH_NUMERIC_UNARY_CONST_EXPR(left_expr)) { + assert(is_allowed_match_case_binary_expr(node)); + expr_ty left_expr = node->v.BinOp.left; + if (is_unarynegative_const_numeric_expr(left_expr)) { CALL(fold_const_unary_or_complex_expr, expr_ty, left_expr); } - /* must have folded if left was IS_MATCH_NUMERIC_UNARY_CONST_EXPR */ - assert(IS_CONST_EXPR(BINARY_EXPR_LEFT(node))); - operator_ty op = BINARY_EXPR_OP(node); - PyObject *left = CONST_EXPR_VALUE(BINARY_EXPR_LEFT(node)); - PyObject *right = CONST_EXPR_VALUE(BINARY_EXPR_RIGHT(node)); - assert(op == Add || op == Sub); + /* must have folded left expr by now */ + assert(left_expr->kind == Constant_kind); + operator_ty op = node->v.BinOp.op; + PyObject *left = left_expr->v.Constant.value; + PyObject *right = node->v.BinOp.right->v.Constant.value; assert(left != NULL && right != NULL); + assert(op == Add || op == Sub); PyObject *folded = op == Add ? PyNumber_Add(left, right) : PyNumber_Subtract(left, right); return make_const(node, folded, ctx_); } From 7c9c69bd55a050619a69d00f73e0d51737844831 Mon Sep 17 00:00:00 2001 From: Yan Yanchii Date: Thu, 20 Feb 2025 15:17:24 +0100 Subject: [PATCH 29/34] update peepholer test --- Lib/test/test_peepholer.py | 42 ++++++++++++++++++++++++++++++-------- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/Lib/test/test_peepholer.py b/Lib/test/test_peepholer.py index 39e7b10837a585..98f6b29dc7fc5e 100644 --- a/Lib/test/test_peepholer.py +++ b/Lib/test/test_peepholer.py @@ -1650,18 +1650,20 @@ def test_optimize_unary_not(self): # test folding before = [ ('LOAD_SMALL_INT', 1, 0), + ('TO_BOOL', None, 0), ('UNARY_NOT', None, 0), ('RETURN_VALUE', None, 0), ] after = [ - ('LOAD_CONST', 0, 0), + ('LOAD_CONST', 1, 0), ('RETURN_VALUE', None, 0), ] - self.cfg_optimization_test(before, after, consts=[], expected_consts=[False]) + self.cfg_optimization_test(before, after, consts=[], expected_consts=[True, False]) # test cancel out before = [ - ('LOAD_SMALL_INT', 1, 0), + ('LOAD_NAME', 0, 0), + ('TO_BOOL', None, 0), ('UNARY_NOT', None, 0), ('UNARY_NOT', None, 0), ('UNARY_NOT', None, 0), @@ -1669,7 +1671,8 @@ def test_optimize_unary_not(self): ('RETURN_VALUE', None, 0), ] after = [ - ('LOAD_SMALL_INT', 1, 0), + ('LOAD_NAME', 0, 0), + ('TO_BOOL', None, 0), ('RETURN_VALUE', None, 0), ] self.cfg_optimization_test(before, after, consts=[], expected_consts=[]) @@ -1677,6 +1680,7 @@ def test_optimize_unary_not(self): # test eliminate to bool before = [ ('LOAD_NAME', 0, 0), + ('TO_BOOL', None, 0), ('UNARY_NOT', None, 0), ('TO_BOOL', None, 0), ('TO_BOOL', None, 0), @@ -1685,14 +1689,32 @@ def test_optimize_unary_not(self): ] after = [ ('LOAD_NAME', 0, 0), + ('TO_BOOL', None, 0), ('UNARY_NOT', None, 0), ('RETURN_VALUE', None, 0), ] self.cfg_optimization_test(before, after, consts=[], expected_consts=[]) + # test folding & cancel out + before = [ + ('LOAD_SMALL_INT', 1, 0), + ('TO_BOOL', None, 0), + ('UNARY_NOT', None, 0), + ('UNARY_NOT', None, 0), + ('UNARY_NOT', None, 0), + ('UNARY_NOT', None, 0), + ('RETURN_VALUE', None, 0), + ] + after = [ + ('LOAD_CONST', 0, 0), + ('RETURN_VALUE', None, 0), + ] + self.cfg_optimization_test(before, after, consts=[], expected_consts=[True]) + # test folding & eliminate to bool before = [ ('LOAD_SMALL_INT', 1, 0), + ('TO_BOOL', None, 0), ('UNARY_NOT', None, 0), ('TO_BOOL', None, 0), ('TO_BOOL', None, 0), @@ -1700,14 +1722,15 @@ def test_optimize_unary_not(self): ('RETURN_VALUE', None, 0), ] after = [ - ('LOAD_CONST', 0, 0), + ('LOAD_CONST', 1, 0), ('RETURN_VALUE', None, 0), ] - self.cfg_optimization_test(before, after, consts=[], expected_consts=[False]) + self.cfg_optimization_test(before, after, consts=[], expected_consts=[True, False]) - # test cancel out & eliminate to bool (to bool stays) + # test cancel out & eliminate to bool (to bool stays as we are not iterating to a fixed point) before = [ - ('LOAD_SMALL_INT', 1, 0), + ('LOAD_NAME', 0, 0), + ('TO_BOOL', None, 0), ('UNARY_NOT', None, 0), ('UNARY_NOT', None, 0), ('UNARY_NOT', None, 0), @@ -1716,7 +1739,8 @@ def test_optimize_unary_not(self): ('RETURN_VALUE', None, 0), ] after = [ - ('LOAD_SMALL_INT', 1, 0), + ('LOAD_NAME', 0, 0), + ('TO_BOOL', None, 0), ('TO_BOOL', None, 0), ('RETURN_VALUE', None, 0), ] From 099afbab8fbb4728ee6d25309c182b51b1645538 Mon Sep 17 00:00:00 2001 From: Yan Yanchii Date: Thu, 20 Feb 2025 17:54:40 +0100 Subject: [PATCH 30/34] define is_unarynegative_const_complex_expr & is_allowed_match_case_binary_expr only on debug builds --- Python/ast_opt.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Python/ast_opt.c b/Python/ast_opt.c index 66984422e02f33..3a44ca0e49439f 100644 --- a/Python/ast_opt.c +++ b/Python/ast_opt.c @@ -740,6 +740,8 @@ is_unarynegative_const_numeric_expr(expr_ty node) ); } +#ifndef NDEBUG + static bool is_unarynegative_const_complex_expr(expr_ty node) { @@ -772,6 +774,8 @@ is_allowed_match_case_binary_expr(expr_ty node) ); } +#endif + static int fold_const_unary_or_complex_expr(expr_ty node, PyArena *ctx_, _PyASTOptimizeState * Py_UNUSED(state)) From 0dad7c17cb8c59c0100b9e864de2c4edb4744900 Mon Sep 17 00:00:00 2001 From: Yan Yanchii Date: Thu, 20 Feb 2025 20:56:39 +0100 Subject: [PATCH 31/34] try to fold match case expression without checks --- Python/ast_opt.c | 127 ++++++++++++++--------------------------------- 1 file changed, 36 insertions(+), 91 deletions(-) diff --git a/Python/ast_opt.c b/Python/ast_opt.c index 3a44ca0e49439f..03bc0edba94a77 100644 --- a/Python/ast_opt.c +++ b/Python/ast_opt.c @@ -726,101 +726,46 @@ astfold_withitem(withitem_ty node_, PyArena *ctx_, _PyASTOptimizeState *state) return 1; } -static bool -is_unarynegative_const_numeric_expr(expr_ty node) -{ - return ( - node->kind == UnaryOp_kind - && node->v.UnaryOp.op == USub - && node->v.UnaryOp.operand->kind == Constant_kind - && ( - PyLong_CheckExact(node->v.UnaryOp.operand->v.Constant.value) - || PyFloat_CheckExact(node->v.UnaryOp.operand->v.Constant.value) - ) - ); -} - -#ifndef NDEBUG - -static bool -is_unarynegative_const_complex_expr(expr_ty node) -{ - return ( - node->kind == UnaryOp_kind - && node->v.UnaryOp.op == USub - && node->v.UnaryOp.operand->kind == Constant_kind - && PyComplex_CheckExact(node->v.UnaryOp.operand->v.Constant.value) - ); -} - -static bool -is_allowed_match_case_binary_expr(expr_ty node) -{ - return ( - node->kind == BinOp_kind - && (node->v.BinOp.op == Add || node->v.BinOp.op == Sub) - && node->v.BinOp.right->kind == Constant_kind - && PyComplex_CheckExact(node->v.BinOp.right->v.Constant.value) - && ( - is_unarynegative_const_numeric_expr(node->v.BinOp.left) - || ( - node->v.BinOp.left->kind == Constant_kind - && ( - PyLong_CheckExact(node->v.BinOp.left->v.Constant.value) - || PyFloat_CheckExact(node->v.BinOp.left->v.Constant.value) - ) - ) - ) - ); -} - -#endif - -static int -fold_const_unary_or_complex_expr(expr_ty node, PyArena *ctx_, - _PyASTOptimizeState * Py_UNUSED(state)) -{ - assert( - is_unarynegative_const_numeric_expr(node) - || is_unarynegative_const_complex_expr(node) - ); - PyObject *constant = node->v.UnaryOp.operand->v.Constant.value; - assert(constant != NULL); - assert(node->v.UnaryOp.op == USub); - PyObject* folded = PyNumber_Negative(constant); - return make_const(node, folded, ctx_); -} - static int -fold_const_binary_complex_expr(expr_ty node, PyArena *ctx_, _PyASTOptimizeState *state) -{ - assert(is_allowed_match_case_binary_expr(node)); - expr_ty left_expr = node->v.BinOp.left; - if (is_unarynegative_const_numeric_expr(left_expr)) { - CALL(fold_const_unary_or_complex_expr, expr_ty, left_expr); - } - /* must have folded left expr by now */ - assert(left_expr->kind == Constant_kind); - operator_ty op = node->v.BinOp.op; - PyObject *left = left_expr->v.Constant.value; - PyObject *right = node->v.BinOp.right->v.Constant.value; - assert(left != NULL && right != NULL); - assert(op == Add || op == Sub); - PyObject *folded = op == Add ? PyNumber_Add(left, right) : PyNumber_Subtract(left, right); - return make_const(node, folded, ctx_); -} - -static int -fold_pattern_match_value(expr_ty node, PyArena *ctx_, _PyASTOptimizeState *state) +try_fold_pattern_match_value(expr_ty node, PyArena *ctx_, _PyASTOptimizeState *state) { switch (node->kind) { case UnaryOp_kind: - CALL(fold_const_unary_or_complex_expr, expr_ty, node); - break; + { + if (node->v.UnaryOp.op != USub && + node->v.UnaryOp.operand->kind != Constant_kind) { + return 1; + } + PyObject *operand = node->v.UnaryOp.operand->v.Constant.value; + if (!PyLong_CheckExact(operand) && + !PyFloat_CheckExact(operand) && + !PyComplex_CheckExact(operand)) { + return 1; + } + PyObject *folded = PyNumber_Negative(operand); + return make_const(node, folded, ctx_); + } case BinOp_kind: - CALL(fold_const_binary_complex_expr, expr_ty, node); - break; + { + operator_ty op = node->v.BinOp.op; + if (op != Add && op != Sub) { + return 1; + } + CALL(try_fold_pattern_match_value, expr_ty, node->v.BinOp.left); + if (node->v.BinOp.left->kind != Constant_kind && + node->v.BinOp.right->kind != Constant_kind) { + return 1; + } + PyObject *left = node->v.BinOp.left->v.Constant.value; + PyObject *right = node->v.BinOp.right->v.Constant.value; + if (!PyComplex_CheckExact(right) || + (!PyLong_CheckExact(left) && !PyFloat_CheckExact(left))) { + return 1; + } + PyObject *folded = op == Add ? PyNumber_Add(left, right) : PyNumber_Subtract(left, right); + return make_const(node, folded, ctx_); + } default: break; } @@ -836,7 +781,7 @@ astfold_pattern(pattern_ty node_, PyArena *ctx_, _PyASTOptimizeState *state) ENTER_RECURSIVE(state); switch (node_->kind) { case MatchValue_kind: - CALL(fold_pattern_match_value, expr_ty, node_->v.MatchValue.value); + CALL(try_fold_pattern_match_value, expr_ty, node_->v.MatchValue.value); break; case MatchSingleton_kind: break; @@ -844,7 +789,7 @@ astfold_pattern(pattern_ty node_, PyArena *ctx_, _PyASTOptimizeState *state) CALL_SEQ(astfold_pattern, pattern, node_->v.MatchSequence.patterns); break; case MatchMapping_kind: - CALL_SEQ(fold_pattern_match_value, expr, node_->v.MatchMapping.keys); + CALL_SEQ(try_fold_pattern_match_value, expr, node_->v.MatchMapping.keys); CALL_SEQ(astfold_pattern, pattern, node_->v.MatchMapping.patterns); break; case MatchClass_kind: From 1e8b552a70480a9f05d33eb37bf7d7594b05fa12 Mon Sep 17 00:00:00 2001 From: Yan Yanchii Date: Thu, 20 Feb 2025 21:43:49 +0100 Subject: [PATCH 32/34] simplify folding --- Python/ast_opt.c | 48 +++++++++++++++++++----------------------------- 1 file changed, 19 insertions(+), 29 deletions(-) diff --git a/Python/ast_opt.c b/Python/ast_opt.c index 03bc0edba94a77..66596cfcb3e3c7 100644 --- a/Python/ast_opt.c +++ b/Python/ast_opt.c @@ -727,44 +727,34 @@ astfold_withitem(withitem_ty node_, PyArena *ctx_, _PyASTOptimizeState *state) } static int -try_fold_pattern_match_value(expr_ty node, PyArena *ctx_, _PyASTOptimizeState *state) +fold_const_match_patterns(expr_ty node, PyArena *ctx_, _PyASTOptimizeState *state) { switch (node->kind) { case UnaryOp_kind: { - if (node->v.UnaryOp.op != USub && - node->v.UnaryOp.operand->kind != Constant_kind) { - return 1; + if (node->v.UnaryOp.op == USub && + node->v.UnaryOp.operand->kind == Constant_kind) { + PyObject *operand = node->v.UnaryOp.operand->v.Constant.value; + PyObject *folded = PyNumber_Negative(operand); + return make_const(node, folded, ctx_); } - PyObject *operand = node->v.UnaryOp.operand->v.Constant.value; - if (!PyLong_CheckExact(operand) && - !PyFloat_CheckExact(operand) && - !PyComplex_CheckExact(operand)) { - return 1; - } - PyObject *folded = PyNumber_Negative(operand); - return make_const(node, folded, ctx_); + break; } case BinOp_kind: { operator_ty op = node->v.BinOp.op; - if (op != Add && op != Sub) { - return 1; - } - CALL(try_fold_pattern_match_value, expr_ty, node->v.BinOp.left); - if (node->v.BinOp.left->kind != Constant_kind && - node->v.BinOp.right->kind != Constant_kind) { - return 1; - } - PyObject *left = node->v.BinOp.left->v.Constant.value; - PyObject *right = node->v.BinOp.right->v.Constant.value; - if (!PyComplex_CheckExact(right) || - (!PyLong_CheckExact(left) && !PyFloat_CheckExact(left))) { - return 1; + if ((op == Add || op == Sub) && + node->v.BinOp.right->kind == Constant_kind) { + CALL(fold_const_match_patterns, expr_ty, node->v.BinOp.left); + if (node->v.BinOp.left->kind == Constant_kind) { + PyObject *left = node->v.BinOp.left->v.Constant.value; + PyObject *right = node->v.BinOp.right->v.Constant.value; + PyObject *folded = op == Add ? PyNumber_Add(left, right) : PyNumber_Subtract(left, right); + return make_const(node, folded, ctx_); + } } - PyObject *folded = op == Add ? PyNumber_Add(left, right) : PyNumber_Subtract(left, right); - return make_const(node, folded, ctx_); + break; } default: break; @@ -781,7 +771,7 @@ astfold_pattern(pattern_ty node_, PyArena *ctx_, _PyASTOptimizeState *state) ENTER_RECURSIVE(state); switch (node_->kind) { case MatchValue_kind: - CALL(try_fold_pattern_match_value, expr_ty, node_->v.MatchValue.value); + CALL(fold_const_match_patterns, expr_ty, node_->v.MatchValue.value); break; case MatchSingleton_kind: break; @@ -789,7 +779,7 @@ astfold_pattern(pattern_ty node_, PyArena *ctx_, _PyASTOptimizeState *state) CALL_SEQ(astfold_pattern, pattern, node_->v.MatchSequence.patterns); break; case MatchMapping_kind: - CALL_SEQ(try_fold_pattern_match_value, expr, node_->v.MatchMapping.keys); + CALL_SEQ(fold_const_match_patterns, expr, node_->v.MatchMapping.keys); CALL_SEQ(astfold_pattern, pattern, node_->v.MatchMapping.patterns); break; case MatchClass_kind: From f4e9a4277063c49242993e3d88e4aca23972eb0b Mon Sep 17 00:00:00 2001 From: Yan Yanchii Date: Thu, 20 Feb 2025 21:49:03 +0100 Subject: [PATCH 33/34] address review --- Python/flowgraph.c | 78 +++++++++++++++++++++++++--------------------- 1 file changed, 43 insertions(+), 35 deletions(-) diff --git a/Python/flowgraph.c b/Python/flowgraph.c index 57d4ce8e42a23f..615d69a0b4c941 100644 --- a/Python/flowgraph.c +++ b/Python/flowgraph.c @@ -23,11 +23,6 @@ return ERROR; \ } -#define RETURN_SUCCESS_IF_NULL(X) \ - if ((X) == NULL) { \ - return SUCCESS; \ - } - #define DEFAULT_BLOCK_SIZE 16 typedef _Py_SourceLocation location; @@ -1448,7 +1443,10 @@ fold_tuple_of_constants(basicblock *bb, int i, PyObject *consts, PyObject *const int seq_size = instr->i_oparg; PyObject *newconst; RETURN_IF_ERROR(get_constant_sequence(bb, i-1, seq_size, consts, &newconst)); - RETURN_SUCCESS_IF_NULL(newconst); + if (newconst == NULL) { + /* not a const sequence */ + return SUCCESS; + } assert(PyTuple_Size(newconst) == seq_size); RETURN_IF_ERROR(instr_make_load_const(instr, newconst, consts, const_cache)); nop_out(bb, i-1, seq_size); @@ -1533,7 +1531,7 @@ const_folding_check_complexity(PyObject *obj, Py_ssize_t limit) for (i = 0; limit >= 0 && i < PyTuple_GET_SIZE(obj); i++) { limit = const_folding_check_complexity(PyTuple_GET_ITEM(obj, i), limit); if (limit < 0) { - break; + return limit; } } } @@ -1697,14 +1695,19 @@ eval_const_binop(PyObject *left, int op, PyObject *right) } static int -optimize_if_const_binop(basicblock *bb, int i, PyObject *consts, PyObject *const_cache) +fold_const_binop(basicblock *bb, int i, PyObject *consts, PyObject *const_cache) { #define BINOP_OPERAND_COUNT 2 + assert(PyDict_CheckExact(const_cache)); + assert(PyList_CheckExact(consts)); cfg_instr *binop = &bb->b_instr[i]; assert(binop->i_opcode == BINARY_OP); PyObject *pair; RETURN_IF_ERROR(get_constant_sequence(bb, i-1, BINOP_OPERAND_COUNT, consts, &pair)); - RETURN_SUCCESS_IF_NULL(pair); + if (pair == NULL) { + /* not a const sequence */ + return SUCCESS; + } assert(PyTuple_Size(pair) == BINOP_OPERAND_COUNT); PyObject *left = PyTuple_GET_ITEM(pair, 0); PyObject *right = PyTuple_GET_ITEM(pair, 1); @@ -1722,21 +1725,18 @@ optimize_if_const_binop(basicblock *bb, int i, PyObject *consts, PyObject *const return SUCCESS; } -static PyObject* -const_folding_unary_not(PyObject *v) -{ - int r = PyObject_IsTrue(v); - if (r < 0) - return NULL; - return PyBool_FromLong(!r); -} - static PyObject * -eval_const_unaryop(PyObject *operand, int op) +eval_const_unaryop(PyObject *operand, int opcode, int oparg) { assert(operand != NULL); + assert( + opcode == UNARY_NEGATIVE || + opcode == UNARY_INVERT || + opcode == UNARY_NOT || + (opcode == CALL_INTRINSIC_1 && oparg == INTRINSIC_UNARY_POSITIVE) + ); PyObject *result; - switch (op) { + switch (opcode) { case UNARY_NEGATIVE: result = PyNumber_Negative(operand); break; @@ -1744,7 +1744,18 @@ eval_const_unaryop(PyObject *operand, int op) result = PyNumber_Invert(operand); break; case UNARY_NOT: - result = const_folding_unary_not(operand); + { + int r = PyObject_IsTrue(operand); + if (r < 0) + return NULL; + result = PyBool_FromLong(!r); + break; + } + case CALL_INTRINSIC_1: + if (oparg != INTRINSIC_UNARY_POSITIVE) { + Py_UNREACHABLE(); + } + result = PyNumber_Positive(operand); break; default: Py_UNREACHABLE(); @@ -1753,24 +1764,21 @@ eval_const_unaryop(PyObject *operand, int op) } static int -optimize_if_const_unaryop(basicblock *bb, int i, - PyObject *consts, PyObject *const_cache) +fold_const_unaryop(basicblock *bb, int i, PyObject *consts, PyObject *const_cache) { #define UNARYOP_OPERAND_COUNT 1 + assert(PyDict_CheckExact(const_cache)); + assert(PyList_CheckExact(consts)); cfg_instr *instr = &bb->b_instr[i]; - assert( - instr->i_opcode == UNARY_NEGATIVE - || instr->i_opcode == UNARY_INVERT - || instr->i_opcode == UNARY_NOT - || (instr->i_opcode == CALL_INTRINSIC_1 && instr->i_oparg == INTRINSIC_UNARY_POSITIVE) - ); PyObject *seq; RETURN_IF_ERROR(get_constant_sequence(bb, i-1, UNARYOP_OPERAND_COUNT, consts, &seq)); - RETURN_SUCCESS_IF_NULL(seq); + if (seq == NULL) { + /* not a const sequence */ + return SUCCESS; + } assert(PyTuple_Size(seq) == UNARYOP_OPERAND_COUNT); PyObject *operand = PyTuple_GET_ITEM(seq, 0); - bool unarypos = instr->i_opcode == CALL_INTRINSIC_1; - PyObject *newconst = unarypos ? PyNumber_Positive(operand) : eval_const_unaryop(operand, instr->i_opcode); + PyObject *newconst = eval_const_unaryop(operand, instr->i_opcode, instr->i_oparg); Py_DECREF(seq); if (newconst == NULL) { if (PyErr_ExceptionMatches(PyExc_KeyboardInterrupt)) { @@ -2287,7 +2295,7 @@ optimize_basic_block(PyObject *const_cache, basicblock *bb, PyObject *consts) _Py_FALLTHROUGH; case UNARY_INVERT: case UNARY_NEGATIVE: - RETURN_IF_ERROR(optimize_if_const_unaryop(bb, i, consts, const_cache)); + RETURN_IF_ERROR(fold_const_unaryop(bb, i, consts, const_cache)); break; case CALL_INTRINSIC_1: // for _ in (*foo, *bar) -> for _ in [*foo, *bar] @@ -2295,11 +2303,11 @@ optimize_basic_block(PyObject *const_cache, basicblock *bb, PyObject *consts) INSTR_SET_OP0(inst, NOP); } else if (oparg == INTRINSIC_UNARY_POSITIVE) { - RETURN_IF_ERROR(optimize_if_const_unaryop(bb, i, consts, const_cache)); + RETURN_IF_ERROR(fold_const_unaryop(bb, i, consts, const_cache)); } break; case BINARY_OP: - RETURN_IF_ERROR(optimize_if_const_binop(bb, i, consts, const_cache)); + RETURN_IF_ERROR(fold_const_binop(bb, i, consts, const_cache)); break; } } From 2dba23f6c843ef750f252745e2310517562607de Mon Sep 17 00:00:00 2001 From: Yan Yanchii Date: Fri, 21 Feb 2025 08:57:08 +0100 Subject: [PATCH 34/34] minor adjustments --- Python/ast_opt.c | 6 ++++-- Python/flowgraph.c | 19 ++++++++----------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/Python/ast_opt.c b/Python/ast_opt.c index 66596cfcb3e3c7..1db9980d4fcd88 100644 --- a/Python/ast_opt.c +++ b/Python/ast_opt.c @@ -734,7 +734,8 @@ fold_const_match_patterns(expr_ty node, PyArena *ctx_, _PyASTOptimizeState *stat case UnaryOp_kind: { if (node->v.UnaryOp.op == USub && - node->v.UnaryOp.operand->kind == Constant_kind) { + node->v.UnaryOp.operand->kind == Constant_kind) + { PyObject *operand = node->v.UnaryOp.operand->v.Constant.value; PyObject *folded = PyNumber_Negative(operand); return make_const(node, folded, ctx_); @@ -745,7 +746,8 @@ fold_const_match_patterns(expr_ty node, PyArena *ctx_, _PyASTOptimizeState *stat { operator_ty op = node->v.BinOp.op; if ((op == Add || op == Sub) && - node->v.BinOp.right->kind == Constant_kind) { + node->v.BinOp.right->kind == Constant_kind) + { CALL(fold_const_match_patterns, expr_ty, node->v.BinOp.left); if (node->v.BinOp.left->kind == Constant_kind) { PyObject *left = node->v.BinOp.left->v.Constant.value; diff --git a/Python/flowgraph.c b/Python/flowgraph.c index 615d69a0b4c941..c5bdf105545459 100644 --- a/Python/flowgraph.c +++ b/Python/flowgraph.c @@ -1515,13 +1515,10 @@ optimize_lists_and_sets(basicblock *bb, int i, int nextop, return SUCCESS; } -/* Check whether a collection doesn't contain too many items (including - subcollections). This protects from creating a constant that needs - too much time for calculating a hash. - "limit" is the maximal number of items. - Returns the negative number if the total number of items exceeds the - limit. Otherwise returns the limit minus the total number of items. -*/ +/* Check whether the total number of items in the (possibly nested) collection obj exceeds + * limit. Return a negative number if it does, and a non-negative number otherwise. + * Used to avoid creating constants which are slow to hash. + */ static Py_ssize_t const_folding_check_complexity(PyObject *obj, Py_ssize_t limit) { @@ -1743,11 +1740,11 @@ eval_const_unaryop(PyObject *operand, int opcode, int oparg) case UNARY_INVERT: result = PyNumber_Invert(operand); break; - case UNARY_NOT: - { + case UNARY_NOT: { int r = PyObject_IsTrue(operand); - if (r < 0) + if (r < 0) { return NULL; + } result = PyBool_FromLong(!r); break; } @@ -1773,7 +1770,7 @@ fold_const_unaryop(basicblock *bb, int i, PyObject *consts, PyObject *const_cach PyObject *seq; RETURN_IF_ERROR(get_constant_sequence(bb, i-1, UNARYOP_OPERAND_COUNT, consts, &seq)); if (seq == NULL) { - /* not a const sequence */ + /* not a const */ return SUCCESS; } assert(PyTuple_Size(seq) == UNARYOP_OPERAND_COUNT);