-
-
Notifications
You must be signed in to change notification settings - Fork 35.3k
gh-155525: Fix quadratic complexity in f-string tokenization #156756
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| Fix quadratic-time tokenization of modules containing many f-strings or | ||
| t-strings. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,15 +16,19 @@ _PyLexer_set_ftstring_expr(struct tok_state* tok, struct token *token, char c) { | |
| if (!(tok_mode->in_debug || tok_mode->string_kind == TSTRING) || token->metadata) { | ||
| return 0; | ||
| } | ||
| const char *expression = tok_mode->last_expr_start; | ||
| assert(expression != NULL); | ||
| assert(expression <= tok->start); | ||
| Py_ssize_t expression_size = tok->start - expression; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unless I am missing something, this also changes behaviour for a
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Nope, spot on. Added tests for both cases. |
||
| PyObject *res = NULL; | ||
|
|
||
| // Look for a # character outside of string literals | ||
| int hash_detected = 0; | ||
| int in_string = 0; | ||
| char quote_char = 0; | ||
|
|
||
| for (Py_ssize_t i = 0; i < tok_mode->last_expr_size - tok_mode->last_expr_end; i++) { | ||
| char ch = tok_mode->last_expr_buffer[i]; | ||
| for (Py_ssize_t i = 0; i < expression_size; i++) { | ||
| char ch = expression[i]; | ||
|
|
||
| // Skip escaped characters | ||
| if (ch == '\\') { | ||
|
|
@@ -60,7 +64,8 @@ _PyLexer_set_ftstring_expr(struct tok_state* tok, struct token *token, char c) { | |
| // If we found a # character in the expression, we need to handle comments | ||
| if (hash_detected) { | ||
| // Allocate buffer for processed result | ||
| char *result = (char *)PyMem_Malloc((tok_mode->last_expr_size - tok_mode->last_expr_end + 1) * sizeof(char)); | ||
| char *result = (char *)PyMem_Malloc( | ||
| (expression_size + 1) * sizeof(char)); | ||
| if (!result) { | ||
| return -1; | ||
| } | ||
|
|
@@ -71,8 +76,8 @@ _PyLexer_set_ftstring_expr(struct tok_state* tok, struct token *token, char c) { | |
| quote_char = 0; // Current string quote char | ||
|
|
||
| // Process each character | ||
| while (i < tok_mode->last_expr_size - tok_mode->last_expr_end) { | ||
| char ch = tok_mode->last_expr_buffer[i]; | ||
| while (i < expression_size) { | ||
| char ch = expression[i]; | ||
|
|
||
| // Handle string quotes | ||
| if (ch == '"' || ch == '\'') { | ||
|
|
@@ -87,11 +92,10 @@ _PyLexer_set_ftstring_expr(struct tok_state* tok, struct token *token, char c) { | |
| } | ||
| // Skip comments | ||
| else if (ch == '#' && !in_string) { | ||
| while (i < tok_mode->last_expr_size - tok_mode->last_expr_end && | ||
| tok_mode->last_expr_buffer[i] != '\n') { | ||
| while (i < expression_size && expression[i] != '\n') { | ||
| i++; | ||
| } | ||
| if (i < tok_mode->last_expr_size - tok_mode->last_expr_end) { | ||
| if (i < expression_size) { | ||
| result[j++] = '\n'; | ||
| } | ||
| } | ||
|
|
@@ -106,11 +110,7 @@ _PyLexer_set_ftstring_expr(struct tok_state* tok, struct token *token, char c) { | |
| res = PyUnicode_DecodeUTF8(result, j, NULL); | ||
| PyMem_Free(result); | ||
| } else { | ||
| res = PyUnicode_DecodeUTF8( | ||
| tok_mode->last_expr_buffer, | ||
| tok_mode->last_expr_size - tok_mode->last_expr_end, | ||
| NULL | ||
| ); | ||
| res = PyUnicode_DecodeUTF8(expression, expression_size, NULL); | ||
| } | ||
|
|
||
| if (!res) { | ||
|
|
@@ -120,61 +120,6 @@ _PyLexer_set_ftstring_expr(struct tok_state* tok, struct token *token, char c) { | |
| return 0; | ||
| } | ||
|
|
||
| int | ||
| _PyLexer_update_ftstring_expr(struct tok_state *tok, char cur) | ||
| { | ||
| assert(tok->cur != NULL); | ||
|
|
||
| Py_ssize_t size = strlen(tok->cur); | ||
| tokenizer_mode *tok_mode = TOK_GET_MODE(tok); | ||
|
|
||
| switch (cur) { | ||
| case 0: | ||
| if (!tok_mode->last_expr_buffer || tok_mode->last_expr_end >= 0) { | ||
| return 1; | ||
| } | ||
| char *new_buffer = PyMem_Realloc( | ||
| tok_mode->last_expr_buffer, | ||
| tok_mode->last_expr_size + size | ||
| ); | ||
| if (new_buffer == NULL) { | ||
| PyMem_Free(tok_mode->last_expr_buffer); | ||
| goto error; | ||
| } | ||
| tok_mode->last_expr_buffer = new_buffer; | ||
| strncpy(tok_mode->last_expr_buffer + tok_mode->last_expr_size, tok->cur, size); | ||
| tok_mode->last_expr_size += size; | ||
| break; | ||
| case '{': | ||
| if (tok_mode->last_expr_buffer != NULL) { | ||
| PyMem_Free(tok_mode->last_expr_buffer); | ||
| } | ||
| tok_mode->last_expr_buffer = PyMem_Malloc(size); | ||
| if (tok_mode->last_expr_buffer == NULL) { | ||
| goto error; | ||
| } | ||
| tok_mode->last_expr_size = size; | ||
| tok_mode->last_expr_end = -1; | ||
| strncpy(tok_mode->last_expr_buffer, tok->cur, size); | ||
| break; | ||
| case '}': | ||
| case '!': | ||
| tok_mode->last_expr_end = strlen(tok->start); | ||
| break; | ||
| case ':': | ||
| if (tok_mode->last_expr_end == -1) { | ||
| tok_mode->last_expr_end = strlen(tok->start); | ||
| } | ||
| break; | ||
| default: | ||
| Py_UNREACHABLE(); | ||
| } | ||
| return 1; | ||
| error: | ||
| tok->done = E_NOMEM; | ||
| return 0; | ||
| } | ||
|
|
||
| int | ||
| _PyLexer_check_string_prefixes(struct tok_state *tok, | ||
| int saw_b, int saw_r, int saw_u, | ||
|
|
@@ -268,9 +213,8 @@ _PyLexer_scan_fstring_start(struct tok_state *tok, struct token *token, int c) | |
| the_current_tok->first_line = tok->lineno; | ||
| the_current_tok->start_offset = -1; | ||
| the_current_tok->multi_line_start_offset = -1; | ||
| the_current_tok->last_expr_buffer = NULL; | ||
| the_current_tok->last_expr_size = 0; | ||
| the_current_tok->last_expr_end = -1; | ||
| the_current_tok->last_expr_start = NULL; | ||
| the_current_tok->last_expr_start_offset = -1; | ||
| the_current_tok->in_format_spec = 0; | ||
| the_current_tok->in_debug = 0; | ||
|
|
||
|
|
@@ -436,6 +380,9 @@ _PyLexer_get_fstring_mode(struct tok_state *tok, tokenizer_mode* current_tok, st | |
| if (start_char == '{') { | ||
| int peek1 = tok_nextc(tok); | ||
| tok_backup(tok, peek1); | ||
| if (peek1 != '{') { | ||
| current_tok->last_expr_start = tok->cur; | ||
| } | ||
| tok_backup(tok, start_char); | ||
| if (peek1 != '{') { | ||
| current_tok->curly_bracket_expr_start_depth++; | ||
|
|
@@ -460,13 +407,6 @@ _PyLexer_get_fstring_mode(struct tok_state *tok, tokenizer_mode* current_tok, st | |
| } | ||
| } | ||
|
|
||
| if (current_tok->last_expr_buffer != NULL) { | ||
| PyMem_Free(current_tok->last_expr_buffer); | ||
| current_tok->last_expr_buffer = NULL; | ||
| current_tok->last_expr_size = 0; | ||
| current_tok->last_expr_end = -1; | ||
| } | ||
|
|
||
| p_start = tok->start; | ||
| p_end = tok->cur; | ||
| tok->tok_mode_stack_index--; | ||
|
|
@@ -551,12 +491,10 @@ _PyLexer_get_fstring_mode(struct tok_state *tok, tokenizer_mode* current_tok, st | |
| } | ||
|
|
||
| if (c == '{') { | ||
| if (!_PyLexer_update_ftstring_expr(tok, c)) { | ||
| return MAKE_TOKEN(ENDMARKER); | ||
| } | ||
| int peek = tok_nextc(tok); | ||
| if (peek != '{' || in_format_spec) { | ||
| tok_backup(tok, peek); | ||
| current_tok->last_expr_start = tok->cur; | ||
| tok_backup(tok, c); | ||
| current_tok->curly_bracket_expr_start_depth++; | ||
| if (current_tok->curly_bracket_expr_start_depth >= MAX_EXPR_NESTING) { | ||
|
|
@@ -580,10 +518,10 @@ _PyLexer_get_fstring_mode(struct tok_state *tok, tokenizer_mode* current_tok, st | |
| } | ||
| int peek = tok_nextc(tok); | ||
|
|
||
| // The tokenizer can only be in the format spec if we have already completed the expression | ||
| // scanning (indicated by the end of the expression being set) and we are not at the top level | ||
| // of the bracket stack (-1 is the top level). Since format specifiers can't legally use double | ||
| // brackets, we can bypass it here. | ||
| // The tokenizer can only be in the format spec if expression | ||
| // scanning is complete and we are not at the top level of the | ||
| // bracket stack (-1 is the top level). Since format specifiers | ||
| // can't legally use double brackets, we can bypass it here. | ||
| int cursor = current_tok->curly_bracket_depth; | ||
| if (peek == '}' && !in_format_spec && cursor == 0) { | ||
| p_start = tok->start; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: can we add a comment here saying that this points into
tok->bufand relies on_PyTok_ReaderUnderflownever resetting the buffer whileINSIDE_FSTRING(tok)? That invariant is what makes this work and it lives a bit far from here.