https://github.com/python/cpython/commit/6af655301a19806a3f8ebe21b166f499cb431e70
commit: 6af655301a19806a3f8ebe21b166f499cb431e70
branch: main
author: Pablo Galindo Salgado <[email protected]>
committer: pablogsal <[email protected]>
date: 2026-10-18T11:54:19+01:00
summary:

gh-159465: Improve incomplete input detection (#159466)

* gh-159465: Improve incomplete input detection

* gh-159465: Track newline effects and preserve parser layout

* gh-159465: Share incomplete-input error cleanup

files:
A Misc/NEWS.d/next/Library/2026-10-15-13-44-03.gh-issue-159465.Ph-m8w.rst
M Include/errcode.h
M Include/internal/pycore_parser.h
M Lib/codeop.py
M Lib/test/test_codeop.py
M Modules/_testinternalcapi/tokenizer.c
M Parser/action_helpers.c
M Parser/lexer/layout.c
M Parser/lexer/lexer.c
M Parser/lexer/state.h
M Parser/lexer/string.c
M Parser/peg_api.c
M Parser/pegen.c
M Parser/pegen.h
M Parser/pegen_errors.c
M Parser/tokenizer/api.c
M Parser/tokenizer/decoder.c
M Parser/tokenizer/reader.c
M Parser/tokenizer/reader_internal.h
M Parser/tokenizer/source.c
M Parser/tokenizer/source.h
M Parser/tokenizer/tokenizer.h
M Python/pythonrun.c
M Python/symtable.c
M Tools/peg_generator/peg_extension/peg_extension.c

diff --git a/Include/errcode.h b/Include/errcode.h
index dac5cf068c99d6d..10cd9b74371f2b6 100644
--- a/Include/errcode.h
+++ b/Include/errcode.h
@@ -38,6 +38,7 @@ extern "C" {
 #define E_BADSINGLE      27      /* Ill-formed single statement input */
 #define E_INTERACT_STOP  28      /* Interactive mode stopped tokenization */
 #define E_COLUMNOVERFLOW 29      /* Column offset overflow */
+#define E_INCOMPLETE     30      /* An unfinished token in partial input */
 
 #ifdef __cplusplus
 }
diff --git a/Include/internal/pycore_parser.h b/Include/internal/pycore_parser.h
index b89d02035db71f9..2355016d8a8fcb5 100644
--- a/Include/internal/pycore_parser.h
+++ b/Include/internal/pycore_parser.h
@@ -27,13 +27,17 @@ _Py_DECLARE_STR(empty, "")
         }, \
     }
 
+/* The caller must initialize *incomplete to NULL if supplied. A returned AST
+   can also return an owned exception there for a missing line terminator.
+   Validate the AST before reporting that exception. */
 extern struct _mod* _PyParser_ASTFromString(
     const char *str,
     PyObject* filename,
     int mode,
     PyCompilerFlags *flags,
     PyArena *arena,
-    PyObject *module);
+    PyObject *module,
+    PyObject **incomplete);
 
 extern struct _mod* _PyParser_ASTFromFile(
     FILE *fp,
diff --git a/Lib/codeop.py b/Lib/codeop.py
index 40e88423119bc4f..140e960b35f4ef7 100644
--- a/Lib/codeop.py
+++ b/Lib/codeop.py
@@ -62,15 +62,10 @@ def _maybe_compile(compiler, source, filename, symbol, 
flags):
         warnings.simplefilter("ignore", (SyntaxWarning, DeprecationWarning))
         try:
             compiler(source, filename, symbol, flags=flags)
+        except _IncompleteInputError:
+            return None
         except SyntaxError:  # Let other compile() errors propagate.
-            try:
-                compiler(source + "\n", filename, symbol, flags=flags)
-                return None
-            except _IncompleteInputError:
-                return None
-            except SyntaxError:
-                pass
-                # fallthrough
+            pass
 
     return compiler(source, filename, symbol, incomplete_input=False)
 
diff --git a/Lib/test/test_codeop.py b/Lib/test/test_codeop.py
index c75c0f8627bf084..69dbc2ddfb6bbe4 100644
--- a/Lib/test/test_codeop.py
+++ b/Lib/test/test_codeop.py
@@ -124,6 +124,99 @@ def test_incomplete_string_diagnostics(self, symbol, 
prefix):
         self.assertEqual(cm.exception.args, (
             'incomplete input', ('<input>', 2, 9, text, 2, -1)))
 
+    @subTests('compiler', COMPILERS)
+    @subTests('newline', ('\n', '\r', '\r\n'))
+    def test_unterminated_trailing_line(self, compiler, newline):
+        for head, symbol in (('1', 'eval'), ('if 1: pass', 'single'),
+                             ('if 1:\n pass', 'single')):
+            for tail in (' ', '\t', '\f ', '\n '):
+                source = head + newline + tail
+                with self.subTest(source=source, symbol=symbol):
+                    if compiler in RAW_COMPILERS:
+                        with self.assertRaises(_IncompleteInputError):
+                            compiler(source, '<input>', symbol)
+                    else:
+                        self.assertIsNone(compiler(source, '<input>', symbol))
+                    self.assertValid(source + '\n', symbol, compiler=compiler)
+            for tail in ('\f', ' \f', '\n\f'):
+                with self.subTest(head=head, tail=tail, symbol=symbol):
+                    self.assertValid(head + newline + tail, symbol,
+                                     compiler=compiler)
+
+        for tail in ('# comment', ' # comment'):
+            self.assertValid('if 1: pass' + newline + tail, compiler=compiler)
+            self.assertValid('1' + newline + tail, 'eval', compiler=compiler)
+
+        for head in ('1', 'if 1: pass', 'if 1:\n pass'):
+            for tail in (' ', '\t', '\f', '# comment'):
+                self.assertValid(head + newline + tail, 'exec',
+                                 compiler=compiler)
+
+    @subTests('symbol', ('single', 'exec', 'eval'))
+    def test_unfinished_token_in_invalid_position(self, symbol):
+        for source in ('a b', '(a b', '1 """', '(1 """',
+                       'f"{lambda:\\', 't"{lambda:\\'):
+            for compiler in COMPILERS:
+                with self.subTest(source=source, compiler=compiler):
+                    with self.assertRaises(SyntaxError) as cm:
+                        compiler(source, '<input>', symbol)
+                    self.assertNotIsInstance(cm.exception, 
_IncompleteInputError)
+
+    @subTests('symbol', ('single', 'exec', 'eval'))
+    def test_invalid_partial_string_diagnostic(self, symbol):
+        with self.assertRaisesRegex(SyntaxError, 'invalid syntax'):
+            Compile()('a"""', '<input>', symbol)
+
+    @subTests('symbol', ('single', 'exec', 'eval'))
+    @subTests('prefix', ('', 'r', 'b', 'f', 't', 'rf', 'rt'))
+    def test_string_continuation(self, symbol, prefix):
+        source = prefix + '"first\\'
+        compile(source + '\nsecond"\n', '<input>', symbol)
+        self.assertIsNone(compile_command(source, symbol=symbol))
+        with self.assertRaises(_IncompleteInputError):
+            Compile()(source, '<input>', symbol)
+
+    @subTests('symbol', ('single', 'exec', 'eval'))
+    @subTests('newline', ('\n', '\r', '\r\n'))
+    def test_continued_final_line(self, symbol, newline):
+        source = '1\\' + newline
+        compile(source + '+ 2\n', '<input>', symbol)
+        self.assertIsNone(compile_command(source, symbol=symbol))
+        with self.assertRaises(_IncompleteInputError):
+            Compile()(source, '<input>', symbol)
+        with self.assertRaises(SyntaxError) as cm:
+            Compile()('1 + \\' + newline, '<input>', symbol)
+        # Appending LF to CR completes the CRLF line terminator.
+        if newline != '\r':
+            self.assertNotIsInstance(cm.exception, _IncompleteInputError)
+
+    @subTests('symbol', ('single', 'exec'))
+    def test_eof_before_nested_body(self, symbol):
+        for source in ('if True:\n    if True:',
+                       'if True:\n    def f():',
+                       'match 1:\n    case 1:'):
+            for tail in ('', '\n', '\n\n'):
+                with self.subTest(source=source, tail=tail):
+                    compile(source + tail + '\n        pass\n', '<input>', 
symbol)
+                    self.assertIsNone(compile_command(source + tail, 
symbol=symbol))
+                    with self.assertRaises(_IncompleteInputError):
+                        Compile()(source + tail, '<input>', symbol)
+
+    def test_incomplete_suite_semantic_errors(self):
+        cases = (
+            ('if True: break', "'break' outside loop"),
+            ('if True: return 1', "'return' outside function"),
+            ('def f(x, x):\n    pass', 'duplicate parameter'),
+        )
+        for source, message in cases:
+            for compiler in COMPILERS:
+                with self.subTest(source=source, compiler=compiler):
+                    with self.assertRaises(SyntaxError) as cm:
+                        compiler(source, '<input>', 'single')
+                    self.assertNotIsInstance(cm.exception, 
_IncompleteInputError)
+                    if compiler in RAW_COMPILERS:
+                        self.assertIn(message, str(cm.exception))
+
     @subTests('compiler', COMPILERS)
     def test_incomplete(self, compiler):
         ai = functools.partial(self.assertIncomplete, compiler=compiler)
diff --git 
a/Misc/NEWS.d/next/Library/2026-10-15-13-44-03.gh-issue-159465.Ph-m8w.rst 
b/Misc/NEWS.d/next/Library/2026-10-15-13-44-03.gh-issue-159465.Ph-m8w.rst
new file mode 100644
index 000000000000000..21199b550f9a54f
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-10-15-13-44-03.gh-issue-159465.Ph-m8w.rst
@@ -0,0 +1,2 @@
+Improve incomplete input detection in :mod:`codeop` without an extra parser
+pass, and preserve syntax and compiler errors.
diff --git a/Modules/_testinternalcapi/tokenizer.c 
b/Modules/_testinternalcapi/tokenizer.c
index 0c2bdee2e803b22..f41a517deefcefd 100644
--- a/Modules/_testinternalcapi/tokenizer.c
+++ b/Modules/_testinternalcapi/tokenizer.c
@@ -95,6 +95,35 @@ test_tokenizer_source(PyObject *Py_UNUSED(module),
         goto error;
     }
 
+    _PyTok_SourceClear(&source);
+    if (check(_PyTok_SourceAppendLineWithNewline(&source, "", 0) == 0,
+              "cannot append empty terminated line") < 0 ||
+            check(_PyTok_SourceAppendLineWithNewline(&source, tail, 4) == 1,
+                  "wrong terminated line offset") < 0 ||
+            check(source.len == 6 && source.nlines == 2 &&
+                      memcmp(source.bytes, "\ntail\n", 7) == 0,
+                  "wrong terminated source contents") < 0 ||
+            check_system_error(_PyTok_SourceAppendLineWithNewline(
+                &source, terminated_line, 2) < 0,
+                "accepted newline in line payload") < 0) {
+        goto error;
+    }
+    source.base_offset = PY_SSIZE_T_MAX - source.len;
+    if (check(_PyTok_SourceAppendLineWithNewline(&source, "", 0) < 0 &&
+                  PyErr_ExceptionMatches(PyExc_MemoryError),
+              "accepted overflowing appended newline") < 0) {
+        goto error;
+    }
+    PyErr_Clear();
+    source.base_offset = 0;
+    source.nlines = INT_MAX - 1;
+    if (check(_PyTok_SourceAppendLineWithNewline(&source, "", 0) < 0 &&
+                  PyErr_ExceptionMatches(PyExc_OverflowError),
+              "accepted overflowing newline count") < 0) {
+        goto error;
+    }
+    PyErr_Clear();
+
     _PyTok_SourceClear(&source);
     Py_RETURN_NONE;
 
diff --git a/Parser/action_helpers.c b/Parser/action_helpers.c
index 2d51795d887993d..2f8a5d84dd9755b 100644
--- a/Parser/action_helpers.c
+++ b/Parser/action_helpers.c
@@ -1497,11 +1497,11 @@ _PyPegen_joined_str(Parser *p, Token* a, asdl_expr_seq* 
raw_expressions, Token*b
 }
 
 expr_ty _PyPegen_decoded_constant_from_token(Parser* p, Token* tok) {
-    Py_ssize_t bsize;
-    char* bstr;
-    if (PyBytes_AsStringAndSize(tok->bytes, &bstr, &bsize) == -1) {
-        return NULL;
+    if (tok->bytes == NULL) {
+        return _PyPegen_raise_incomplete_input(p);
     }
+    Py_ssize_t bsize = PyBytes_GET_SIZE(tok->bytes);
+    const char *bstr = PyBytes_AS_STRING(tok->bytes);
 
     int is_raw = tok->is_raw;
 
@@ -1519,10 +1519,10 @@ expr_ty _PyPegen_decoded_constant_from_token(Parser* p, 
Token* tok) {
 }
 
 expr_ty _PyPegen_constant_from_token(Parser* p, Token* tok) {
-    const char* bstr = PyBytes_AsString(tok->bytes);
-    if (bstr == NULL) {
-        return NULL;
+    if (tok->bytes == NULL) {
+        return _PyPegen_raise_incomplete_input(p);
     }
+    const char *bstr = PyBytes_AS_STRING(tok->bytes);
     PyObject* str = PyUnicode_FromString(bstr);
     if (str == NULL) {
         return NULL;
@@ -1537,10 +1537,10 @@ expr_ty _PyPegen_constant_from_token(Parser* p, Token* 
tok) {
 }
 
 expr_ty _PyPegen_constant_from_string(Parser* p, Token* tok) {
-    const char* the_str = PyBytes_AsString(tok->bytes);
-    if (the_str == NULL) {
-        return NULL;
+    if (tok->bytes == NULL) {
+        return _PyPegen_raise_incomplete_input(p);
     }
+    const char *the_str = PyBytes_AS_STRING(tok->bytes);
     PyObject *s = _PyPegen_parse_string(p, tok);
     if (s == NULL) {
         _Pypegen_raise_decode_error(p);
diff --git a/Parser/lexer/layout.c b/Parser/lexer/layout.c
index 6091f9adeac958c..f503f36f76ff14c 100644
--- a/Parser/lexer/layout.c
+++ b/Parser/lexer/layout.c
@@ -21,6 +21,9 @@ _PyLexer_ContinueLine(struct tok_state *tok)
         return -1;
     }
     c = tok_nextc(tok);
+    if (tok->implicit_newline == _PyTokenizer_APPEND_NEWLINE) {
+        tok->needs_newline |= _PyTokenizer_NEWLINE_CONTINUATION;
+    }
     if (c == EOF) {
         tok->done = E_EOF;
         tok->cur = tok->inp;
@@ -114,6 +117,11 @@ _PyLexer_BeginLine(struct tok_state *tok)
         }
     }
     tok_backup(tok, c);
+    if (c == '\n' && tok->implicit_newline == _PyTokenizer_IMPLY_NEWLINE &&
+        tok->level == 0 && (cont_line_col || col)) {
+        // Without the newline, a whitespace-only line affects indentation.
+        tok->needs_newline |= _PyTokenizer_NEWLINE_INDENT;
+    }
     if (c == '#' || c == '\n' || c == '\r') {
         int interactive = _PyTok_ReaderIsInteractive(tok);
         /* Lines with only whitespace and/or comments
@@ -171,6 +179,10 @@ _PyLexer_IndentationToken(struct tok_state *tok, struct 
token *token)
 int
 _PyLexer_Newline(struct tok_state *tok, struct token *token, int blankline)
 {
+    if (!blankline && tok->implicit_newline == _PyTokenizer_IMPLY_NEWLINE) {
+        // The statement's terminating newline was not in the input.
+        tok->needs_newline |= _PyTokenizer_NEWLINE_STATEMENT;
+    }
     tok->layout.at_bol = 1;
     if (blankline || tok->level > 0) {
         if (!tok->tok_extra_tokens) {
diff --git a/Parser/lexer/lexer.c b/Parser/lexer/lexer.c
index 83071e9e0b8ddf0..39b39b981c1fc49 100644
--- a/Parser/lexer/lexer.c
+++ b/Parser/lexer/lexer.c
@@ -235,6 +235,11 @@ _PyLexer_get_normal(struct tok_state *tok, ftstring_state 
*current, struct token
                 return type;
             }
         }
+        if (tok->implicit_newline == _PyTokenizer_IMPLY_NEWLINE &&
+            tok->layout.depth > 0) {
+            // Ending this comment lets the next line dedent the suite.
+            tok->needs_newline |= _PyTokenizer_NEWLINE_STATEMENT;
+        }
         if (tok->tok_extra_tokens) {
             tok_backup(tok, c);  /* don't eat the newline or EOF */
             p_start = comment;
@@ -516,7 +521,7 @@ _PyTokenizer_Get(struct tok_state *tok, struct token *token)
             Py_UNREACHABLE();
         }
     }
-    if (tok_failed(tok)) {
+    if (tok_failed(tok) && tok->done != E_INCOMPLETE) {
         result = ERRORTOKEN;
     }
     token->type = result;
diff --git a/Parser/lexer/state.h b/Parser/lexer/state.h
index 51ad278b13c40db..9c1475356b05353 100644
--- a/Parser/lexer/state.h
+++ b/Parser/lexer/state.h
@@ -105,6 +105,8 @@ struct tok_state {
     int debug;
 #endif
     int barry_as_bdfl;
+    unsigned char allow_incomplete_input;
+    unsigned char needs_newline;
 };
 
 static inline ftstring_state *
diff --git a/Parser/lexer/string.c b/Parser/lexer/string.c
index d0f568dfa857580..c2bef9773eab937 100644
--- a/Parser/lexer/string.c
+++ b/Parser/lexer/string.c
@@ -21,6 +21,15 @@ string_error_token(struct tok_state *tok, struct token 
*token,
     return type;
 }
 
+static int
+incomplete_string_token(struct tok_state *tok, struct token *token,
+                        int type, _PyTok_Off start, _PyTok_Loc location)
+{
+    string_error_token(tok, token, start, location);
+    tok->done = E_INCOMPLETE;
+    return type;
+}
+
 int
 _PyLexer_record_ftstring_comment(struct tok_state *tok, ftstring_state *state,
                                  _PyTok_Off start, _PyTok_Off end)
@@ -337,6 +346,10 @@ _PyLexer_scan_string(struct tok_state *tok, struct token 
*token, int c)
             if (tok_failed(tok)) {
                 return MAKE_TOKEN(ERRORTOKEN);
             }
+            if (c == EOF && tok->allow_incomplete_input) {
+                return incomplete_string_token(tok, token, STRING,
+                                               tok->start, tok->start_loc);
+            }
             int end_lineno = tok->lineno;
             _PyTok_Loc location = tok->start_loc;
             const char *line = _PyTok_SourcePointer(
@@ -450,6 +463,10 @@ _PyLexer_get_ftstring(struct tok_state *tok, 
ftstring_state *current, struct tok
             if (tok_failed(tok)) {
                 return MAKE_TOKEN(ERRORTOKEN);
             }
+            if (c == EOF && tok->allow_incomplete_input) {
+                return incomplete_string_token(tok, token,
+                    FTSTRING_MIDDLE(current), current->start, 
current->start_loc);
+            }
 
             if (in_format_spec && c == '\n') {
                 return MAKE_TOKEN(_PyTokenizer_syntaxerror(
diff --git a/Parser/peg_api.c b/Parser/peg_api.c
index e30ca0453bd3e13..6791478ba688a68 100644
--- a/Parser/peg_api.c
+++ b/Parser/peg_api.c
@@ -5,14 +5,14 @@
 mod_ty
 _PyParser_ASTFromString(const char *str, PyObject* filename, int mode,
                         PyCompilerFlags *flags, PyArena *arena,
-                        PyObject *module)
+                        PyObject *module, PyObject **incomplete)
 {
     if (PySys_Audit("compile", "yO", str, filename) < 0) {
         return NULL;
     }
 
     mod_ty result = _PyPegen_run_parser_from_string(str, mode, filename, flags,
-                                                    arena, module);
+                                                    arena, module, incomplete);
     return result;
 }
 
@@ -39,4 +39,4 @@ _PyParser_InteractiveASTFromFile(FILE *fp, PyObject 
*filename_ob, const char *en
     }
     return _PyPegen_run_parser_from_file_pointer(fp, mode, filename_ob, enc, 
ps1, ps2,
                                                  flags, errcode, 
interactive_src, arena);
-}
\ No newline at end of file
+}
diff --git a/Parser/pegen.c b/Parser/pegen.c
index 4ca42a407bcde80..bb14a1e78c5ad35 100644
--- a/Parser/pegen.c
+++ b/Parser/pegen.c
@@ -247,23 +247,26 @@ static int
 initialize_token(Parser *p, Token *parser_token, struct token *new_token, int 
token_type) {
     assert(parser_token != NULL);
 
-    Py_ssize_t length;
-    const char *text = _PyToken_TextView(p->tok, new_token, &length);
-    parser_token->type = token_type == NAME
-        ? _get_keyword_or_name_type(p, text, length) : token_type;
-    if (token_needs_text(parser_token->type)) {
-        parser_token->bytes = PyBytes_FromStringAndSize(text, length);
-        if (parser_token->bytes == NULL) {
-            return -1;
+    parser_token->type = token_type;
+    parser_token->bytes = NULL;
+    if (token_needs_text(token_type) && new_token->span.start >= 0) {
+        Py_ssize_t length;
+        const char *text = _PyToken_TextView(p->tok, new_token, &length);
+        if (token_type == NAME) {
+            parser_token->type = _get_keyword_or_name_type(p, text, length);
         }
-        if (_PyArena_AddPyObject(p->arena, parser_token->bytes) < 0) {
-            Py_DECREF(parser_token->bytes);
-            return -1;
+        // A name reclassified as a keyword does not need its text.
+        if (parser_token->type == token_type) {
+            parser_token->bytes = PyBytes_FromStringAndSize(text, length);
+            if (parser_token->bytes == NULL) {
+                return -1;
+            }
+            if (_PyArena_AddPyObject(p->arena, parser_token->bytes) < 0) {
+                Py_DECREF(parser_token->bytes);
+                return -1;
+            }
         }
     }
-    else {
-        parser_token->bytes = NULL;
-    }
 
     parser_token->metadata = NULL;
     if (new_token->metadata != NULL) {
@@ -287,7 +290,7 @@ initialize_token(Parser *p, Token *parser_token, struct 
token *new_token, int to
 
     p->fill += 1;
 
-    if (token_type == ERRORTOKEN && _PyTokenizer_GetInfo(p->tok).status == 
E_DECODE) {
+    if (token_type == ERRORTOKEN && _PyTokenizer_GetStatus(p->tok) == 
E_DECODE) {
         return _Pypegen_raise_decode_error(p);
     }
 
@@ -352,6 +355,13 @@ _PyPegen_fill_token(Parser *p)
     }
 
     int type = new_token.type;
+    // EOF can finish an ordinary token. Only a boundary token means that
+    // the grammar requested input beyond the last physical line.
+    if (type == ENDMARKER ||
+        (type == DEDENT && (p->flags & PyPARSE_ALLOW_INCOMPLETE_INPUT) &&
+         _PyTokenizer_GetStatus(p->tok) == E_EOF)) {
+        p->input_exhausted = 1;
+    }
 
     // If we have reached the end and we are in single input mode we need to 
insert a newline and reset the parsing
     if (p->start_rule == Py_single_input && type == ENDMARKER && 
p->parsing_started) {
@@ -953,6 +963,7 @@ _PyPegen_Parser_New(struct tok_state *tok, int start_rule, 
int flags,
     }
     assert(tok != NULL);
     _PyTokenizer_SetOptions(tok, 0, (flags & PyPARSE_TYPE_COMMENTS) > 0);
+    _PyTokenizer_AllowIncompleteInput(tok, flags & 
PyPARSE_ALLOW_INCOMPLETE_INPUT);
     p->tok = tok;
     p->keywords = NULL;
     p->n_keyword_lists = -1;
@@ -998,6 +1009,7 @@ _PyPegen_Parser_New(struct tok_state *tok, int start_rule, 
int flags,
 #endif
     p->level = 0;
     p->call_invalid_rules = 0;
+    p->input_exhausted = 0;
     p->last_stmt_location.lineno = 0;
     p->last_stmt_location.col_offset = 0;
     p->last_stmt_location.end_lineno = 0;
@@ -1035,16 +1047,12 @@ reset_parser_state_for_error_pass(Parser *p)
         p->tokens[i]->memo_mask = 0;
     }
     p->mark = 0;
+    p->flags &= ~PyPARSE_ALLOW_INCOMPLETE_INPUT;
+    _PyTokenizer_AllowIncompleteInput(p->tok, 0);
     p->call_invalid_rules = 1;
     _PyTokenizer_StopInteractive(p->tok);
 }
 
-static inline int
-_is_end_of_source(Parser *p) {
-    int err = _PyTokenizer_GetInfo(p->tok).status;
-    return err == E_EOF || err == E_EOFS || err == E_EOLS;
-}
-
 static void
 _PyPegen_set_syntax_error_metadata(Parser *p) {
     PyObject *exc = PyErr_GetRaisedException();
@@ -1095,13 +1103,15 @@ _PyPegen_run_parser(Parser *p)
         return NULL;
     }
     if (res == NULL) {
-        if ((p->flags & PyPARSE_ALLOW_INCOMPLETE_INPUT) &&  
_is_end_of_source(p)) {
-            PyErr_Clear();
-            return _PyPegen_raise_error(p, PyExc_IncompleteInputError, 0, 
"incomplete input");
-        }
-        if (PyErr_Occurred() && !PyErr_ExceptionMatches(PyExc_SyntaxError)) {
+        if (PyErr_Occurred() &&
+            (PyErr_ExceptionMatches(PyExc_IncompleteInputError) ||
+             !PyErr_ExceptionMatches(PyExc_SyntaxError))) {
             return NULL;
         }
+        if ((p->flags & PyPARSE_ALLOW_INCOMPLETE_INPUT) &&
+            p->input_exhausted && !PyErr_Occurred()) {
+            return _PyPegen_raise_incomplete_input(p);
+        }
        // Make a second parser pass. In this pass we activate heavier and 
slower checks
         // to produce better error messages and more complete diagnostics. 
Extra "invalid_*"
         // rules will be active during parsing.
@@ -1194,16 +1204,21 @@ _PyPegen_run_parser_from_file_pointer(FILE *fp, int 
start_rule, PyObject *filena
 
 mod_ty
 _PyPegen_run_parser_from_string(const char *str, int start_rule, PyObject 
*filename_ob,
-                       PyCompilerFlags *flags, PyArena *arena, PyObject 
*module)
+                       PyCompilerFlags *flags, PyArena *arena, PyObject 
*module,
+                       PyObject **incomplete)
 {
-    int exec_input = start_rule == Py_file_input;
+    _PyTokenizer_Newline newline_mode = start_rule == Py_file_input
+        ? _PyTokenizer_IMPLY_NEWLINE : _PyTokenizer_KEEP_NEWLINE;
     int parser_flags = compute_parser_flags(flags);
+    if (parser_flags & PyPARSE_ALLOW_INCOMPLETE_INPUT) {
+        newline_mode = _PyTokenizer_APPEND_NEWLINE;
+    }
 
     struct tok_state *tok;
     if (flags != NULL && flags->cf_flags & PyCF_IGNORE_COOKIE) {
-        tok = _PyTokenizer_FromUTF8(str, exec_input, 0);
+        tok = _PyTokenizer_FromUTF8(str, newline_mode, 0);
     } else {
-        tok = _PyTokenizer_FromString(str, exec_input, 0);
+        tok = _PyTokenizer_FromString(str, newline_mode, 0);
     }
     if (tok == NULL) {
         if (PyErr_Occurred()) {
@@ -1231,6 +1246,22 @@ _PyPegen_run_parser_from_string(const char *str, int 
start_rule, PyObject *filen
     }
 
     result = _PyPegen_run_parser(p);
+    if (result != NULL && (parser_flags & PyPARSE_ALLOW_INCOMPLETE_INPUT)) {
+        int needs_newline = _PyTokenizer_GetNewlineRequirements(tok);
+        if ((needs_newline & _PyTokenizer_NEWLINE_CONTINUATION) ||
+            (start_rule != Py_file_input &&
+             (needs_newline & _PyTokenizer_NEWLINE_INDENT)) ||
+            (start_rule == Py_single_input && p->input_exhausted &&
+             (needs_newline & _PyTokenizer_NEWLINE_STATEMENT))) {
+            _PyPegen_raise_incomplete_input(p);
+            if (incomplete != NULL && 
PyErr_ExceptionMatches(PyExc_IncompleteInputError)) {
+                *incomplete = PyErr_GetRaisedException();
+            }
+            else {
+                result = NULL;
+            }
+        }
+    }
     _PyPegen_Parser_Free(p);
 
 error:
diff --git a/Parser/pegen.h b/Parser/pegen.h
index 7454289ef6df03d..44f993101328397 100644
--- a/Parser/pegen.h
+++ b/Parser/pegen.h
@@ -39,6 +39,8 @@ typedef struct _memo {
 typedef struct {
     int type;
     // Text required by grammar actions or helpers; otherwise NULL.
+    // An unfinished string token also has NULL text. Its grammar action
+    // requests more input only if the token is valid at that position.
     PyObject *bytes;
     int level;
     int is_raw;
@@ -86,7 +88,8 @@ typedef struct {
     int n_keyword_lists;
     int start_rule;
     int *errcode;
-    int parsing_started;
+    unsigned char parsing_started;
+    unsigned char input_exhausted;
     PyObject* normalize;
     int starting_lineno;
     int starting_col_offset;
@@ -210,6 +213,7 @@ typedef enum {
 // Error handling functions and APIs
 int _Pypegen_raise_decode_error(Parser *p);
 int _Pypegen_tokenizer_error(Parser *p);
+void *_PyPegen_raise_incomplete_input(Parser *p);
 void *_PyPegen_raise_error(Parser *p, PyObject *errtype, int use_mark, const 
char *errmsg, ...);
 void *_PyPegen_raise_error_known_location(Parser *p, PyObject *errtype,
                                           Py_ssize_t lineno, Py_ssize_t 
col_offset,
@@ -422,7 +426,7 @@ mod_ty _PyPegen_run_parser_from_file_pointer(FILE *, int, 
PyObject *, const char
                                     const char *, const char *, 
PyCompilerFlags *, int *, PyObject **,
                                     PyArena *);
 void *_PyPegen_run_parser(Parser *);
-mod_ty _PyPegen_run_parser_from_string(const char *, int, PyObject *, 
PyCompilerFlags *, PyArena *, PyObject *);
+mod_ty _PyPegen_run_parser_from_string(const char *, int, PyObject *, 
PyCompilerFlags *, PyArena *, PyObject *, PyObject **);
 asdl_stmt_seq *_PyPegen_interactive_exit(Parser *);
 
 // Generated function in parse.c - function definition in python.gram
diff --git a/Parser/pegen_errors.c b/Parser/pegen_errors.c
index 6997a1158e9310f..007397fa0358229 100644
--- a/Parser/pegen_errors.c
+++ b/Parser/pegen_errors.c
@@ -9,6 +9,16 @@
 
 // TOKENIZER ERRORS
 
+void *
+_PyPegen_raise_incomplete_input(Parser *p)
+{
+    if ((p->flags & PyPARSE_ALLOW_INCOMPLETE_INPUT) && !PyErr_Occurred()) {
+        return _PyPegen_raise_error(p, PyExc_IncompleteInputError, 0,
+                                    "incomplete input");
+    }
+    return NULL;
+}
+
 static inline void
 raise_unclosed_parentheses_error(Parser *p) {
        _PyTokenizer_Info info = _PyTokenizer_GetInfo(p->tok);
@@ -32,7 +42,17 @@ _Pypegen_tokenizer_error(Parser *p)
     Py_ssize_t col_offset = -1;
     p->error_indicator = 1;
     _PyTokenizer_Info info = _PyTokenizer_GetInfo(p->tok);
+    if ((p->flags & PyPARSE_ALLOW_INCOMPLETE_INPUT) &&
+        (info.status == E_EOF || info.status == E_INCOMPLETE))
+    {
+        _PyPegen_raise_incomplete_input(p);
+        return -1;
+    }
     switch (info.status) {
+        case E_INCOMPLETE:
+            // The diagnostic pass rejected this unfinished token. Let the
+            // parser report the syntax error at its original failure point.
+            return -1;
         case E_TOKEN:
             msg = "invalid token";
             break;
@@ -298,6 +318,12 @@ _PyPegen_raise_error_known_location(Parser *p, PyObject 
*errtype,
             Py_ssize_t size;
             const char *line = _PyTokenizer_SpanView(
                 p->tok, text_span, &size);
+            if (errtype == PyExc_IncompleteInputError && size > 0 &&
+                (info.implicit_newline == _PyTokenizer_APPEND_NEWLINE ||
+                 (info.implicit_newline == _PyTokenizer_IMPLY_NEWLINE &&
+                  p->start_rule != Py_file_input))) {
+                size--;
+            }
             error_line = PyUnicode_DecodeUTF8(line, size, "replace");
         }
         else if (!info.is_file) {
diff --git a/Parser/tokenizer/api.c b/Parser/tokenizer/api.c
index d955c6b335e1db6..99f0e4e0ddc1289 100644
--- a/Parser/tokenizer/api.c
+++ b/Parser/tokenizer/api.c
@@ -7,11 +7,24 @@
 #include "reader_internal.h"
 #include "../lexer/state.h"
 
+int
+_PyTokenizer_GetStatus(const struct tok_state *tok)
+{
+    return tok->done;
+}
+
+int
+_PyTokenizer_GetNewlineRequirements(const struct tok_state *tok)
+{
+    return tok->needs_newline;
+}
+
 _PyTokenizer_Info
 _PyTokenizer_GetInfo(const struct tok_state *tok)
 {
     _PyTokenizer_Info info = {
         .status = tok->done,
+        .implicit_newline = tok->implicit_newline,
         .diagnostic = tok->diagnostic,
         .location = {tok->lineno, tok->line_start < 0
             ? -1 : (int)(tok->cur - tok->line_start)},
@@ -103,6 +116,12 @@ _PyTokenizer_ImplyDedents(struct tok_state *tok)
     _PyLexer_ImplyDedents(tok);
 }
 
+void
+_PyTokenizer_AllowIncompleteInput(struct tok_state *tok, int allow)
+{
+    tok->allow_incomplete_input = allow != 0;
+}
+
 int
 _PyTokenizer_HasTrailingStatement(const struct tok_state *tok)
 {
diff --git a/Parser/tokenizer/decoder.c b/Parser/tokenizer/decoder.c
index 48f906845651aab..6388b71e635cc3f 100644
--- a/Parser/tokenizer/decoder.c
+++ b/Parser/tokenizer/decoder.c
@@ -14,7 +14,6 @@
 #define UTF16_BOM_LENGTH ((Py_ssize_t)(sizeof(UTF16_LE_BOM) - 1))
 #define UTF8_BOM "\xef\xbb\xbf"
 #define UTF8_BOM_LENGTH ((Py_ssize_t)(sizeof(UTF8_BOM) - 1))
-#define NORMALIZED_LINE_INITIAL_CAPACITY 256
 
 char *
 _PyTok_CopyBytes(const char *data, Py_ssize_t len)
@@ -284,9 +283,6 @@ store_prepared_source(struct tok_state *tok, const char 
*data, Py_ssize_t len,
                       int preserve_crlf, int add_final_newline)
 {
     Py_ssize_t pos = 0;
-    // SourceAppendLine copies the bytes, so reuse this buffer for each line.
-    char *normalized = NULL;
-    Py_ssize_t capacity = 0;
     while (pos < len) {
         Py_ssize_t raw_line_len;
         if (preserve_crlf) {
@@ -304,52 +300,45 @@ store_prepared_source(struct tok_state *tok, const char 
*data, Py_ssize_t len,
         int add_newline = add_final_newline &&
             pos + raw_line_len == len && !terminated;
         const char *line = data + pos;
-        Py_ssize_t line_len = raw_line_len;
         // raw_line_length stops at the first CR or LF, so any CR is in
         // the line terminator.
         Py_ssize_t terminator_len = 0;
         if (!preserve_crlf) {
-            if (line[line_len - 1] == '\r') {
+            if (line[raw_line_len - 1] == '\r') {
                 terminator_len = 1;
             }
-            else if (line_len > 1 && line[line_len - 2] == '\r') {
+            else if (raw_line_len > 1 && line[raw_line_len - 2] == '\r') {
                 terminator_len = 2;
             }
         }
+        _PyTok_Off appended;
         if (terminator_len || add_newline) {
-            Py_ssize_t payload_len = line_len - terminator_len;
-            if (payload_len > PY_SSIZE_T_MAX - 2) {
-                PyErr_NoMemory();
-                tok->done = E_NOMEM;
-                goto error;
-            }
-            Py_ssize_t needed = payload_len + 2;
-            if (_PyTok_ReserveBuffer(&normalized, &capacity, needed,
-                                    NORMALIZED_LINE_INITIAL_CAPACITY) < 0) {
-                tok->done = E_NOMEM;
-                goto error;
-            }
-            memcpy(normalized, line, payload_len);
-            normalized[payload_len] = '\n';
-            line_len = payload_len + 1;
-            normalized[line_len] = '\0';
-            line = normalized;
+            appended = _PyTok_SourceAppendLineWithNewline(
+                &tok->source, line, raw_line_len - terminator_len);
+        }
+        else {
+            appended = _PyTok_SourceAppendLine(&tok->source, line, 
raw_line_len);
         }
-        _PyTok_Off appended = _PyTok_SourceAppendLine(
-            &tok->source, line, line_len);
         if (appended < 0) {
             tok->done = PyErr_ExceptionMatches(PyExc_MemoryError)
                 ? E_NOMEM : E_ERROR;
-            goto error;
+            return -1;
         }
-        tok->reader->prepared_final_newline_is_implicit = add_newline;
+        tok->reader->prepared_final_newline = add_newline;
         pos += raw_line_len;
     }
-    PyMem_Free(normalized);
+    // Partial input includes the newline submitted by the next Enter key.
+    // A final CR pairs with that newline instead of starting a blank line.
+    if (add_final_newline == _PyTokenizer_APPEND_NEWLINE &&
+        !tok->reader->prepared_final_newline &&
+        (len == 0 || data[len - 1] != '\r'))
+    {
+        if (_PyTok_SourceAppendLine(&tok->source, "\n", 1) < 0) {
+            return -1;
+        }
+        tok->reader->prepared_final_newline = _PyTokenizer_APPEND_NEWLINE;
+    }
     return 0;
-error:
-    PyMem_Free(normalized);
-    return -1;
 }
 
 int
diff --git a/Parser/tokenizer/reader.c b/Parser/tokenizer/reader.c
index ce2bb1cf5c4bc30..86cec96bdcc1249 100644
--- a/Parser/tokenizer/reader.c
+++ b/Parser/tokenizer/reader.c
@@ -180,8 +180,8 @@ next_prepared(struct tok_state *tok, _PyTok_Chunk *chunk)
     chunk->data = (char *)_PyTok_SourceSpanView(
         &tok->source, line, &chunk->len);
     chunk->ownership = _PYTOK_CHUNK_BORROWED;
-    chunk->implicit_newline = line.end == tail.end &&
-        tok->reader->prepared_final_newline_is_implicit;
+    chunk->implicit_newline = line.end == tail.end
+        ? tok->reader->prepared_final_newline : 0;
     return _PYTOK_READ_LINE;
 }
 
diff --git a/Parser/tokenizer/reader_internal.h 
b/Parser/tokenizer/reader_internal.h
index a7a68269fffaa4a..c080af3c05eef52 100644
--- a/Parser/tokenizer/reader_internal.h
+++ b/Parser/tokenizer/reader_internal.h
@@ -58,7 +58,7 @@ typedef struct _PyTok_Reader {
     Py_ssize_t decoded_len;
     Py_ssize_t decoded_cap;
     _PyTok_ReaderKind kind;
-    unsigned char prepared_final_newline_is_implicit;
+    unsigned char prepared_final_newline;  // _PyTokenizer_Newline
     unsigned char decoded_tail_is_implicit;
     unsigned char file_initialized;
     unsigned char file_eof;
diff --git a/Parser/tokenizer/source.c b/Parser/tokenizer/source.c
index ebb7b05507c56ec..8688ef0f06e8302 100644
--- a/Parser/tokenizer/source.c
+++ b/Parser/tokenizer/source.c
@@ -71,51 +71,68 @@ reserve_bytes(_PyTok_SourceText *source, Py_ssize_t needed)
 
 static int
 validate_line(const _PyTok_SourceText *source, const char *bytes,
-              Py_ssize_t len)
+              Py_ssize_t len, int append_newline)
 {
-    if (len <= 0 || bytes == NULL ||
+    if (len < 0 || (len == 0 && !append_newline) || bytes == NULL ||
             (source->nlines > 0 &&
              (source->len == 0 || source->bytes[source->len - 1] != '\n'))) {
         PyErr_SetString(PyExc_SystemError, "invalid tokenizer source line");
         return -1;
     }
     const char *newline = memchr(bytes, '\n', len);
-    if (newline != NULL && newline != bytes + len - 1) {
+    if (newline != NULL && (append_newline || newline != bytes + len - 1)) {
         PyErr_SetString(PyExc_SystemError, "invalid tokenizer source line");
         return -1;
     }
     if (source->nlines == INT_MAX ||
-            (source->nlines == INT_MAX - 1 && newline != NULL)) {
+            (source->nlines == INT_MAX - 1 && (newline != NULL || 
append_newline))) {
         PyErr_SetString(PyExc_OverflowError, "too many tokenizer source 
lines");
         return -1;
     }
     return 0;
 }
 
-_PyTok_Off
-_PyTok_SourceAppendLine(_PyTok_SourceText *source, const char *bytes,
-                        Py_ssize_t len)
+static _PyTok_Off
+append_line(_PyTok_SourceText *source, const char *bytes,
+            Py_ssize_t len, int append_newline)
 {
-    if (validate_line(source, bytes, len) < 0) {
+    if (validate_line(source, bytes, len, append_newline) < 0) {
         return -1;
     }
-    if (source->len > PY_SSIZE_T_MAX - len - 1 ||
-            source->base_offset > PY_SSIZE_T_MAX - source->len - len) {
+    if (source->len > PY_SSIZE_T_MAX - len - append_newline - 1 ||
+            source->base_offset > PY_SSIZE_T_MAX - source->len - len - 
append_newline) {
         PyErr_NoMemory();
         return -1;
     }
-    if (reserve_bytes(source, source->len + len + 1) < 0) {
+    if (reserve_bytes(source, source->len + len + append_newline + 1) < 0) {
         return -1;
     }
 
     _PyTok_Off start = source->len;
     memcpy(source->bytes + start, bytes, len);
     source->len += len;
+    if (append_newline) {
+        source->bytes[source->len++] = '\n';
+    }
     source->bytes[source->len] = '\0';
     source->nlines++;
     return source->base_offset + start;
 }
 
+_PyTok_Off
+_PyTok_SourceAppendLine(_PyTok_SourceText *source, const char *bytes,
+                        Py_ssize_t len)
+{
+    return append_line(source, bytes, len, 0);
+}
+
+_PyTok_Off
+_PyTok_SourceAppendLineWithNewline(_PyTok_SourceText *source, const char 
*bytes,
+                                   Py_ssize_t len)
+{
+    return append_line(source, bytes, len, 1);
+}
+
 const char *
 _PyTok_SourceLineView(const _PyTok_SourceText *source, Py_ssize_t lineno,
                       Py_ssize_t *len)
diff --git a/Parser/tokenizer/source.h b/Parser/tokenizer/source.h
index 0fd9fe1877bed2b..d2c202830479523 100644
--- a/Parser/tokenizer/source.h
+++ b/Parser/tokenizer/source.h
@@ -70,6 +70,10 @@ PyAPI_FUNC(void) _PyTok_SourceDiscard(_PyTok_SourceText *);
    final line. The input must not point into source storage. */
 PyAPI_FUNC(_PyTok_Off) _PyTok_SourceAppendLine(
     _PyTok_SourceText *source, const char *bytes, Py_ssize_t len);
+/* Append a line payload followed by '\n'. The payload may be empty and must
+   contain no newline. Other SourceAppendLine constraints still apply. */
+PyAPI_FUNC(_PyTok_Off) _PyTok_SourceAppendLineWithNewline(
+    _PyTok_SourceText *source, const char *bytes, Py_ssize_t len);
 /* Return borrowed bytes excluding '\n', writing the byte length to *len.
    Line numbers are 1-based and clamp to the first or final line; a trailing
    '\n' adds an empty final line. The view need not be NUL-terminated.
diff --git a/Parser/tokenizer/tokenizer.h b/Parser/tokenizer/tokenizer.h
index bc4009700fdc967..149bca06b8b7099 100644
--- a/Parser/tokenizer/tokenizer.h
+++ b/Parser/tokenizer/tokenizer.h
@@ -6,6 +6,19 @@
 
 struct tok_state;
 
+typedef enum {
+    _PyTokenizer_KEEP_NEWLINE,   // Preserve the original line terminator.
+    _PyTokenizer_IMPLY_NEWLINE,  // Add a missing line terminator.
+    _PyTokenizer_APPEND_NEWLINE, // Also append a blank line to terminated 
input.
+} _PyTokenizer_Newline;
+
+/* Reasons that the appended newline can change the token stream. */
+enum {
+    _PyTokenizer_NEWLINE_STATEMENT = 1,
+    _PyTokenizer_NEWLINE_INDENT = 2,
+    _PyTokenizer_NEWLINE_CONTINUATION = 4,
+};
+
 /* Initialize before use. metadata owns a reference released by _PyToken_Free;
    a consumer taking that reference must set metadata to NULL. */
 struct token {
@@ -37,6 +50,7 @@ typedef struct {
 
 typedef struct {
     int status;
+    unsigned char implicit_newline;  // _PyTokenizer_Newline
     _PyTokenizer_Diagnostic diagnostic;
     _PyTok_Loc location;
     _PyTok_Off cursor;
@@ -54,7 +68,9 @@ typedef struct {
 } _PyTokenizer_Info;
 
 /* Get replaces the initialized token, releasing its previous metadata.
-   Errors are returned as ERRORTOKEN, with or without a Python exception. */
+   Errors are returned as ERRORTOKEN, with or without a Python exception.
+   In incomplete-input mode, an unfinished string retains its token type
+   with an absent span and E_INCOMPLETE status. No Python exception is set. */
 void _PyTokenizer_Get(struct tok_state *, struct token *);
 void _PyTokenizer_Free(struct tok_state *);
 int _PyTokenizer_Traverse(struct tok_state *, visitproc, void *);
@@ -69,6 +85,8 @@ _PyToken_Free(struct token *token)
 /* Views and borrowed snapshot references remain valid until the tokenizer is
    mutated or freed. Source spans may be discarded when reading more input. */
 _PyTokenizer_Info _PyTokenizer_GetInfo(const struct tok_state *);
+int _PyTokenizer_GetStatus(const struct tok_state *);
+int _PyTokenizer_GetNewlineRequirements(const struct tok_state *);
 /* An absent token span has a nonnull empty text view. */
 const char *_PyToken_TextView(
     const struct tok_state *, const struct token *, Py_ssize_t *);
@@ -91,6 +109,7 @@ void _PyTokenizer_SetContext(
     struct tok_state *tok, PyObject *filename, PyObject *module);
 void _PyTokenizer_SetOptions(
     struct tok_state *tok, int extra_tokens, int type_comments);
+void _PyTokenizer_AllowIncompleteInput(struct tok_state *, int);
 void _PyTokenizer_ImplyDedents(struct tok_state *);
 int _PyTokenizer_HasTrailingStatement(const struct tok_state *);
 int _PyTokenizer_IsInteractive(const struct tok_state *);
diff --git a/Python/pythonrun.c b/Python/pythonrun.c
index 049eaea4994ac46..5e026a93d5c1806 100644
--- a/Python/pythonrun.c
+++ b/Python/pythonrun.c
@@ -1285,7 +1285,7 @@ _PyRun_String(const char *str, PyObject* name, int start,
         goto done;
     }
 
-    mod = _PyParser_ASTFromString(str, name, start, flags, arena, module);
+    mod = _PyParser_ASTFromString(str, name, start, flags, arena, module, 
NULL);
     Py_XDECREF(module);
 
     if (mod != NULL) {
@@ -1569,32 +1569,43 @@ _Py_CompileString(const char *str, PyObject *filename, 
int start,
         return NULL;
     }
 
-    PyCodeObject *co;
-    mod_ty mod;
     PyArena *arena = _PyArena_New();
     if (arena == NULL)
         return NULL;
 
-    mod = _PyParser_ASTFromString(str, filename, start, flags, arena, module);
+    PyObject *result = NULL;
+    PyObject *incomplete = NULL;
+    mod_ty mod = _PyParser_ASTFromString(str, filename, start, flags, arena,
+                                        module, &incomplete);
     if (mod == NULL) {
-        _PyArena_Free(arena);
-        return NULL;
+        goto done;
     }
     if (flags && (flags->cf_flags & PyCF_ONLY_AST)) {
         int syntax_check_only = ((flags->cf_flags & PyCF_OPTIMIZED_AST) == 
PyCF_ONLY_AST); /* unoptiomized AST */
         if (_PyCompile_AstPreprocess(mod, filename, flags, optimize, arena,
                                      syntax_check_only, module) < 0)
         {
-            _PyArena_Free(arena);
-            return NULL;
+            goto done;
         }
-        PyObject *result = PyAST_mod2obj(mod);
+        if (incomplete != NULL) {
+            goto done;
+        }
+        result = PyAST_mod2obj(mod);
         _PyArena_Free(arena);
         return result;
     }
-    co = _PyAST_Compile(mod, filename, flags, optimize, arena, module);
+    result = (PyObject *)_PyAST_Compile(mod, filename, flags, optimize, arena, 
module);
+
+done:
     _PyArena_Free(arena);
-    return (PyObject *)co;
+    if (incomplete != NULL && !PyErr_Occurred()) {
+        Py_CLEAR(result);
+        PyErr_SetRaisedException(incomplete);
+    }
+    else {
+        Py_XDECREF(incomplete);
+    }
+    return result;
 }
 
 PyObject *
diff --git a/Python/symtable.c b/Python/symtable.c
index b97778ada20c04b..7f945dad1984ed9 100644
--- a/Python/symtable.c
+++ b/Python/symtable.c
@@ -3271,7 +3271,7 @@ _Py_SymtableStringObjectFlags(const char *str, PyObject 
*filename,
     if (arena == NULL)
         return NULL;
 
-    mod = _PyParser_ASTFromString(str, filename, start, flags, arena, module);
+    mod = _PyParser_ASTFromString(str, filename, start, flags, arena, module, 
NULL);
     if (mod == NULL) {
         _PyArena_Free(arena);
         return NULL;
diff --git a/Tools/peg_generator/peg_extension/peg_extension.c 
b/Tools/peg_generator/peg_extension/peg_extension.c
index 2fec5b0512940f3..83dd668a06987eb 100644
--- a/Tools/peg_generator/peg_extension/peg_extension.c
+++ b/Tools/peg_generator/peg_extension/peg_extension.c
@@ -93,7 +93,7 @@ parse_string(PyObject *self, PyObject *args, PyObject *kwds)
 
     PyCompilerFlags flags = _PyCompilerFlags_INIT;
     mod_ty res = _PyPegen_run_parser_from_string(the_string, Py_file_input, 
filename_ob,
-                                        &flags, arena, NULL);
+                                        &flags, arena, NULL, NULL);
     if (res == NULL) {
         goto error;
     }

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to