Skip to content

Commit dddc133

Browse files
committed
gh-153569: streamline tokenizer hot paths
1 parent b73a1a6 commit dddc133

20 files changed

Lines changed: 310 additions & 283 deletions

File tree

Lib/test/test_fstring.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1303,6 +1303,13 @@ def test_nested_fstrings(self):
13031303
self.assertEqual(f'{f"{0}"*3}', '000')
13041304
self.assertEqual(f'{f"{y}"*3}', '555')
13051305

1306+
def test_deeply_nested_fstrings_with_leading_text(self):
1307+
source = "0"
1308+
for depth in range(20):
1309+
quote = '"' if depth % 2 == 0 else "'"
1310+
source = f"f{quote}x{{{source}}}{quote}"
1311+
self.assertEqual(eval(source), "x" * 20 + "0")
1312+
13061313
def test_invalid_string_prefixes(self):
13071314
single_quote_cases = ["fu''",
13081315
"uf''",

Lib/test/test_syntax.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3106,7 +3106,7 @@ def test_tokenizer_eof_error_offsets_after_non_ascii(self):
31063106
end_offset=-1,
31073107
)
31083108

3109-
def test_error_line_excludes_old_newline(self):
3109+
def test_fstring_error_text_excludes_previous_newline(self):
31103110
cases = [
31113111
('f"{a:{\n0\n\'\'=(\n="0("', (2, 1, "0", 3, 2)),
31123112
("f'{x!=[\n]a\n}(]==!", (2, 3, "]a", 2, 2)),
@@ -3115,7 +3115,14 @@ def test_error_line_excludes_old_newline(self):
31153115
with self.subTest(source=source):
31163116
with self.assertRaises(SyntaxError) as caught:
31173117
compile(source, "<testcase>", "exec")
3118-
self.assertEqual(caught.exception.args[1][1:], expected)
3118+
actual = (
3119+
caught.exception.lineno,
3120+
caught.exception.offset,
3121+
caught.exception.text,
3122+
caught.exception.end_lineno,
3123+
caught.exception.end_offset,
3124+
)
3125+
self.assertEqual(actual, expected)
31193126

31203127
def test_assign_call(self):
31213128
self._check_error("f() = 1", "assign")

Modules/_testinternalcapi/tokenizer.c

Lines changed: 7 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ same_cursor(const _PyTok_Cursor *left, const _PyTok_Cursor *right)
3030
left->pos == right->pos &&
3131
left->line_start == right->line_start &&
3232
left->line_end == right->line_end &&
33-
left->lineno == right->lineno;
33+
left->lineno == right->lineno &&
34+
left->implicit_newline == right->implicit_newline;
3435
}
3536

3637
static PyObject *
@@ -96,19 +97,16 @@ test_tokenizer_source(PyObject *Py_UNUSED(module),
9697
check(line.start == marker_start &&
9798
line.end == marker_start + 7,
9899
"wrong late source line") < 0 ||
99-
check(!line.implicit_newline && !line.contains_nul,
100+
check(!line.implicit_newline,
100101
"wrong late source flags") < 0 ||
101102
check(_PyTok_SourceLine(&source, 2, &line) == 0,
102103
"cannot find second source line") < 0 ||
103104
check(line.start == 6 && line.end == 9 &&
104-
line.implicit_newline && !line.contains_nul,
105+
line.implicit_newline,
105106
"wrong second source line") < 0 ||
106107
check(!_PyTok_SourceLineIsImplicit(&source, 1) &&
107108
_PyTok_SourceLineIsImplicit(&source, 2),
108109
"wrong early implicit newline flags") < 0 ||
109-
check(_PyTok_SourceLine(&source, 3, &line) == 0,
110-
"cannot find third source line") < 0 ||
111-
check(line.contains_nul, "missing null byte flag") < 0 ||
112110
check(_PyTok_SourceLine(&source, final_line, &line) == 0,
113111
"cannot find final source line") < 0 ||
114112
check(line.implicit_newline &&
@@ -234,27 +232,19 @@ test_tokenizer_cursor(PyObject *Py_UNUSED(module),
234232
"wrong cursor peek after relocation") < 0 ||
235233
check(_PyTok_CursorPeek(&cursor, 1) == 'b',
236234
"wrong distant cursor peek") < 0 ||
237-
check(_PyTok_CursorAdvance(&cursor) == 'a',
238-
"wrong first cursor byte") < 0 ||
239-
check(_PyTok_CursorAdvance(&cursor) == 'b',
240-
"wrong second cursor byte") < 0 ||
241-
check(_PyTok_CursorAdvance(&cursor) == '\n',
242-
"wrong final cursor byte") < 0 ||
243-
check(_PyTok_CursorAdvance(&cursor) == EOF,
244-
"cursor advanced past line") < 0 ||
245235
check(_PyTok_CursorSetOffset(&cursor, 2) == 0,
246236
"cannot seek cursor offset") < 0 ||
247-
check(_PyTok_CursorAdvance(&cursor) == '\n',
237+
check(_PyTok_CursorPeek(&cursor, 0) == '\n',
248238
"wrong cursor byte after seek") < 0 ||
249239
check(_PyTok_CursorSetOffset(&cursor, 3) == 0,
250240
"cannot seek line boundary") < 0 ||
251241
check(cursor.lineno == 2 && cursor.line_start == 3 &&
252-
_PyTok_CursorAdvance(&cursor) == 'c',
242+
_PyTok_CursorPeek(&cursor, 0) == 'c',
253243
"wrong cursor at line boundary") < 0 ||
254244
check(_PyTok_CursorSetLine(&cursor, 3) == 0,
255245
"cannot advance cursor to final line") < 0 ||
256246
check(cursor.line_start == 6 &&
257-
_PyTok_CursorAdvance(&cursor) == 'z',
247+
_PyTok_CursorPeek(&cursor, 0) == 'z',
258248
"wrong cursor byte on final line") < 0) {
259249
goto error;
260250
}
@@ -277,25 +267,6 @@ test_tokenizer_cursor(PyObject *Py_UNUSED(module),
277267
goto error;
278268
}
279269

280-
#if SIZEOF_VOID_P > 4
281-
char byte = 0;
282-
_PyTok_SourceText huge_source = {
283-
.bytes = &byte,
284-
.len = (_PyTok_Off)INT_MAX + 1,
285-
};
286-
_PyTok_Cursor huge_cursor = {
287-
.source = &huge_source,
288-
.pos = INT_MAX,
289-
.line_end = (_PyTok_Off)INT_MAX + 1,
290-
.lineno = 1,
291-
};
292-
if (check(_PyTok_CursorAdvance(&huge_cursor) == EOF &&
293-
huge_cursor.pos == INT_MAX,
294-
"cursor advanced past maximum column") < 0) {
295-
goto error;
296-
}
297-
#endif
298-
299270
_PyTok_SourceClear(&source);
300271
Py_RETURN_NONE;
301272

Parser/lexer/lexer.c

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,7 @@ emit_type_comment(struct _PyTokenizer *tok, _PyTok_Token *token, int type,
2121
token->span = start < 0 || end < start
2222
? _PyTok_InvalidSpan()
2323
: _PyTok_SpanFromBounds(start, end);
24-
token->flags = _PyTok_SourceLineIsImplicit(
25-
&tok->source, tok->cursor.lineno) ? _PYTOK_IMPLICIT_NL : 0;
24+
token->flags = tok->cursor.implicit_newline ? _PYTOK_IMPLICIT_NL : 0;
2625
return type;
2726
}
2827

@@ -55,6 +54,47 @@ source_column(struct _PyTokenizer *tok, _PyTok_Off offset)
5554
/* Spaces in the prefix match any sequence of spaces and tabs. */
5655
static const char type_comment_prefix[] = "# type: ";
5756

57+
int
58+
_PyTok_EmitTokenWithLocationEnd(struct _PyTokenizer *tok,
59+
_PyTok_Token *token, int type,
60+
_PyTok_Off start, _PyTok_Off end,
61+
_PyTok_Off location_end)
62+
{
63+
assert((start < 0 && end < 0) || (start >= 0 && end >= start));
64+
assert(start < 0 || location_end >= end);
65+
if (start < 0 && type == ENDMARKER) {
66+
start = end = location_end = tok->cursor.pos;
67+
}
68+
token->type = type;
69+
token->level = tok->level;
70+
token->span = start < 0
71+
? _PyTok_InvalidSpan()
72+
: _PyTok_SpanFromBounds(start, end);
73+
token->start = (_PyTok_Loc){tok->cursor.lineno, -1};
74+
token->end = token->start;
75+
if (start >= 0) {
76+
if (_PyTok_CursorLocation(
77+
&tok->cursor, start, _PYTOK_AFFINITY_RIGHT,
78+
&token->start) < 0) {
79+
return record_location_error(tok);
80+
}
81+
_PyTok_Affinity end_affinity =
82+
type == FSTRING_MIDDLE || type == TSTRING_MIDDLE ||
83+
start == location_end
84+
? _PYTOK_AFFINITY_RIGHT
85+
: _PYTOK_AFFINITY_LEFT;
86+
if (_PyTok_CursorLocation(
87+
&tok->cursor, location_end, end_affinity,
88+
&token->end) < 0) {
89+
return record_location_error(tok);
90+
}
91+
}
92+
token->flags =
93+
(tok->cursor.implicit_newline ? _PYTOK_IMPLICIT_NL : 0) |
94+
(location_end != end ? _PYTOK_TEXT_END_DIFFERS : 0);
95+
return type;
96+
}
97+
5898
/* Verify that the identifier follows PEP 3131. */
5999
static int
60100
verify_identifier(struct _PyTokenizer *tok)

Parser/lexer/lexer_internal.h

Lines changed: 33 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,17 @@ static inline int
2121
_PyTok_LexerAdvance(struct _PyTokenizer *tok)
2222
{
2323
for (;;) {
24-
if (tok->cursor.pos < tok->cursor.line_end &&
25-
tok->cursor.pos - tok->cursor.line_start >= INT_MAX) {
26-
_PyTok_RecordCurrentError(
27-
tok, _PYTOK_ERR_COLUMN_OVERFLOW, NULL);
28-
return EOF;
29-
}
30-
int c = _PyTok_CursorAdvance(&tok->cursor);
31-
if (c != EOF) {
32-
return c;
24+
assert(tok->cursor.source != NULL);
25+
assert(tok->cursor.pos >= tok->cursor.line_start);
26+
assert(tok->cursor.pos <= tok->cursor.line_end);
27+
assert(tok->cursor.line_end <= tok->source.len);
28+
if (tok->cursor.pos < tok->cursor.line_end) {
29+
if (tok->cursor.pos - tok->cursor.line_start >= INT_MAX) {
30+
_PyTok_RecordCurrentError(
31+
tok, _PYTOK_ERR_COLUMN_OVERFLOW, NULL);
32+
return EOF;
33+
}
34+
return Py_CHARMASK(tok->source.bytes[tok->cursor.pos++]);
3335
}
3436
if (_PyTok_HasError(tok) || !_PyTok_ReaderUnderflow(tok)) {
3537
return EOF;
@@ -52,6 +54,10 @@ _PyTok_LexerMark(const struct _PyTokenizer *tok)
5254
static inline void
5355
_PyTok_LexerReset(struct _PyTokenizer *tok, _PyTok_Off offset)
5456
{
57+
if (offset >= tok->cursor.line_start && offset < tok->cursor.line_end) {
58+
tok->cursor.pos = offset;
59+
return;
60+
}
5561
int result = _PyTok_CursorSetOffset(&tok->cursor, offset);
5662
if (result < 0) {
5763
_PyTok_RecordPending(tok, _PYTOK_ERR_PROPAGATE);
@@ -63,6 +69,7 @@ static inline _PyTok_Frame *
6369
_PyTok_CurrentFrame(struct _PyTokenizer *tok)
6470
{
6571
assert(tok->frame_index >= 0);
72+
assert(tok->frame_index < tok->frame_capacity);
6673
assert(tok->frame_index < _PYTOK_MAX_FRAMES);
6774
return &tok->frames[tok->frame_index];
6875
}
@@ -91,50 +98,31 @@ _PyTok_LexerLocation(struct _PyTokenizer *tok, _PyTok_Off offset)
9198
return loc;
9299
}
93100

94-
static inline int
101+
int
95102
_PyTok_EmitTokenWithLocationEnd(struct _PyTokenizer *tok,
96103
_PyTok_Token *token, int type,
97104
_PyTok_Off start, _PyTok_Off end,
98-
_PyTok_Off location_end)
99-
{
100-
assert((start < 0 && end < 0) || (start >= 0 && end >= start));
101-
assert(start < 0 || location_end >= end);
102-
int synthetic = start < 0;
103-
if (synthetic && type == ENDMARKER) {
104-
start = end = location_end = tok->cursor.pos;
105-
}
106-
token->type = type;
107-
token->level = tok->level;
108-
token->span = start < 0
109-
? _PyTok_InvalidSpan()
110-
: _PyTok_SpanFromBounds(start, end);
111-
token->start = (_PyTok_Loc){tok->cursor.lineno, -1};
112-
token->end = token->start;
113-
if (start >= 0) {
114-
if (_PyTok_CursorLocation(
115-
&tok->cursor, start, _PYTOK_AFFINITY_RIGHT,
116-
&token->start) < 0) {
117-
return record_location_error(tok);
118-
}
119-
_PyTok_Affinity end_affinity =
120-
type == FSTRING_MIDDLE || type == TSTRING_MIDDLE ||
121-
start == location_end
122-
? _PYTOK_AFFINITY_RIGHT
123-
: _PYTOK_AFFINITY_LEFT;
124-
if (_PyTok_CursorLocation(
125-
&tok->cursor, location_end, end_affinity, &token->end) < 0) {
126-
return record_location_error(tok);
127-
}
128-
}
129-
token->flags = _PyTok_SourceLineIsImplicit(
130-
&tok->source, tok->cursor.lineno) ? _PYTOK_IMPLICIT_NL : 0;
131-
return type;
132-
}
105+
_PyTok_Off location_end);
133106

134107
static inline int
135108
_PyTok_EmitToken(struct _PyTokenizer *tok, _PyTok_Token *token, int type,
136109
_PyTok_Off start, _PyTok_Off end)
137110
{
111+
assert(start < 0 || end >= start);
112+
if (start >= tok->cursor.line_start &&
113+
start < tok->cursor.line_end &&
114+
end <= tok->cursor.line_end &&
115+
end - tok->cursor.line_start <= INT_MAX) {
116+
token->type = type;
117+
token->level = tok->level;
118+
token->span = _PyTok_SpanFromBounds(start, end);
119+
token->start = (_PyTok_Loc){
120+
tok->cursor.lineno, (int)(start - tok->cursor.line_start)};
121+
token->end = (_PyTok_Loc){
122+
tok->cursor.lineno, (int)(end - tok->cursor.line_start)};
123+
token->flags = tok->cursor.implicit_newline ? _PYTOK_IMPLICIT_NL : 0;
124+
return type;
125+
}
138126
return _PyTok_EmitTokenWithLocationEnd(
139127
tok, token, type, start, end, end);
140128
}

Parser/lexer/state.c

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,13 @@ _PyTok_StateNew(void)
2020
_PyTok_CursorInit(&tok->cursor, &tok->source);
2121
tok->tabsize = TABSIZE;
2222
tok->atbol = 1;
23-
tok->frames[0].body_start = -1;
24-
tok->frames[0].debug_expr_start = -1;
25-
tok->frames[0].debug_expr_end = -1;
23+
tok->frames = tok->inline_frames;
24+
tok->frame_capacity = _PYTOK_INLINE_FRAMES;
25+
tok->frames[0] = (_PyTok_Frame){
26+
.body_start = -1,
27+
.debug_expr_start = -1,
28+
.debug_expr_end = -1,
29+
};
2630
return tok;
2731
}
2832

@@ -34,5 +38,8 @@ _PyTok_StateFree(struct _PyTokenizer *tok)
3438
Py_XDECREF(tok->module);
3539
_PyTok_ReaderFree(tok);
3640
_PyTok_SourceClear(&tok->source);
41+
if (tok->frames != tok->inline_frames) {
42+
PyMem_Free(tok->frames);
43+
}
3744
PyMem_Free(tok);
3845
}

Parser/lexer/state.h

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#define MAXLEVEL 200 /* Max parentheses level */
1212
#define MAXFSTRINGLEVEL 150 /* Max f-string nesting level */
1313
#define _PYTOK_MAX_FRAMES (MAXFSTRINGLEVEL * 3)
14+
#define _PYTOK_INLINE_FRAMES 4
1415

1516
#define INSIDE_FSTRING(tok) (tok->frame_index > 0)
1617
#define INSIDE_FSTRING_EXPR(mode) ((mode)->kind == _PYTOK_FRAME_FSTRING_EXPR)
@@ -71,14 +72,22 @@ struct _PyTokenizer {
7172

7273
int type_comments; /* Whether to look for type comments */
7374

74-
_PyTok_Frame frames[_PYTOK_MAX_FRAMES];
75+
_PyTok_Frame *frames;
76+
_PyTok_Frame inline_frames[_PYTOK_INLINE_FRAMES];
77+
int frame_capacity;
7578
int frame_index;
7679
int extra_tokens;
7780
int comment_newline;
7881
int is_interactive;
7982
int is_prompting;
8083
};
8184

85+
static inline int
86+
_PyTok_HasError(const struct _PyTokenizer *tok)
87+
{
88+
return tok->error.kind != _PYTOK_ERR_NONE;
89+
}
90+
8291
struct _PyTokenizer *_PyTok_StateNew(void);
8392
void _PyTok_StateFree(struct _PyTokenizer *);
8493

0 commit comments

Comments
 (0)