Skip to content

Commit baef09e

Browse files
committed
gh-153569: store formatted-string text as source spans
Formatted-string expressions and comments can outlive the buffer window where scanning began. Pointer boundaries require buffer relocation to repair tokenizer state. Record logical source ranges instead. Retain the active input window while a formatted string is open, give each mode ownership of its comment spans, and materialize text through shared span views.
1 parent 09117bc commit baef09e

11 files changed

Lines changed: 244 additions & 231 deletions

File tree

Lib/test/test_fstring.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1679,6 +1679,20 @@ def __repr__(self):
16791679

16801680
self.assertEqual(f'{" # nooo "=}', '" # nooo "=\' # nooo \'')
16811681
self.assertEqual(f'{" \" # nooo \" "=}', '" \\" # nooo \\" "=\' " # nooo " \'')
1682+
self.assertEqual(f'{"""a" # inside"""=}',
1683+
'"""a" # inside"""=\'a" # inside\'')
1684+
self.assertEqual(f"{'''a' # inside'''=}",
1685+
"'''a' # inside'''=\"a' # inside\"")
1686+
self.assertEqual(f'{"""a""""#" # outside
1687+
=}', '"""a""""#" \n=\'a#\'')
1688+
1689+
x, y = 1, 2
1690+
self.assertEqual(f'{x != y # outside
1691+
=}', 'x != y \n=True')
1692+
1693+
d = {'a#b': 42}
1694+
self.assertEqual(f'''{f"{d["a#b"]}"=}''',
1695+
'f"{d["a#b"]}"=\'42\'')
16821696

16831697
self.assertEqual(f'{ # some comment goes here
16841698
"""hello"""=}', ' \n """hello"""=\'hello\'')

Lib/test/test_tokenize.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2573,6 +2573,25 @@ def test_degraded_fstring_format_spec(self):
25732573
("f-string: single '}' is not allowed", (1, 11)),
25742574
)
25752575

2576+
def test_carriage_return_after_debug_comment(self):
2577+
for prefix in ("f", "t"):
2578+
with self.subTest(prefix=prefix):
2579+
tokens = self._get_tokens(f"{prefix}'''{{x=# comment\r}}'''")
2580+
self.assertEqual(tokens[4].string, "# comment\r}")
2581+
2582+
def test_incomplete_formatted_string_comment_after_carriage_return(self):
2583+
for prefix in ("f", "t"):
2584+
with self.subTest(prefix=prefix):
2585+
for extra_tokens in (False, True):
2586+
with self.assertRaises(tokenize.TokenError) as caught:
2587+
self._get_tokens(
2588+
f"{prefix}'{{#\r!", extra_tokens=extra_tokens
2589+
)
2590+
self.assertEqual(
2591+
caught.exception.args,
2592+
("unexpected EOF in multi-line statement", (1, 7)),
2593+
)
2594+
25762595
def test_escaped_fstring_brace_has_a_position_gap(self):
25772596
tokens = self._get_tokens('f"a{{"', extra_tokens=True)
25782597
self.assertEqual(

Lib/test/test_tstring.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,5 +287,22 @@ def test_triple_quoted(self):
287287
)
288288
self.assertEqual(fstring(t), "\n Hello,\n Python\n ")
289289

290+
t = t'{"""a" # inside"""}'
291+
self.assertEqual(t.interpolations[0].expression,
292+
'"""a" # inside"""')
293+
294+
t = t'{"""a""""#" # outside
295+
}'
296+
self.assertEqual(t.interpolations[0].expression, '"""a""""#"')
297+
298+
x, y = 1, 2
299+
t = t'{x != y # outside
300+
}'
301+
self.assertEqual(t.interpolations[0].expression, 'x != y')
302+
303+
d = {'a#b': 42}
304+
t = t'''{f"{d["a#b"]}"}'''
305+
self.assertEqual(t.interpolations[0].expression, 'f"{d["a#b"]}"')
306+
290307
if __name__ == '__main__':
291308
unittest.main()

Parser/lexer/buffer.c

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,6 @@ _PyLexer_SaveBufferPointers(struct tok_state *tok, const char *base,
1515
? -1 : tok->line_start - tok->buf;
1616
pointers->multi_line_start_from_buf = tok->multi_line_start == NULL
1717
? -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];
20-
mode->start_offset = mode->start == NULL ? -1 : mode->start - tok->buf;
21-
mode->multi_line_start_offset = mode->multi_line_start == NULL
22-
? -1 : mode->multi_line_start - tok->buf;
23-
}
2418
}
2519

2620
void
@@ -36,11 +30,4 @@ _PyLexer_RestoreBufferPointers(struct tok_state *tok, char *base,
3630
? NULL : tok->buf + pointers->line_start_from_buf;
3731
tok->multi_line_start = pointers->multi_line_start_from_buf < 0
3832
? 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;
45-
}
4633
}

Parser/lexer/lexer.c

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,18 @@ _PyLexer_get_normal_mode(struct tok_state *tok, tokenizer_mode* current_tok, str
317317
c = tok_nextc(tok);
318318
}
319319

320+
if (INSIDE_FSTRING(tok) && INSIDE_FSTRING_EXPR(current_tok)) {
321+
const char *comment_end = tok->cur;
322+
if (c == '\n' || c == '\r') {
323+
comment_end--;
324+
}
325+
if (_PyLexer_record_ftstring_comment(
326+
tok, tok->start, comment_end) < 0) {
327+
tok->done = E_NOMEM;
328+
return MAKE_TOKEN(ERRORTOKEN);
329+
}
330+
}
331+
320332
if (tok->tok_extra_tokens) {
321333
p = tok->start;
322334
}
@@ -544,10 +556,18 @@ _PyLexer_get_normal_mode(struct tok_state *tok, tokenizer_mode* current_tok, str
544556
int cursor_in_format_with_debug =
545557
cursor == 1 && (current_tok->in_debug || in_format_spec);
546558
int cursor_valid = cursor == 0 || cursor_in_format_with_debug;
547-
if ((cursor_valid) && !_PyLexer_update_ftstring_expr(tok, c)) {
548-
return MAKE_TOKEN(ENDMARKER);
559+
if (cursor_valid && c == '!') {
560+
int c2 = tok_nextc(tok);
561+
if (c2 == '=') {
562+
cursor_valid = 0;
563+
}
564+
tok_backup(tok, c2);
565+
}
566+
if (cursor_valid) {
567+
_PyLexer_update_ftstring_expr(tok, c);
549568
}
550-
if ((cursor_valid) && c != '{' && _PyLexer_set_ftstring_expr(tok, token, c)) {
569+
if (cursor_valid && c != '{' &&
570+
_PyLexer_set_ftstring_expr_metadata(tok, token)) {
551571
return MAKE_TOKEN(ERRORTOKEN);
552572
}
553573

Parser/lexer/lexer.h

Lines changed: 1 addition & 9 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
/* The view points into the current input window. The next
@@ -19,13 +17,7 @@ _PyToken_TextView(const struct tok_state *tok, const struct token *token,
1917
*length = 0;
2018
return "";
2119
}
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);
20+
return _PyLexer_BufferSpanView(tok, token->span, length);
2921
}
3022

3123
#endif

Parser/lexer/lexer_internal.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,10 @@ TOK_NEXT_MODE(struct tok_state *tok)
4646

4747
int _PyLexer_nextc(struct tok_state *);
4848
void _PyLexer_backup(struct tok_state *, int);
49-
int _PyLexer_set_ftstring_expr(struct tok_state *, struct token *, char);
49+
void _PyLexer_update_ftstring_expr(struct tok_state *, char);
50+
int _PyLexer_record_ftstring_comment(
51+
struct tok_state *, const char *, const char *);
52+
int _PyLexer_set_ftstring_expr_metadata(struct tok_state *, struct token *);
5053
int _PyLexer_check_string_prefixes(struct tok_state *, int, int, int, int, int);
5154
int _PyLexer_scan_number(struct tok_state *, struct token *, int, int);
5255
int _PyLexer_scan_fstring_start(struct tok_state *, struct token *, int);

Parser/lexer/state.c

Lines changed: 4 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -60,24 +60,6 @@ _PyTokenizer_tok_new(void)
6060
return tok;
6161
}
6262

63-
static void
64-
free_fstring_expressions(struct tok_state *tok)
65-
{
66-
int index;
67-
tokenizer_mode *mode;
68-
69-
for (index = tok->tok_mode_stack_index; index >= 0; --index) {
70-
mode = &(tok->tok_mode_stack[index]);
71-
if (mode->last_expr_buffer != NULL) {
72-
PyMem_Free(mode->last_expr_buffer);
73-
mode->last_expr_buffer = NULL;
74-
mode->last_expr_size = 0;
75-
mode->last_expr_end = -1;
76-
mode->in_format_spec = 0;
77-
}
78-
}
79-
}
80-
8163
/* Free a tok_state structure */
8264
void
8365
_PyTokenizer_Free(struct tok_state *tok)
@@ -89,7 +71,9 @@ _PyTokenizer_Free(struct tok_state *tok)
8971
Py_XDECREF(tok->module);
9072
_PyTok_ReaderFree(tok);
9173
_PyTok_SourceClear(&tok->source);
92-
free_fstring_expressions(tok);
74+
for (int i = 0; i <= tok->tok_mode_stack_index; i++) {
75+
PyMem_Free(tok->tok_mode_stack[i].comments);
76+
}
9377
PyMem_Free(tok);
9478
}
9579

@@ -108,31 +92,11 @@ _PyToken_Init(struct token *token) {
10892
token->metadata = NULL;
10993
}
11094

111-
static inline _PyTok_Span
112-
buffer_span(const struct tok_state *tok, const char *start, const char *end)
113-
{
114-
if (start == NULL) {
115-
assert(end == NULL);
116-
return (_PyTok_Span){-1, -1};
117-
}
118-
assert(end != NULL);
119-
const char *base = tok->buf;
120-
assert(base != NULL);
121-
assert(tok->inp >= base);
122-
Py_ssize_t start_offset = start - base;
123-
Py_ssize_t end_offset = end - base;
124-
assert(start_offset >= 0 && start_offset <= end_offset);
125-
assert(end_offset <= tok->inp - base);
126-
assert(tok->buf_offset <= PY_SSIZE_T_MAX - end_offset);
127-
return _PyTok_SpanFromBounds(
128-
tok->buf_offset + start_offset, tok->buf_offset + end_offset);
129-
}
130-
13195
int
13296
_PyLexer_token_setup(struct tok_state *tok, struct token *token, int type, const char *start, const char *end)
13397
{
13498
token->level = tok->level;
135-
token->span = buffer_span(tok, start, end);
99+
token->span = _PyLexer_BufferSpan(tok, start, end);
136100
int lineno = ISSTRINGLIT(type) ? tok->first_lineno : tok->lineno;
137101
token->start_loc = (_PyTok_Loc){lineno, -1};
138102
token->end_loc = (_PyTok_Loc){tok->lineno, -1};

Parser/lexer/state.h

Lines changed: 57 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@ enum string_kind_t {
4141

4242
#define MAX_EXPR_NESTING 3
4343

44+
typedef struct _tokenizer_comments {
45+
Py_ssize_t count;
46+
Py_ssize_t capacity;
47+
_PyTok_Span spans[];
48+
} tokenizer_comments;
49+
4450
typedef struct _tokenizer_mode {
4551
enum tokenizer_mode_kind_t kind;
4652

@@ -50,20 +56,16 @@ typedef struct _tokenizer_mode {
5056
char quote;
5157
int quote_size;
5258
int raw;
53-
const char* start;
54-
const char* multi_line_start;
59+
_PyTok_Off start;
60+
_PyTok_Off multi_line_start;
5561
int first_line;
5662

57-
Py_ssize_t start_offset;
58-
Py_ssize_t multi_line_start_offset;
59-
60-
Py_ssize_t last_expr_size;
61-
Py_ssize_t last_expr_end;
62-
char* last_expr_buffer;
63+
_PyTok_Span expr_span;
6364
int in_debug;
6465
int in_format_spec;
6566

6667
enum string_kind_t string_kind;
68+
tokenizer_comments *comments;
6769
} tokenizer_mode;
6870

6971
/* Tokenizer state */
@@ -129,6 +131,53 @@ struct tok_state {
129131
#endif
130132
};
131133

134+
static inline _PyTok_Off
135+
_PyLexer_BufferOffset(const struct tok_state *tok, const char *position)
136+
{
137+
assert(tok->buf != NULL);
138+
assert(tok->inp >= tok->buf);
139+
assert(position >= tok->buf && position <= tok->inp);
140+
Py_ssize_t offset = position - tok->buf;
141+
assert(tok->buf_offset <= PY_SSIZE_T_MAX - offset);
142+
return tok->buf_offset + offset;
143+
}
144+
145+
static inline char *
146+
_PyLexer_BufferPointer(const struct tok_state *tok, _PyTok_Off offset)
147+
{
148+
assert(tok->buf != NULL);
149+
assert(tok->inp >= tok->buf);
150+
assert(offset >= tok->buf_offset);
151+
assert(offset - tok->buf_offset <= tok->inp - tok->buf);
152+
return tok->buf + (offset - tok->buf_offset);
153+
}
154+
155+
static inline const char *
156+
_PyLexer_BufferSpanView(const struct tok_state *tok, _PyTok_Span span,
157+
Py_ssize_t *length)
158+
{
159+
assert(length != NULL);
160+
assert(_PyTok_SpanIsValid(span));
161+
*length = span.end - span.start;
162+
(void)_PyLexer_BufferPointer(tok, span.end);
163+
return _PyLexer_BufferPointer(tok, span.start);
164+
}
165+
166+
static inline _PyTok_Span
167+
_PyLexer_BufferSpan(const struct tok_state *tok, const char *start,
168+
const char *end)
169+
{
170+
if (start == NULL) {
171+
assert(end == NULL);
172+
return (_PyTok_Span){-1, -1};
173+
}
174+
assert(end != NULL);
175+
assert(start <= end);
176+
return _PyTok_SpanFromBounds(
177+
_PyLexer_BufferOffset(tok, start),
178+
_PyLexer_BufferOffset(tok, end));
179+
}
180+
132181
int _PyLexer_token_setup(struct tok_state *tok, struct token *token, int type, const char *start, const char *end);
133182

134183
struct tok_state *_PyTokenizer_tok_new(void);

0 commit comments

Comments
 (0)