Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions Lib/test/test_peepholer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
125 changes: 125 additions & 0 deletions Python/flowgraph.c
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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) {
Expand Down
Loading