Skip to content

Commit 1145c05

Browse files
miss-islingtonpablogsalgwosti
authored
[3.14] gh-155525: Fix quadratic f-string tokenization (GH-156756) (#157829)
* [ 3.14 ] gh-155525: Avoid quadratic f-string tokenization Adapt the original expression-span fix to the release tokenizer and retain interactive buffers while a formatted string is open. Co-authored-by: gwosti <322952417+gwosti@users.noreply.github.com> * gh-155525: Cover quadratic f-string tokenization regression (GH-156756) * gh-155525: Avoid quadratic f-string tokenization * fixup! gh-155525: Avoid quadratic f-string tokenization --------- (cherry picked from commit c1df684) Co-authored-by: gwosti <322952417+gwosti@users.noreply.github.com> Co-authored-by: Pablo Galindo Salgado <Pablogsal@gmail.com> --------- Co-authored-by: Pablo Galindo Salgado <Pablogsal@gmail.com> Co-authored-by: gwosti <322952417+gwosti@users.noreply.github.com>
1 parent 65204b7 commit 1145c05

10 files changed

Lines changed: 127 additions & 120 deletions

File tree

‎Lib/test/test_fstring.py‎

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,13 @@
1414
import re
1515
import types
1616
import decimal
17+
import subprocess
1718
import unittest
1819
import warnings
1920
from test import support
2021
from test.support.os_helper import temp_cwd
21-
from test.support.script_helper import assert_python_failure, assert_python_ok
22+
from test.support.script_helper import (
23+
assert_python_failure, assert_python_ok, spawn_python)
2224

2325
a_global = 'global variable'
2426

@@ -822,6 +824,18 @@ def build_fstr(n, extra=''):
822824
s = "f'{1}' 'x' 'y'" * 1024
823825
self.assertEqual(eval(s), '1xy' * 1024)
824826

827+
@support.requires_resource('cpu')
828+
def test_many_fstrings_in_module(self):
829+
fields = ''.join(f'{{x{i}}}' for i in range(100))
830+
source = ''.join(
831+
f"value_{i} = f'{fields}'\n" for i in range(1_000)
832+
)
833+
namespace = {f'x{i}': str(i) for i in range(100)}
834+
expected = ''.join(str(i) for i in range(100))
835+
exec(source, namespace)
836+
self.assertEqual(namespace['value_0'], expected)
837+
self.assertEqual(namespace['value_999'], expected)
838+
825839
def test_format_specifier_expressions(self):
826840
width = 10
827841
precision = 4
@@ -1338,6 +1352,9 @@ def test_not_equal(self):
13381352
self.assertEqual(f'{3!=4:}', 'True')
13391353
self.assertEqual(f'{3!=4!s}', 'True')
13401354
self.assertEqual(f'{3!=4!s:.3}', 'Tru')
1355+
a = 3
1356+
b = 4
1357+
self.assertEqual(f'{a!=b=:>10}', 'a!=b= 1')
13411358

13421359
def test_equal_equal(self):
13431360
# Because an expression ending in = has special meaning,
@@ -1789,6 +1806,32 @@ def test_debug_in_file(self):
17891806
self.assertEqual(stdout.decode('utf-8').strip().replace('\r\n', '\n').replace('\r', '\n'),
17901807
"3\n=3")
17911808

1809+
@support.requires_subprocess()
1810+
def test_expression_in_interactive_after_buffer_resize(self):
1811+
expression = "(\n" + (" " * 64 + "\n") * 256 + "1\n)"
1812+
source = (
1813+
f"result = f'''{{{expression}=}}'''\n"
1814+
"print(repr(result))\n"
1815+
)
1816+
with spawn_python('-i', '-q', stderr=subprocess.PIPE) as process:
1817+
stdout, stderr = process.communicate(
1818+
source.encode(), timeout=support.SHORT_TIMEOUT)
1819+
self.assertEqual(process.returncode, 0, stderr)
1820+
self.assertEqual(stdout.decode().strip(), repr(expression + "=1"))
1821+
1822+
def test_debug_in_file_after_buffer_resize(self):
1823+
expression = "(\n" + (" " * 64 + "\n") * 256 + "1\n)"
1824+
expected = expression + "=1"
1825+
with temp_cwd():
1826+
script = 'script.py'
1827+
source = (
1828+
f"result = f'''{{{expression}=}}'''\n"
1829+
f"assert result == {expected!r}\n"
1830+
)
1831+
with open(script, 'w') as f:
1832+
f.write(source)
1833+
assert_python_ok(script)
1834+
17921835
def test_syntax_warning_infinite_recursion_in_file(self):
17931836
with temp_cwd():
17941837
script = 'script.py'

‎Lib/test/test_tstring.py‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1+
import subprocess
12
import unittest
23

4+
from test import support
5+
from test.support.os_helper import temp_cwd
6+
from test.support.script_helper import assert_python_ok, spawn_python
37
from test.test_string._support import TStringBaseCase, fstring
48

59

@@ -79,6 +83,44 @@ def upper(self):
7983
)
8084
self.assertEqual(fstring(t), "Name: Bob, Age: 30")
8185

86+
@support.requires_subprocess()
87+
def test_expression_in_interactive_after_buffer_resize(self):
88+
expression = "(\n" + (" " * 64 + "\n") * 256 + "1\n)"
89+
source = (
90+
f"result = t'''{{{expression}}}'''\n"
91+
"print(repr(result.interpolations[0].expression))\n"
92+
)
93+
with spawn_python('-i', '-q', stderr=subprocess.PIPE) as process:
94+
stdout, stderr = process.communicate(
95+
source.encode(), timeout=support.SHORT_TIMEOUT)
96+
self.assertEqual(process.returncode, 0, stderr)
97+
self.assertEqual(stdout.decode().strip(), repr(expression))
98+
99+
def test_interpolation_expression_in_file_after_buffer_resize(self):
100+
expression = "(\n" + (" " * 64 + "\n") * 256 + "1\n)"
101+
with temp_cwd():
102+
script = 'script.py'
103+
source = (
104+
f"template = t'''{{{expression}}}'''\n"
105+
"interpolation = template.interpolations[0]\n"
106+
f"assert interpolation.expression == {expression!r}\n"
107+
)
108+
with open(script, 'w') as f:
109+
f.write(source)
110+
assert_python_ok(script)
111+
112+
@support.requires_resource('cpu')
113+
def test_many_tstrings_in_module(self):
114+
fields = ''.join(f'{{x{i}}}' for i in range(100))
115+
source = ''.join(
116+
f"value_{i} = t'{fields}'\n" for i in range(1_000)
117+
)
118+
namespace = {f'x{i}': str(i) for i in range(100)}
119+
expected = ''.join(str(i) for i in range(100))
120+
exec(source, namespace)
121+
self.assertEqual(fstring(namespace['value_0']), expected)
122+
self.assertEqual(fstring(namespace['value_999']), expected)
123+
82124
def test_format_specifiers(self):
83125
# Test basic format specifiers
84126
value = 3.14159
@@ -88,6 +130,14 @@ def test_format_specifiers(self):
88130
)
89131
self.assertEqual(fstring(t), "Pi: 3.14")
90132

133+
a = 3
134+
b = 4
135+
t = t"{a!=b:>10}"
136+
self.assertTStringEqual(
137+
t, ("", ""), [(a != b, "a!=b", None, ">10")]
138+
)
139+
self.assertEqual(fstring(t), " 1")
140+
91141
def test_conversions(self):
92142
# Test !s conversion (str)
93143
obj = object()
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix quadratic-time tokenization of modules containing many f-strings or
2+
t-strings.

‎Parser/lexer/buffer.c‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ _PyLexer_remember_fstring_buffers(struct tok_state *tok)
1515
mode = &(tok->tok_mode_stack[index]);
1616
mode->start_offset = mode->start == NULL ? -1 : mode->start - tok->buf;
1717
mode->multi_line_start_offset = mode->multi_line_start == NULL ? -1 : mode->multi_line_start - tok->buf;
18+
mode->last_expr_start_offset = mode->last_expr_start == NULL
19+
? -1 : mode->last_expr_start - tok->buf;
1820
}
1921
}
2022

@@ -29,6 +31,8 @@ _PyLexer_restore_fstring_buffers(struct tok_state *tok)
2931
mode = &(tok->tok_mode_stack[index]);
3032
mode->start = mode->start_offset < 0 ? NULL : tok->buf + mode->start_offset;
3133
mode->multi_line_start = mode->multi_line_start_offset < 0 ? NULL : tok->buf + mode->multi_line_start_offset;
34+
mode->last_expr_start = mode->last_expr_start_offset < 0
35+
? NULL : tok->buf + mode->last_expr_start_offset;
3236
}
3337
}
3438

‎Parser/lexer/lexer.c‎

Lines changed: 22 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -119,15 +119,19 @@ set_ftstring_expr(struct tok_state* tok, struct token *token, char c) {
119119
if (!(tok_mode->in_debug || tok_mode->string_kind == TSTRING) || token->metadata) {
120120
return 0;
121121
}
122+
const char *expression = tok_mode->last_expr_start;
123+
assert(expression != NULL);
124+
assert(expression <= tok->start);
125+
Py_ssize_t expression_size = tok->start - expression;
122126
PyObject *res = NULL;
123127

124128
// Look for a # character outside of string literals
125129
int hash_detected = 0;
126130
int in_string = 0;
127131
char quote_char = 0;
128132

129-
for (Py_ssize_t i = 0; i < tok_mode->last_expr_size - tok_mode->last_expr_end; i++) {
130-
char ch = tok_mode->last_expr_buffer[i];
133+
for (Py_ssize_t i = 0; i < expression_size; i++) {
134+
char ch = expression[i];
131135

132136
// Skip escaped characters
133137
if (ch == '\\') {
@@ -163,7 +167,7 @@ set_ftstring_expr(struct tok_state* tok, struct token *token, char c) {
163167
// If we found a # character in the expression, we need to handle comments
164168
if (hash_detected) {
165169
// Allocate buffer for processed result
166-
char *result = (char *)PyMem_Malloc((tok_mode->last_expr_size - tok_mode->last_expr_end + 1) * sizeof(char));
170+
char *result = (char *)PyMem_Malloc((expression_size + 1) * sizeof(char));
167171
if (!result) {
168172
return -1;
169173
}
@@ -174,16 +178,16 @@ set_ftstring_expr(struct tok_state* tok, struct token *token, char c) {
174178
quote_char = 0; // Current string quote char
175179

176180
// Process each character
177-
while (i < tok_mode->last_expr_size - tok_mode->last_expr_end) {
178-
char ch = tok_mode->last_expr_buffer[i];
181+
while (i < expression_size) {
182+
char ch = expression[i];
179183

180184
// Copy escaped characters without interpreting the escaped
181185
// character as a quote or comment marker.
182186
if (ch == '\\') {
183187
result[j++] = ch;
184188
i++;
185-
if (i < tok_mode->last_expr_size - tok_mode->last_expr_end) {
186-
result[j++] = tok_mode->last_expr_buffer[i];
189+
if (i < expression_size) {
190+
result[j++] = expression[i];
187191
}
188192
}
189193
// Handle string quotes
@@ -199,11 +203,11 @@ set_ftstring_expr(struct tok_state* tok, struct token *token, char c) {
199203
}
200204
// Skip comments
201205
else if (ch == '#' && !in_string) {
202-
while (i < tok_mode->last_expr_size - tok_mode->last_expr_end &&
203-
tok_mode->last_expr_buffer[i] != '\n') {
206+
while (i < expression_size &&
207+
expression[i] != '\n') {
204208
i++;
205209
}
206-
if (i < tok_mode->last_expr_size - tok_mode->last_expr_end) {
210+
if (i < expression_size) {
207211
result[j++] = '\n';
208212
}
209213
}
@@ -219,8 +223,8 @@ set_ftstring_expr(struct tok_state* tok, struct token *token, char c) {
219223
PyMem_Free(result);
220224
} else {
221225
res = PyUnicode_DecodeUTF8(
222-
tok_mode->last_expr_buffer,
223-
tok_mode->last_expr_size - tok_mode->last_expr_end,
226+
expression,
227+
expression_size,
224228
NULL
225229
);
226230
}
@@ -232,61 +236,6 @@ set_ftstring_expr(struct tok_state* tok, struct token *token, char c) {
232236
return 0;
233237
}
234238

235-
int
236-
_PyLexer_update_ftstring_expr(struct tok_state *tok, char cur)
237-
{
238-
assert(tok->cur != NULL);
239-
240-
Py_ssize_t size = strlen(tok->cur);
241-
tokenizer_mode *tok_mode = TOK_GET_MODE(tok);
242-
243-
switch (cur) {
244-
case 0:
245-
if (!tok_mode->last_expr_buffer || tok_mode->last_expr_end >= 0) {
246-
return 1;
247-
}
248-
char *new_buffer = PyMem_Realloc(
249-
tok_mode->last_expr_buffer,
250-
tok_mode->last_expr_size + size
251-
);
252-
if (new_buffer == NULL) {
253-
PyMem_Free(tok_mode->last_expr_buffer);
254-
goto error;
255-
}
256-
tok_mode->last_expr_buffer = new_buffer;
257-
strncpy(tok_mode->last_expr_buffer + tok_mode->last_expr_size, tok->cur, size);
258-
tok_mode->last_expr_size += size;
259-
break;
260-
case '{':
261-
if (tok_mode->last_expr_buffer != NULL) {
262-
PyMem_Free(tok_mode->last_expr_buffer);
263-
}
264-
tok_mode->last_expr_buffer = PyMem_Malloc(size);
265-
if (tok_mode->last_expr_buffer == NULL) {
266-
goto error;
267-
}
268-
tok_mode->last_expr_size = size;
269-
tok_mode->last_expr_end = -1;
270-
strncpy(tok_mode->last_expr_buffer, tok->cur, size);
271-
break;
272-
case '}':
273-
case '!':
274-
tok_mode->last_expr_end = strlen(tok->start);
275-
break;
276-
case ':':
277-
if (tok_mode->last_expr_end == -1) {
278-
tok_mode->last_expr_end = strlen(tok->start);
279-
}
280-
break;
281-
default:
282-
Py_UNREACHABLE();
283-
}
284-
return 1;
285-
error:
286-
tok->done = E_NOMEM;
287-
return 0;
288-
}
289-
290239
static int
291240
lookahead(struct tok_state *tok, const char *test)
292241
{
@@ -1103,9 +1052,8 @@ tok_get_normal_mode(struct tok_state *tok, tokenizer_mode* current_tok, struct t
11031052
the_current_tok->first_line = tok->lineno;
11041053
the_current_tok->start_offset = -1;
11051054
the_current_tok->multi_line_start_offset = -1;
1106-
the_current_tok->last_expr_buffer = NULL;
1107-
the_current_tok->last_expr_size = 0;
1108-
the_current_tok->last_expr_end = -1;
1055+
the_current_tok->last_expr_start = NULL;
1056+
the_current_tok->last_expr_start_offset = -1;
11091057
the_current_tok->in_format_spec = 0;
11101058
the_current_tok->in_debug = 0;
11111059

@@ -1270,9 +1218,6 @@ tok_get_normal_mode(struct tok_state *tok, tokenizer_mode* current_tok, struct t
12701218
int cursor_in_format_with_debug =
12711219
cursor == 1 && (current_tok->in_debug || in_format_spec);
12721220
int cursor_valid = cursor == 0 || cursor_in_format_with_debug;
1273-
if ((cursor_valid) && !_PyLexer_update_ftstring_expr(tok, c)) {
1274-
return MAKE_TOKEN(ENDMARKER);
1275-
}
12761221
if ((cursor_valid) && c != '{' && set_ftstring_expr(tok, token, c)) {
12771222
return MAKE_TOKEN(ERRORTOKEN);
12781223
}
@@ -1416,6 +1361,9 @@ tok_get_fstring_mode(struct tok_state *tok, tokenizer_mode* current_tok, struct
14161361
if (start_char == '{') {
14171362
int peek1 = tok_nextc(tok);
14181363
tok_backup(tok, peek1);
1364+
if (peek1 != '{') {
1365+
current_tok->last_expr_start = tok->cur;
1366+
}
14191367
tok_backup(tok, start_char);
14201368
if (peek1 != '{') {
14211369
current_tok->curly_bracket_expr_start_depth++;
@@ -1440,13 +1388,6 @@ tok_get_fstring_mode(struct tok_state *tok, tokenizer_mode* current_tok, struct
14401388
}
14411389
}
14421390

1443-
if (current_tok->last_expr_buffer != NULL) {
1444-
PyMem_Free(current_tok->last_expr_buffer);
1445-
current_tok->last_expr_buffer = NULL;
1446-
current_tok->last_expr_size = 0;
1447-
current_tok->last_expr_end = -1;
1448-
}
1449-
14501391
p_start = tok->start;
14511392
p_end = tok->cur;
14521393
tok->tok_mode_stack_index--;
@@ -1531,12 +1472,10 @@ tok_get_fstring_mode(struct tok_state *tok, tokenizer_mode* current_tok, struct
15311472
}
15321473

15331474
if (c == '{') {
1534-
if (!_PyLexer_update_ftstring_expr(tok, c)) {
1535-
return MAKE_TOKEN(ENDMARKER);
1536-
}
15371475
int peek = tok_nextc(tok);
15381476
if (peek != '{' || in_format_spec) {
15391477
tok_backup(tok, peek);
1478+
current_tok->last_expr_start = tok->cur;
15401479
tok_backup(tok, c);
15411480
current_tok->curly_bracket_expr_start_depth++;
15421481
if (current_tok->curly_bracket_expr_start_depth >= MAX_EXPR_NESTING) {

‎Parser/lexer/lexer.h‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33

44
#include "state.h"
55

6-
int _PyLexer_update_ftstring_expr(struct tok_state *tok, char cur);
7-
86
int _PyTokenizer_Get(struct tok_state *, struct token *);
97

108
#endif

0 commit comments

Comments
 (0)