diff --git a/Lib/test/test_peepholer.py b/Lib/test/test_peepholer.py index 28748009f731bc..7056e3dd598fc0 100644 --- a/Lib/test/test_peepholer.py +++ b/Lib/test/test_peepholer.py @@ -277,6 +277,49 @@ def test_constant_folding_small_int(self): self.assertNotInBytecode(code, 'LOAD_SMALL_INT') self.check_lnotab(code) + def test_constant_folding_fstring(self): + # An f-string whose fields are all constants folds to a + # single str constant. + folded = [ + ("f'{1}{2}'", '12'), + ("f'{1 + 2}'", '3'), + ("f'{1.5}'", '1.5'), + ("f'{True}'", 'True'), + ("f'-{1}-'", '-1-'), + ('f\'{"ab"}cd\'', 'abcd'), + ('f\'{""}{""}\'', ''), + ] + for expr, value in folded: + with self.subTest(expr=expr): + code = compile(expr, '', 'eval') + self.assertNotInBytecode(code, 'FORMAT_SIMPLE') + self.assertNotInBytecode(code, 'BUILD_STRING') + self.assertEqual(eval(code), value) + self.check_lnotab(code) + + # Conversions and format specs can reach user code, so those + # fields are not folded. + for expr, opname, value in [ + ("f'{1!r}'", 'CONVERT_VALUE', '1'), + ("f'{1:>3}'", 'FORMAT_WITH_SPEC', ' 1'), + ]: + with self.subTest(expr=expr): + code = compile(expr, '', 'eval') + self.assertInBytecode(code, opname) + self.assertEqual(eval(code), value) + self.check_lnotab(code) + + # Constant fields of a partly-constant f-string still fold. + def f(x): + return f'{"prefix"}{x}' + + format_count = sum(instr.opname == 'FORMAT_SIMPLE' + for instr in dis.get_instructions(f)) + self.assertEqual(format_count, 1) + self.assertInBytecode(f, 'BUILD_STRING', 2) + self.assertEqual(f('!'), 'prefix!') + self.check_lnotab(f) + def test_constant_folding_unaryop(self): intrinsic_positive = 5 tests = [ diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-07-30-02-59-33.gh-issue-154910.zjPbHIFOgO.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-07-30-02-59-33.gh-issue-154910.zjPbHIFOgO.rst new file mode 100644 index 00000000000000..88f1667ab1df74 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-07-30-02-59-33.gh-issue-154910.zjPbHIFOgO.rst @@ -0,0 +1,4 @@ +The bytecode compiler now constant-folds f-string fields that are +constants of exact type :class:`str`, :class:`int`, :class:`float` or +:class:`bool`, so f-strings built entirely from such fields become a +single string constant. diff --git a/Python/flowgraph.c b/Python/flowgraph.c index f135f243c74e2f..4066c806913b92 100644 --- a/Python/flowgraph.c +++ b/Python/flowgraph.c @@ -1658,6 +1658,125 @@ fold_constant_intrinsic_list_to_tuple(basicblock *bb, int i, return SUCCESS; } +/* Replace LOAD_CONST c, FORMAT_SIMPLE + with LOAD_CONST format(c) + where c is a constant of exact type str, int, float or bool. + Formatting these types calls no user code, so the result is known at + compile time. This mirrors the fast path of the FORMAT_SIMPLE + instruction: formatting an exact str is the identity, so in that case + the instruction is simply removed. +*/ +static int +fold_format_simple(basicblock *bb, int i, PyObject *consts, + PyObject *const_cache, _Py_hashtable_t *consts_index) +{ + /* Pre-conditions */ + assert(PyDict_CheckExact(const_cache)); + assert(PyList_CheckExact(consts)); + + cfg_instr *instr = &bb->b_instr[i]; + assert(instr->i_opcode == FORMAT_SIMPLE); + + cfg_instr *load; + if (!get_const_loading_instrs(bb, i-1, &load, 1)) { + /* not a constant operand */ + return SUCCESS; + } + + PyObject *value = get_const_value(load->i_opcode, load->i_oparg, consts); + if (value == NULL) { + return ERROR; + } + if (PyUnicode_CheckExact(value)) { + /* format(value) is value itself: keep the load, drop the format. */ + Py_DECREF(value); + nop_out(&instr, 1); + return SUCCESS; + } + if (!PyLong_CheckExact(value) && !PyFloat_CheckExact(value) && + !PyBool_Check(value)) + { + /* Formatting anything else could call user code. */ + Py_DECREF(value); + return SUCCESS; + } + PyObject *formatted = PyObject_Format(value, NULL); + Py_DECREF(value); + if (formatted == NULL) { + return ERROR; + } + nop_out(&load, 1); + return instr_make_load_const(instr, formatted, consts, const_cache, + consts_index); +} + +/* Replace LOAD_CONST s1, LOAD_CONST s2, ..., LOAD_CONST sN, BUILD_STRING N + with LOAD_CONST s1s2...sN + All pieces must be exact str constants; constant f-string fields have + already been reduced to that shape by fold_format_simple. +*/ +static int +fold_build_string_of_constants(basicblock *bb, int i, PyObject *consts, + PyObject *const_cache, + _Py_hashtable_t *consts_index) +{ + /* Pre-conditions */ + assert(PyDict_CheckExact(const_cache)); + assert(PyList_CheckExact(consts)); + + cfg_instr *instr = &bb->b_instr[i]; + assert(instr->i_opcode == BUILD_STRING); + + int seq_size = instr->i_oparg; + if (seq_size < 1 || seq_size > _PY_STACK_USE_GUIDELINE) { + return SUCCESS; + } + + cfg_instr *const_instrs[_PY_STACK_USE_GUIDELINE]; + if (!get_const_loading_instrs(bb, i-1, const_instrs, seq_size)) { + /* not a const sequence */ + return SUCCESS; + } + + PyObject *pieces = PyTuple_New((Py_ssize_t)seq_size); + if (pieces == NULL) { + return ERROR; + } + for (int pos = 0; pos < seq_size; pos++) { + cfg_instr *inst = const_instrs[pos]; + assert(loads_const(inst->i_opcode)); + PyObject *piece = get_const_value(inst->i_opcode, inst->i_oparg, + consts); + if (piece == NULL) { + Py_DECREF(pieces); + return ERROR; + } + PyTuple_SET_ITEM(pieces, pos, piece); + if (!PyUnicode_CheckExact(piece)) { + /* BUILD_STRING operands are always str when the sequence + comes from an f-string; don't fold anything else. */ + Py_DECREF(pieces); + return SUCCESS; + } + } + + PyObject *empty = PyUnicode_FromStringAndSize(NULL, 0); + if (empty == NULL) { + Py_DECREF(pieces); + return ERROR; + } + PyObject *joined = PyUnicode_Join(empty, pieces); + Py_DECREF(empty); + Py_DECREF(pieces); + if (joined == NULL) { + return ERROR; + } + + nop_out(const_instrs, seq_size); + return instr_make_load_const(instr, joined, consts, const_cache, + consts_index); +} + #define MIN_CONST_SEQUENCE_SIZE 3 /* Optimize lists and sets for: @@ -2442,6 +2561,12 @@ optimize_basic_block(PyObject *const_cache, basicblock *bb, PyObject *consts, case BUILD_SET: RETURN_IF_ERROR(optimize_lists_and_sets(bb, i, nextop, consts, const_cache, consts_index)); break; + case FORMAT_SIMPLE: + RETURN_IF_ERROR(fold_format_simple(bb, i, consts, const_cache, consts_index)); + break; + case BUILD_STRING: + RETURN_IF_ERROR(fold_build_string_of_constants(bb, i, consts, const_cache, consts_index)); + break; case POP_JUMP_IF_NOT_NONE: case POP_JUMP_IF_NONE: switch (target->i_opcode) {