Skip to content

Commit 09117bc

Browse files
authored
gh-153569: return token spans and unify decoded source storage (#156482)
Represent token boundaries as logical source offsets with explicit start and end locations, and adapt pegen and _tokenize to consume them. Use SourceText to own decoded input across all reader modes. Streaming readers discard consumed windows while preserving logical offsets and reusing the allocation. Prepared and interactive input retain their source. Force source relocation on growth in debug builds and add coverage for formatted-string growth, interactive input, storage reuse, and offset limits. Scanner positions remain pointers, rebased when storage moves.
1 parent 878b5e2 commit 09117bc

18 files changed

Lines changed: 453 additions & 199 deletions

File tree

Lib/test/test_capi/test_tokenizer.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ class TokenizerTests(unittest.TestCase):
99
def test_source(self):
1010
_testinternalcapi.test_tokenizer_source()
1111

12+
def test_source_discard(self):
13+
_testinternalcapi.test_tokenizer_source_discard()
14+
1215
def test_cursor(self):
1316
_testinternalcapi.test_tokenizer_cursor()
1417

Lib/test/test_repl.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,22 @@ def test_lexer_buffer_realloc_with_null_start(self):
198198
self.assertEqual(p.returncode, 0)
199199
self.assertIn(long_value, output)
200200

201+
@cpython_only
202+
def test_multiline_fstring_source_reallocation(self):
203+
long_line = " " * 9000 + "+ 2"
204+
user_input = (
205+
'value = f"""{(\n'
206+
'1\n'
207+
f'{long_line}\n'
208+
')}"""\n'
209+
'print(value)\n'
210+
)
211+
p = spawn_repl()
212+
p.stdin.write(user_input)
213+
output = kill_python(p)
214+
self.assertEqual(p.returncode, 0)
215+
self.assertIn(">>> 3\n>>> ", output)
216+
201217
def test_close_stdin(self):
202218
user_input = dedent('''
203219
import os

Lib/test/test_tokenize.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2427,6 +2427,30 @@ def test_stop_iteration_skips_encoded_readline_codec_lookup(self):
24272427
(token.ENDMARKER, "", (1, 0), (1, 0), ""),
24282428
)
24292429

2430+
def test_fstring_offsets_survive_buffer_reallocation(self):
2431+
for prefix in ("f", "t"):
2432+
for extra_tokens in (False, True):
2433+
with self.subTest(prefix=prefix, extra_tokens=extra_tokens):
2434+
physical_lines = [
2435+
prefix + '"""\n',
2436+
"{(\n",
2437+
" " * 9000 + "1\n",
2438+
")=:>{2}}\n",
2439+
'"""\n',
2440+
]
2441+
source = "".join(physical_lines)
2442+
chunks = iter([
2443+
"".join(physical_lines[:2]),
2444+
"".join(physical_lines[2:4]),
2445+
physical_lines[4],
2446+
"",
2447+
])
2448+
expected = self._get_tokens(
2449+
source, extra_tokens=extra_tokens)
2450+
tokens = list(tokenize._generate_tokens_from_c_tokenizer(
2451+
chunks.__next__, extra_tokens=extra_tokens))
2452+
self.assertEqual(tokens, expected)
2453+
24302454
def test_extra_tokens_relaxes_lexer_errors(self):
24312455
cases = [
24322456
(

Modules/_testinternalcapi/tokenizer.c

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,28 @@ test_tokenizer_source(PyObject *Py_UNUSED(module),
195195
goto error;
196196
}
197197

198+
_PyTok_SourceDiscard(&source);
199+
if (check(_PyTok_SourceAppendLine(&source, "a\n", 2, 0) == 4,
200+
"wrong retained source offset") < 0 ||
201+
_PyTok_SourceLine(&source, 1, &line) < 0 ||
202+
check(line.start == 4 && line.end == 6,
203+
"wrong retained source line") < 0 ||
204+
_PyTok_SourceLocation(
205+
&source, 4, _PYTOK_AFFINITY_LEFT, &loc) < 0 ||
206+
check(loc.lineno == 1 && loc.byte_col == 0,
207+
"wrong retained source location") < 0) {
208+
goto error;
209+
}
210+
view = _PyTok_SourceSpanView(
211+
&source, _PyTok_SpanFromBounds(4, 5), &view_len);
212+
if (check(view != NULL && view_len == 1 && view[0] == 'a',
213+
"wrong retained source span") < 0 ||
214+
check_system_error(_PyTok_SourceSpanView(
215+
&source, _PyTok_SpanFromBounds(0, 1), &view_len) == NULL,
216+
"accepted discarded source span") < 0) {
217+
goto error;
218+
}
219+
198220
_PyTok_SourceClear(&source);
199221
Py_RETURN_NONE;
200222

@@ -296,6 +318,88 @@ test_tokenizer_cursor(PyObject *Py_UNUSED(module),
296318
}
297319
#endif
298320

321+
_PyTok_Off base = source.len;
322+
_PyTok_SourceDiscard(&source);
323+
if (_PyTok_SourceAppendLine(&source, "ab\n", 3, 0) < 0 ||
324+
_PyTok_SourceAppendLine(&source, "cd", 2, 0) < 0) {
325+
goto error;
326+
}
327+
_PyTok_CursorInit(&cursor, &source);
328+
if (_PyTok_CursorSetLine(&cursor, 1) < 0 ||
329+
check(cursor.pos == base && _PyTok_CursorPeek(&cursor, 1) == 'b',
330+
"wrong retained cursor line") < 0 ||
331+
_PyTok_CursorSetLine(&cursor, 2) < 0 ||
332+
check(_PyTok_CursorAdvance(&cursor) == 'c',
333+
"wrong retained cursor byte") < 0 ||
334+
_PyTok_CursorSetOffset(&cursor, base + 5) < 0 ||
335+
check(cursor.lineno == 2 && _PyTok_CursorAdvance(&cursor) == EOF,
336+
"wrong retained cursor EOF") < 0) {
337+
goto error;
338+
}
339+
340+
_PyTok_SourceClear(&source);
341+
Py_RETURN_NONE;
342+
343+
error:
344+
_PyTok_SourceClear(&source);
345+
return NULL;
346+
}
347+
348+
static PyObject *
349+
test_tokenizer_source_discard(PyObject *Py_UNUSED(module),
350+
PyObject *Py_UNUSED(args))
351+
{
352+
_PyTok_SourceText source;
353+
_PyTok_SourceInit(&source);
354+
for (int i = 0; i < 260; i++) {
355+
if (_PyTok_SourceAppendLine(&source, "x\n", 2, 1) < 0) {
356+
goto error;
357+
}
358+
}
359+
char *bytes = source.bytes;
360+
_PyTok_Off capacity = source.cap;
361+
_PyTok_SourceDiscard(&source);
362+
if (check(source.base_offset == 520 && source.len == 0 &&
363+
source.nlines == 0 && source.bytes == bytes &&
364+
source.cap == capacity && source.bytes[0] == '\0',
365+
"discard did not preserve source allocation") < 0) {
366+
goto error;
367+
}
368+
for (int i = 0; i < 260; i++) {
369+
if (check(_PyTok_SourceAppendLine(&source, "y\n", 2, 0) == 520 + 2 * i,
370+
"wrong source offset after discard") < 0 ||
371+
check(!_PyTok_SourceLineIsImplicit(&source, i + 1),
372+
"discard preserved implicit newline flag") < 0) {
373+
goto error;
374+
}
375+
}
376+
if (check(source.bytes == bytes && source.cap == capacity,
377+
"discarded allocation was not reused") < 0) {
378+
goto error;
379+
}
380+
_PyTok_SourceDiscard(&source);
381+
if (check(_PyTok_SourceAppendLine(&source, "tail", 4, 0) == 1040,
382+
"wrong source offset after repeated discard") < 0) {
383+
goto error;
384+
}
385+
_PyTok_SourceDiscard(&source);
386+
if (check(_PyTok_SourceAppendLine(&source, "z\n", 2, 0) == 1044,
387+
"cannot append after discarding unterminated line") < 0) {
388+
goto error;
389+
}
390+
_PyTok_SourceDiscard(&source);
391+
source.base_offset = PY_SSIZE_T_MAX - 1;
392+
if (check(_PyTok_SourceAppendLine(&source, "z\n", 2, 0) < 0 &&
393+
PyErr_ExceptionMatches(PyExc_MemoryError),
394+
"accepted overflowing logical source offset") < 0) {
395+
goto error;
396+
}
397+
PyErr_Clear();
398+
if (check(source.len == 0 && source.nlines == 0 &&
399+
source.base_offset == PY_SSIZE_T_MAX - 1,
400+
"overflow changed retained source") < 0) {
401+
goto error;
402+
}
299403
_PyTok_SourceClear(&source);
300404
Py_RETURN_NONE;
301405

@@ -307,6 +411,7 @@ test_tokenizer_cursor(PyObject *Py_UNUSED(module),
307411
static PyMethodDef test_methods[] = {
308412
{"test_tokenizer_source", test_tokenizer_source, METH_NOARGS},
309413
{"test_tokenizer_cursor", test_tokenizer_cursor, METH_NOARGS},
414+
{"test_tokenizer_source_discard", test_tokenizer_source_discard, METH_NOARGS},
310415
{NULL},
311416
};
312417

Parser/lexer/buffer.c

Lines changed: 33 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,46 @@
11
#include "Python.h"
2-
#include "errcode.h"
3-
2+
#include "buffer.h"
43
#include "state.h"
54

6-
/* Traverse and remember all f-string buffers, in order to be able to restore
7-
them after reallocating tok->buf */
85
void
9-
_PyLexer_remember_fstring_buffers(struct tok_state *tok)
6+
_PyLexer_SaveBufferPointers(struct tok_state *tok, const char *base,
7+
_PyLexer_BufferPointers *pointers)
108
{
11-
int index;
12-
tokenizer_mode *mode;
13-
14-
for (index = tok->tok_mode_stack_index; index >= 0; --index) {
15-
mode = &(tok->tok_mode_stack[index]);
9+
pointers->buf_from_base = tok->buf - base;
10+
pointers->cur_from_buf = tok->cur - tok->buf;
11+
pointers->inp_from_buf = tok->inp - tok->buf;
12+
pointers->start_from_buf = tok->start == NULL
13+
? -1 : tok->start - tok->buf;
14+
pointers->line_start_from_buf = tok->line_start == NULL
15+
? -1 : tok->line_start - tok->buf;
16+
pointers->multi_line_start_from_buf = tok->multi_line_start == NULL
17+
? -1 : tok->multi_line_start - tok->buf;
18+
for (int index = tok->tok_mode_stack_index; index > 0; --index) {
19+
tokenizer_mode *mode = &tok->tok_mode_stack[index];
1620
mode->start_offset = mode->start == NULL ? -1 : mode->start - tok->buf;
17-
mode->multi_line_start_offset = mode->multi_line_start == NULL ? -1 : mode->multi_line_start - tok->buf;
21+
mode->multi_line_start_offset = mode->multi_line_start == NULL
22+
? -1 : mode->multi_line_start - tok->buf;
1823
}
1924
}
2025

21-
/* Traverse and restore all f-string buffers after reallocating tok->buf */
2226
void
23-
_PyLexer_restore_fstring_buffers(struct tok_state *tok)
24-
{
25-
int index;
26-
tokenizer_mode *mode;
27-
28-
for (index = tok->tok_mode_stack_index; index >= 0; --index) {
29-
mode = &(tok->tok_mode_stack[index]);
30-
mode->start = mode->start_offset < 0 ? NULL : tok->buf + mode->start_offset;
31-
mode->multi_line_start = mode->multi_line_start_offset < 0 ? NULL : tok->buf + mode->multi_line_start_offset;
32-
}
33-
}
34-
35-
int
36-
_PyLexer_tok_reserve_buf(struct tok_state *tok, Py_ssize_t size)
27+
_PyLexer_RestoreBufferPointers(struct tok_state *tok, char *base,
28+
const _PyLexer_BufferPointers *pointers)
3729
{
38-
Py_ssize_t cur = tok->cur - tok->buf;
39-
Py_ssize_t oldsize = tok->inp - tok->buf;
40-
Py_ssize_t newsize = oldsize + Py_MAX(size, oldsize >> 1);
41-
if (newsize > tok->end - tok->buf) {
42-
char *newbuf = tok->buf;
43-
Py_ssize_t start = tok->start == NULL ? -1 : tok->start - tok->buf;
44-
Py_ssize_t line_start = tok->start == NULL ? -1 : tok->line_start - tok->buf;
45-
Py_ssize_t multi_line_start = tok->multi_line_start - tok->buf;
46-
_PyLexer_remember_fstring_buffers(tok);
47-
newbuf = (char *)PyMem_Realloc(newbuf, newsize);
48-
if (newbuf == NULL) {
49-
tok->done = E_NOMEM;
50-
return 0;
51-
}
52-
tok->buf = newbuf;
53-
tok->cur = tok->buf + cur;
54-
tok->inp = tok->buf + oldsize;
55-
tok->end = tok->buf + newsize;
56-
tok->start = start < 0 ? NULL : tok->buf + start;
57-
tok->line_start = line_start < 0 ? NULL : tok->buf + line_start;
58-
tok->multi_line_start = multi_line_start < 0 ? NULL : tok->buf + multi_line_start;
59-
_PyLexer_restore_fstring_buffers(tok);
30+
tok->buf = base + pointers->buf_from_base;
31+
tok->cur = tok->buf + pointers->cur_from_buf;
32+
tok->inp = tok->buf + pointers->inp_from_buf;
33+
tok->start = pointers->start_from_buf < 0
34+
? NULL : tok->buf + pointers->start_from_buf;
35+
tok->line_start = pointers->line_start_from_buf < 0
36+
? NULL : tok->buf + pointers->line_start_from_buf;
37+
tok->multi_line_start = pointers->multi_line_start_from_buf < 0
38+
? NULL : tok->buf + pointers->multi_line_start_from_buf;
39+
for (int index = tok->tok_mode_stack_index; index > 0; --index) {
40+
tokenizer_mode *mode = &tok->tok_mode_stack[index];
41+
mode->start = mode->start_offset < 0
42+
? NULL : tok->buf + mode->start_offset;
43+
mode->multi_line_start = mode->multi_line_start_offset < 0
44+
? NULL : tok->buf + mode->multi_line_start_offset;
6045
}
61-
return 1;
6246
}

Parser/lexer/buffer.h

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,20 @@
33

44
#include "pyport.h"
55

6-
void _PyLexer_remember_fstring_buffers(struct tok_state *tok);
7-
void _PyLexer_restore_fstring_buffers(struct tok_state *tok);
8-
int _PyLexer_tok_reserve_buf(struct tok_state *tok, Py_ssize_t size);
6+
struct tok_state;
7+
8+
typedef struct {
9+
Py_ssize_t buf_from_base;
10+
Py_ssize_t cur_from_buf;
11+
Py_ssize_t inp_from_buf;
12+
Py_ssize_t start_from_buf;
13+
Py_ssize_t line_start_from_buf;
14+
Py_ssize_t multi_line_start_from_buf;
15+
} _PyLexer_BufferPointers;
16+
17+
void _PyLexer_SaveBufferPointers(
18+
struct tok_state *, const char *, _PyLexer_BufferPointers *);
19+
void _PyLexer_RestoreBufferPointers(
20+
struct tok_state *, char *, const _PyLexer_BufferPointers *);
921

1022
#endif

Parser/lexer/lexer.c

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@
1212

1313

1414
#define MAKE_TOKEN(token_type) _PyLexer_token_setup(tok, token, token_type, p_start, p_end)
15-
#define MAKE_TYPE_COMMENT_TOKEN(token_type, col_offset, end_col_offset) (\
16-
_PyLexer_type_comment_token_setup(tok, token, token_type, col_offset, end_col_offset, p_start, p_end))
1715

1816
/* Spaces in this constant are treated as "zero or more spaces or tabs" when
1917
tokenizing. */
@@ -360,21 +358,25 @@ _PyLexer_get_normal_mode(struct tok_state *tok, tokenizer_mode* current_tok, str
360358
&& !(tok->cur > ignore_end
361359
&& ((unsigned char)ignore_end[0] >= 128 || Py_ISALNUM(ignore_end[0]))));
362360

361+
int type = is_type_ignore ? TYPE_IGNORE : TYPE_COMMENT;
362+
int start_col_offset = is_type_ignore
363+
? ignore_end_col_offset : current_starting_col_offset;
364+
p_end = tok->cur;
363365
if (is_type_ignore) {
364366
p_start = ignore_end;
365-
p_end = tok->cur;
366367

367368
/* If this type ignore is the only thing on the line, consume the newline also. */
368369
if (blankline) {
369370
tok_nextc(tok);
370371
tok->atbol = 1;
371372
}
372-
return MAKE_TYPE_COMMENT_TOKEN(TYPE_IGNORE, ignore_end_col_offset, tok->col_offset);
373373
} else {
374374
p_start = type_start;
375-
p_end = tok->cur;
376-
return MAKE_TYPE_COMMENT_TOKEN(TYPE_COMMENT, current_starting_col_offset, tok->col_offset);
377375
}
376+
_PyLexer_token_setup(tok, token, type, p_start, p_end);
377+
token->start_loc = (_PyTok_Loc){tok->lineno, start_col_offset};
378+
token->end_loc = (_PyTok_Loc){tok->lineno, tok->col_offset};
379+
return type;
378380
}
379381
}
380382
if (tok->tok_extra_tokens) {

Parser/lexer/lexer.h

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,25 @@ int _PyLexer_update_ftstring_expr(struct tok_state *tok, char cur);
77

88
int _PyTokenizer_Get(struct tok_state *, struct token *);
99

10+
/* The view points into the current input window. The next
11+
_PyTokenizer_Get() call may discard it. */
12+
static inline const char *
13+
_PyToken_TextView(const struct tok_state *tok, const struct token *token,
14+
Py_ssize_t *length)
15+
{
16+
assert(length != NULL);
17+
if (token->span.start < 0) {
18+
assert(token->span.start == -1 && token->span.end == -1);
19+
*length = 0;
20+
return "";
21+
}
22+
assert(_PyTok_SpanIsValid(token->span));
23+
assert(tok->buf != NULL);
24+
assert(tok->inp >= tok->buf);
25+
assert(token->span.start >= tok->buf_offset);
26+
assert(token->span.end - tok->buf_offset <= tok->inp - tok->buf);
27+
*length = token->span.end - token->span.start;
28+
return tok->buf + (token->span.start - tok->buf_offset);
29+
}
30+
1031
#endif

0 commit comments

Comments
 (0)