From 65ca012d46beac45d9612b8bccd1d02f3e4fc82e Mon Sep 17 00:00:00 2001 From: Aneesh Durg Date: Sun, 27 Apr 2025 22:42:12 -0500 Subject: [PATCH 01/14] continue accepting REPL input for multiline strings --- compiler/src/lib.rs | 2 ++ src/shell.rs | 35 ++++++++++++++++++++++++++++++++++- vm/src/stdlib/ast.rs | 1 + 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/compiler/src/lib.rs b/compiler/src/lib.rs index 390a2d5669..7647e19348 100644 --- a/compiler/src/lib.rs +++ b/compiler/src/lib.rs @@ -25,6 +25,7 @@ pub enum CompileErrorType { pub struct ParseError { #[source] pub error: parser::ParseErrorType, + pub raw_location: ruff_text_size::TextRange, pub location: SourceLocation, pub source_path: String, } @@ -48,6 +49,7 @@ impl CompileError { let location = source_code.source_location(error.location.start()); Self::Parse(ParseError { error: error.error, + raw_location: error.location, location, source_path: source_code.path.to_owned(), }) diff --git a/src/shell.rs b/src/shell.rs index cbe2c9efe0..b530dc9830 100644 --- a/src/shell.rs +++ b/src/shell.rs @@ -1,7 +1,8 @@ mod helper; use rustpython_compiler::{ - CompileError, ParseError, parser::LexicalErrorType, parser::ParseErrorType, + CompileError, ParseError, parser::FStringErrorType, parser::LexicalErrorType, + parser::ParseErrorType, }; use rustpython_vm::{ AsObject, PyResult, VirtualMachine, @@ -24,6 +25,12 @@ fn shell_exec( empty_line_given: bool, continuing: bool, ) -> ShellExecResult { + // compiling expects only UNIX style line endings, and will replace windows line endings + // internally. Since we might need to analyze the source to determine if an error could be + // resolved by future input, we need the location from the error to match the source code that + // was actually compiled. + #[cfg(windows)] + let source = &source.replace("\r\n", "\n"); match vm.compile(source, compiler::Mode::Single, "".to_owned()) { Ok(code) => { if empty_line_given || !continuing { @@ -41,7 +48,33 @@ fn shell_exec( error: ParseErrorType::Lexical(LexicalErrorType::Eof), .. })) => ShellExecResult::Continue, + Err(CompileError::Parse(ParseError { + error: + ParseErrorType::Lexical(LexicalErrorType::FStringError( + FStringErrorType::UnterminatedTripleQuotedString, + )), + .. + })) => ShellExecResult::Continue, Err(err) => { + // Check if the error is from an unclosed triple quoted string (which should always + // continue) + match err { + CompileError::Parse(ParseError { + error: ParseErrorType::Lexical(LexicalErrorType::UnclosedStringError), + raw_location, + .. + }) => { + let loc = raw_location.start().to_usize(); + let mut iter = source.chars(); + if let Some(quote) = iter.nth(loc) { + if iter.next() == Some(quote) && iter.next() == Some(quote) { + return ShellExecResult::Continue; + } + } + } + _ => (), + }; + // bad_error == true if we are handling an error that should be thrown even if we are continuing // if its an indentation error, set to true if we are continuing and the error is on column 0, // since indentations errors on columns other than 0 should be ignored. diff --git a/vm/src/stdlib/ast.rs b/vm/src/stdlib/ast.rs index 13341c1b1e..df96196761 100644 --- a/vm/src/stdlib/ast.rs +++ b/vm/src/stdlib/ast.rs @@ -245,6 +245,7 @@ pub(crate) fn parse( let top = parser::parse(source, mode.into()) .map_err(|parse_error| ParseError { error: parse_error.error, + raw_location: parse_error.location, location: text_range_to_source_range(&source_code, parse_error.location) .start .to_source_location(), From 1fc8c50e6e9d4c091375cf3ea23c97df4e2a58f7 Mon Sep 17 00:00:00 2001 From: Aneesh Durg Date: Sun, 27 Apr 2025 22:54:43 -0500 Subject: [PATCH 02/14] Match cpython behavior for all multi-line statements (execute when complete) --- src/shell.rs | 71 +++++++++++++++++++++++++++++++++------------------- 1 file changed, 45 insertions(+), 26 deletions(-) diff --git a/src/shell.rs b/src/shell.rs index b530dc9830..d8a92c62a8 100644 --- a/src/shell.rs +++ b/src/shell.rs @@ -15,7 +15,8 @@ use rustpython_vm::{ enum ShellExecResult { Ok, PyErr(PyBaseExceptionRef), - Continue, + ContinueBlock, + ContinueLine, } fn shell_exec( @@ -23,7 +24,7 @@ fn shell_exec( source: &str, scope: Scope, empty_line_given: bool, - continuing: bool, + continuing_block: bool, ) -> ShellExecResult { // compiling expects only UNIX style line endings, and will replace windows line endings // internally. Since we might need to analyze the source to determine if an error could be @@ -33,7 +34,7 @@ fn shell_exec( let source = &source.replace("\r\n", "\n"); match vm.compile(source, compiler::Mode::Single, "".to_owned()) { Ok(code) => { - if empty_line_given || !continuing { + if empty_line_given || !continuing_block { // We want to execute the full code match vm.run_code_obj(code, scope) { Ok(_val) => ShellExecResult::Ok, @@ -47,14 +48,14 @@ fn shell_exec( Err(CompileError::Parse(ParseError { error: ParseErrorType::Lexical(LexicalErrorType::Eof), .. - })) => ShellExecResult::Continue, + })) => ShellExecResult::ContinueLine, Err(CompileError::Parse(ParseError { error: ParseErrorType::Lexical(LexicalErrorType::FStringError( FStringErrorType::UnterminatedTripleQuotedString, )), .. - })) => ShellExecResult::Continue, + })) => ShellExecResult::ContinueLine, Err(err) => { // Check if the error is from an unclosed triple quoted string (which should always // continue) @@ -68,7 +69,7 @@ fn shell_exec( let mut iter = source.chars(); if let Some(quote) = iter.nth(loc) { if iter.next() == Some(quote) && iter.next() == Some(quote) { - return ShellExecResult::Continue; + return ShellExecResult::ContinueLine; } } } @@ -83,10 +84,12 @@ fn shell_exec( let bad_error = match err { CompileError::Parse(ref p) => { match &p.error { - ParseErrorType::Lexical(LexicalErrorType::IndentationError) => continuing, // && p.location.is_some() + ParseErrorType::Lexical(LexicalErrorType::IndentationError) => { + continuing_block + } // && p.location.is_some() ParseErrorType::OtherError(msg) => { if msg.starts_with("Expected an indented block") { - continuing + continuing_block } else { true } @@ -101,7 +104,7 @@ fn shell_exec( if empty_line_given || bad_error { ShellExecResult::PyErr(vm.new_syntax_error(&err, Some(source))) } else { - ShellExecResult::Continue + ShellExecResult::ContinueBlock } } } @@ -126,10 +129,19 @@ pub fn run_shell(vm: &VirtualMachine, scope: Scope) -> PyResult<()> { println!("No previous history."); } - let mut continuing = false; + // We might either be waiting to know if a block is complete, or waiting to know if a multiline + // statement is complete. In the former case, we need to ensure that we read one extra new line + // to know that the block is complete. In the latter, we can execute as soon as the statement is + // valid. + let mut continuing_block = false; + let mut continuing_line = false; loop { - let prompt_name = if continuing { "ps2" } else { "ps1" }; + let prompt_name = if continuing_block || continuing_line { + "ps2" + } else { + "ps1" + }; let prompt = vm .sys_module .get_attr(prompt_name, vm) @@ -138,6 +150,8 @@ pub fn run_shell(vm: &VirtualMachine, scope: Scope) -> PyResult<()> { Ok(ref s) => s.as_str(), Err(_) => "", }; + + continuing_line = false; let result = match repl.readline(prompt) { ReadlineResult::Line(line) => { debug!("You entered {:?}", line); @@ -153,39 +167,44 @@ pub fn run_shell(vm: &VirtualMachine, scope: Scope) -> PyResult<()> { } full_input.push('\n'); - match shell_exec(vm, &full_input, scope.clone(), empty_line_given, continuing) { + match shell_exec( + vm, + &full_input, + scope.clone(), + empty_line_given, + continuing_block, + ) { ShellExecResult::Ok => { - if continuing { + if continuing_block { if empty_line_given { - // We should be exiting continue mode - continuing = false; + // We should exit continue mode since the block succesfully executed + continuing_block = false; full_input.clear(); - Ok(()) - } else { - // We should stay in continue mode - continuing = true; - Ok(()) } } else { // We aren't in continue mode so proceed normally - continuing = false; full_input.clear(); - Ok(()) } + Ok(()) + } + // Continue, but don't change the mode + ShellExecResult::ContinueLine => { + continuing_line = true; + Ok(()) } - ShellExecResult::Continue => { - continuing = true; + ShellExecResult::ContinueBlock => { + continuing_block = true; Ok(()) } ShellExecResult::PyErr(err) => { - continuing = false; + continuing_block = false; full_input.clear(); Err(err) } } } ReadlineResult::Interrupt => { - continuing = false; + continuing_block = false; full_input.clear(); let keyboard_interrupt = vm.new_exception_empty(vm.ctx.exceptions.keyboard_interrupt.to_owned()); From b8e6e753f863c339ef8c4a9ad45a7d3aebf62263 Mon Sep 17 00:00:00 2001 From: Aneesh Durg Date: Mon, 28 Apr 2025 09:58:56 -0500 Subject: [PATCH 03/14] Emit _IncompleteInputError when compiling with incomplete flag --- Lib/codeop.py | 2 ++ vm/src/compiler.rs | 7 +++++++ vm/src/exceptions.rs | 27 +++++++++++++++++++++++++-- vm/src/stdlib/ast.rs | 4 ++-- vm/src/stdlib/builtins.rs | 10 ++++++++-- vm/src/vm/vm_new.rs | 26 +++++++++++++++++++++++++- 6 files changed, 69 insertions(+), 7 deletions(-) diff --git a/Lib/codeop.py b/Lib/codeop.py index 96868047cb..4bbac5cef0 100644 --- a/Lib/codeop.py +++ b/Lib/codeop.py @@ -69,6 +69,8 @@ def _maybe_compile(compiler, source, filename, symbol): # XXX: RustPython; support multiline definitions in REPL # See also: https://github.com/RustPython/RustPython/pull/5743 strerr = str(e) + if isinstance(e, _IncompleteInputError): + return None if source.endswith(":") and "expected an indented block" in strerr: return None elif "incomplete input" in str(e): diff --git a/vm/src/compiler.rs b/vm/src/compiler.rs index b819fd9a42..84fea452d1 100644 --- a/vm/src/compiler.rs +++ b/vm/src/compiler.rs @@ -49,3 +49,10 @@ impl crate::convert::ToPyException for (CompileError, Option<&str>) { vm.new_syntax_error(&self.0, self.1) } } + +#[cfg(any(feature = "parser", feature = "codegen"))] +impl crate::convert::ToPyException for (CompileError, Option<&str>, bool) { + fn to_pyexception(&self, vm: &crate::VirtualMachine) -> crate::builtins::PyBaseExceptionRef { + vm.new_syntax_error_maybe_incomplete(&self.0, self.1, self.2) + } +} diff --git a/vm/src/exceptions.rs b/vm/src/exceptions.rs index 6c4f97fe38..ee503d81d3 100644 --- a/vm/src/exceptions.rs +++ b/vm/src/exceptions.rs @@ -495,6 +495,7 @@ pub struct ExceptionZoo { pub not_implemented_error: &'static Py, pub recursion_error: &'static Py, pub syntax_error: &'static Py, + pub incomplete_input_error: &'static Py, pub indentation_error: &'static Py, pub tab_error: &'static Py, pub system_error: &'static Py, @@ -743,6 +744,7 @@ impl ExceptionZoo { let recursion_error = PyRecursionError::init_builtin_type(); let syntax_error = PySyntaxError::init_builtin_type(); + let incomplete_input_error = PyIncompleteInputError::init_builtin_type(); let indentation_error = PyIndentationError::init_builtin_type(); let tab_error = PyTabError::init_builtin_type(); @@ -817,6 +819,7 @@ impl ExceptionZoo { not_implemented_error, recursion_error, syntax_error, + incomplete_input_error, indentation_error, tab_error, system_error, @@ -965,6 +968,7 @@ impl ExceptionZoo { "end_offset" => ctx.none(), "text" => ctx.none(), }); + extend_exception!(PyIncompleteInputError, ctx, excs.incomplete_input_error); extend_exception!(PyIndentationError, ctx, excs.indentation_error); extend_exception!(PyTabError, ctx, excs.tab_error); @@ -1623,11 +1627,30 @@ pub(super) mod types { } } - #[pyexception(name, base = "PySyntaxError", ctx = "indentation_error", impl)] + #[pyexception(name, base = "PySyntaxError", ctx = "incomplete_input_error")] + #[derive(Debug)] + pub struct PyIncompleteInputError {} + + #[pyexception] + impl PyIncompleteInputError { + #[pyslot] + #[pymethod(name = "__init__")] + pub(crate) fn slot_init( + zelf: PyObjectRef, + _args: FuncArgs, + vm: &VirtualMachine, + ) -> PyResult<()> { + zelf.set_attr("name", vm.ctx.new_str("SyntaxError"), vm)?; + Ok(()) + } + + } + + #[pyexception(name, base = "PyIncompleteInputError", ctx = "indentation_error", impl)] #[derive(Debug)] pub struct PyIndentationError {} - #[pyexception(name, base = "PyIndentationError", ctx = "tab_error", impl)] + #[pyexception(name, base = "PyIncompleteInputError", ctx = "tab_error", impl)] #[derive(Debug)] pub struct PyTabError {} diff --git a/vm/src/stdlib/ast.rs b/vm/src/stdlib/ast.rs index df96196761..95a1162f5b 100644 --- a/vm/src/stdlib/ast.rs +++ b/vm/src/stdlib/ast.rs @@ -296,8 +296,8 @@ pub const PY_COMPILE_FLAG_AST_ONLY: i32 = 0x0400; // The following flags match the values from Include/cpython/compile.h // Caveat emptor: These flags are undocumented on purpose and depending // on their effect outside the standard library is **unsupported**. -const PY_CF_DONT_IMPLY_DEDENT: i32 = 0x200; -const PY_CF_ALLOW_INCOMPLETE_INPUT: i32 = 0x4000; +pub const PY_CF_DONT_IMPLY_DEDENT: i32 = 0x200; +pub const PY_CF_ALLOW_INCOMPLETE_INPUT: i32 = 0x4000; // __future__ flags - sync with Lib/__future__.py // TODO: These flags aren't being used in rust code diff --git a/vm/src/stdlib/builtins.rs b/vm/src/stdlib/builtins.rs index 9a21dd34dd..60f6dd8dd9 100644 --- a/vm/src/stdlib/builtins.rs +++ b/vm/src/stdlib/builtins.rs @@ -186,6 +186,8 @@ mod builtins { return Err(vm.new_value_error("compile() unrecognized flags".to_owned())); } + let allow_incomplete = !(flags & ast::PY_CF_ALLOW_INCOMPLETE_INPUT).is_zero(); + if (flags & ast::PY_COMPILE_FLAG_AST_ONLY).is_zero() { #[cfg(not(feature = "compiler"))] { @@ -207,14 +209,17 @@ mod builtins { args.filename.to_string_lossy().into_owned(), opts, ) - .map_err(|err| (err, Some(source)).to_pyexception(vm))?; + .map_err(|err| { + (err, Some(source), allow_incomplete).to_pyexception(vm) + })?; Ok(code.into()) } } else { let mode = mode_str .parse::() .map_err(|err| vm.new_value_error(err.to_string()))?; - ast::parse(vm, source, mode).map_err(|e| (e, Some(source)).to_pyexception(vm)) + ast::parse(vm, source, mode) + .map_err(|e| (e, Some(source), allow_incomplete).to_pyexception(vm)) } } } @@ -1056,6 +1061,7 @@ pub fn init_module(vm: &VirtualMachine, module: &Py) { "NotImplementedError" => ctx.exceptions.not_implemented_error.to_owned(), "RecursionError" => ctx.exceptions.recursion_error.to_owned(), "SyntaxError" => ctx.exceptions.syntax_error.to_owned(), + "_IncompleteInputError" => ctx.exceptions.incomplete_input_error.to_owned(), "IndentationError" => ctx.exceptions.indentation_error.to_owned(), "TabError" => ctx.exceptions.tab_error.to_owned(), "SystemError" => ctx.exceptions.system_error.to_owned(), diff --git a/vm/src/vm/vm_new.rs b/vm/src/vm/vm_new.rs index 9a7a7fe748..c2032cdd02 100644 --- a/vm/src/vm/vm_new.rs +++ b/vm/src/vm/vm_new.rs @@ -320,10 +320,11 @@ impl VirtualMachine { } #[cfg(any(feature = "parser", feature = "compiler"))] - pub fn new_syntax_error( + pub fn new_syntax_error_maybe_incomplete( &self, error: &crate::compiler::CompileError, source: Option<&str>, + allow_incomplete: bool, ) -> PyBaseExceptionRef { use crate::source::SourceLocation; @@ -343,6 +344,20 @@ impl VirtualMachine { .. }) => self.ctx.exceptions.indentation_error, #[cfg(feature = "parser")] + crate::compiler::CompileError::Parse(rustpython_compiler::ParseError { + error: + ruff_python_parser::ParseErrorType::Lexical( + ruff_python_parser::LexicalErrorType::Eof, + ), + .. + }) => { + if allow_incomplete { + self.ctx.exceptions.incomplete_input_error + } else { + self.ctx.exceptions.syntax_error + } + } + #[cfg(feature = "parser")] crate::compiler::CompileError::Parse(rustpython_compiler::ParseError { error: ruff_python_parser::ParseErrorType::OtherError(s), .. @@ -410,6 +425,15 @@ impl VirtualMachine { syntax_error } + #[cfg(any(feature = "parser", feature = "compiler"))] + pub fn new_syntax_error( + &self, + error: &crate::compiler::CompileError, + source: Option<&str>, + ) -> PyBaseExceptionRef { + self.new_syntax_error_maybe_incomplete(error, source, false) + } + pub fn new_import_error(&self, msg: String, name: PyStrRef) -> PyBaseExceptionRef { let import_error = self.ctx.exceptions.import_error.to_owned(); let exc = self.new_exception_msg(import_error, msg); From 1885e392d80461f16755e02fbb22271ac87b8891 Mon Sep 17 00:00:00 2001 From: Aneesh Durg Date: Wed, 30 Apr 2025 13:42:04 -0500 Subject: [PATCH 04/14] Refine when _IncompleteInputError is emitted --- Lib/codeop.py | 4 ---- vm/src/exceptions.rs | 4 ++-- vm/src/vm/vm_new.rs | 6 +++++- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Lib/codeop.py b/Lib/codeop.py index 4bbac5cef0..75127a1825 100644 --- a/Lib/codeop.py +++ b/Lib/codeop.py @@ -71,10 +71,6 @@ def _maybe_compile(compiler, source, filename, symbol): strerr = str(e) if isinstance(e, _IncompleteInputError): return None - if source.endswith(":") and "expected an indented block" in strerr: - return None - elif "incomplete input" in str(e): - return None # fallthrough return compiler(source, filename, symbol, incomplete_input=False) diff --git a/vm/src/exceptions.rs b/vm/src/exceptions.rs index ee503d81d3..cbf2d8b08e 100644 --- a/vm/src/exceptions.rs +++ b/vm/src/exceptions.rs @@ -1646,11 +1646,11 @@ pub(super) mod types { } - #[pyexception(name, base = "PyIncompleteInputError", ctx = "indentation_error", impl)] + #[pyexception(name, base = "PySyntaxError", ctx = "indentation_error", impl)] #[derive(Debug)] pub struct PyIndentationError {} - #[pyexception(name, base = "PyIncompleteInputError", ctx = "tab_error", impl)] + #[pyexception(name, base = "PySyntaxError", ctx = "tab_error", impl)] #[derive(Debug)] pub struct PyTabError {} diff --git a/vm/src/vm/vm_new.rs b/vm/src/vm/vm_new.rs index c2032cdd02..14e6991b75 100644 --- a/vm/src/vm/vm_new.rs +++ b/vm/src/vm/vm_new.rs @@ -363,7 +363,11 @@ impl VirtualMachine { .. }) => { if s.starts_with("Expected an indented block after") { - self.ctx.exceptions.indentation_error + if allow_incomplete { + self.ctx.exceptions.incomplete_input_error + } else { + self.ctx.exceptions.indentation_error + } } else { self.ctx.exceptions.syntax_error } From d94f81e7db90df65a9e1cf5183d9fe6dc7ca6c47 Mon Sep 17 00:00:00 2001 From: Aneesh Durg Date: Wed, 30 Apr 2025 13:52:09 -0500 Subject: [PATCH 05/14] Support multiline strings emitting _IncompleteInputError --- vm/src/vm/vm_new.rs | 47 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/vm/src/vm/vm_new.rs b/vm/src/vm/vm_new.rs index 14e6991b75..5e2ec349d5 100644 --- a/vm/src/vm/vm_new.rs +++ b/vm/src/vm/vm_new.rs @@ -358,6 +358,53 @@ impl VirtualMachine { } } #[cfg(feature = "parser")] + crate::compiler::CompileError::Parse(rustpython_compiler::ParseError { + error: + ruff_python_parser::ParseErrorType::Lexical( + ruff_python_parser::LexicalErrorType::FStringError( + ruff_python_parser::FStringErrorType::UnterminatedTripleQuotedString, + ), + ), + .. + }) => { + if allow_incomplete { + self.ctx.exceptions.incomplete_input_error + } else { + self.ctx.exceptions.syntax_error + } + } + #[cfg(feature = "parser")] + crate::compiler::CompileError::Parse(rustpython_compiler::ParseError { + error: + ruff_python_parser::ParseErrorType::Lexical( + ruff_python_parser::LexicalErrorType::UnclosedStringError, + ), + raw_location, + .. + }) => { + if allow_incomplete { + let mut is_incomplete = false; + + if let Some(source) = source { + let loc = raw_location.start().to_usize(); + let mut iter = source.chars(); + if let Some(quote) = iter.nth(loc) { + if iter.next() == Some(quote) && iter.next() == Some(quote) { + is_incomplete = true; + } + } + } + + if is_incomplete { + self.ctx.exceptions.incomplete_input_error + } else { + self.ctx.exceptions.syntax_error + } + } else { + self.ctx.exceptions.syntax_error + } + } + #[cfg(feature = "parser")] crate::compiler::CompileError::Parse(rustpython_compiler::ParseError { error: ruff_python_parser::ParseErrorType::OtherError(s), .. From bc3cd7b2b0b63ff8076b4cdfaa0429107a0e0b51 Mon Sep 17 00:00:00 2001 From: Aneesh Durg Date: Fri, 2 May 2025 08:13:25 -0500 Subject: [PATCH 06/14] lint --- vm/src/exceptions.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/vm/src/exceptions.rs b/vm/src/exceptions.rs index cbf2d8b08e..abb70d4a5a 100644 --- a/vm/src/exceptions.rs +++ b/vm/src/exceptions.rs @@ -1643,7 +1643,6 @@ pub(super) mod types { zelf.set_attr("name", vm.ctx.new_str("SyntaxError"), vm)?; Ok(()) } - } #[pyexception(name, base = "PySyntaxError", ctx = "indentation_error", impl)] From cff757459c85056bfd1bd75e7387326522943dac Mon Sep 17 00:00:00 2001 From: Aneesh Durg Date: Fri, 2 May 2025 08:24:52 -0500 Subject: [PATCH 07/14] Undo accidental change to PyTabError --- vm/src/exceptions.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vm/src/exceptions.rs b/vm/src/exceptions.rs index abb70d4a5a..11d941a09e 100644 --- a/vm/src/exceptions.rs +++ b/vm/src/exceptions.rs @@ -1649,7 +1649,7 @@ pub(super) mod types { #[derive(Debug)] pub struct PyIndentationError {} - #[pyexception(name, base = "PySyntaxError", ctx = "tab_error", impl)] + #[pyexception(name, base = "PyIndentationError", ctx = "tab_error", impl)] #[derive(Debug)] pub struct PyTabError {} From 1c827817689eaa57460aa287759bfe6a632bb3f5 Mon Sep 17 00:00:00 2001 From: Aneesh Durg Date: Fri, 2 May 2025 08:44:25 -0500 Subject: [PATCH 08/14] match -> if let --- src/shell.rs | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/src/shell.rs b/src/shell.rs index d8a92c62a8..1930dc997f 100644 --- a/src/shell.rs +++ b/src/shell.rs @@ -59,21 +59,19 @@ fn shell_exec( Err(err) => { // Check if the error is from an unclosed triple quoted string (which should always // continue) - match err { - CompileError::Parse(ParseError { - error: ParseErrorType::Lexical(LexicalErrorType::UnclosedStringError), - raw_location, - .. - }) => { - let loc = raw_location.start().to_usize(); - let mut iter = source.chars(); - if let Some(quote) = iter.nth(loc) { - if iter.next() == Some(quote) && iter.next() == Some(quote) { - return ShellExecResult::ContinueLine; - } + if let CompileError::Parse(ParseError { + error: ParseErrorType::Lexical(LexicalErrorType::UnclosedStringError), + raw_location, + .. + }) = err + { + let loc = raw_location.start().to_usize(); + let mut iter = source.chars(); + if let Some(quote) = iter.nth(loc) { + if iter.next() == Some(quote) && iter.next() == Some(quote) { + return ShellExecResult::ContinueLine; } } - _ => (), }; // bad_error == true if we are handling an error that should be thrown even if we are continuing From 1aec53e6a9e6a1548aa07d3e8be3c8958789f0ac Mon Sep 17 00:00:00 2001 From: Aneesh Durg Date: Fri, 2 May 2025 13:43:47 -0500 Subject: [PATCH 09/14] Fix test_baseexception and test_codeop --- Lib/test/test_baseexception.py | 2 ++ vm/src/vm/vm_new.rs | 27 ++++++++++++++++++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_baseexception.py b/Lib/test/test_baseexception.py index e19162a6ab..cdeae3655b 100644 --- a/Lib/test/test_baseexception.py +++ b/Lib/test/test_baseexception.py @@ -83,6 +83,8 @@ def test_inheritance(self): exc_set = set(e for e in exc_set if not e.startswith('_')) # RUSTPYTHON specific exc_set.discard("JitError") + # RUSTPYTHON specific + exc_set.discard("IncompleteInputError") self.assertEqual(len(exc_set), 0, "%s not accounted for" % exc_set) interface_tests = ("length", "args", "str", "repr") diff --git a/vm/src/vm/vm_new.rs b/vm/src/vm/vm_new.rs index 5e2ec349d5..978b694fc4 100644 --- a/vm/src/vm/vm_new.rs +++ b/vm/src/vm/vm_new.rs @@ -407,11 +407,36 @@ impl VirtualMachine { #[cfg(feature = "parser")] crate::compiler::CompileError::Parse(rustpython_compiler::ParseError { error: ruff_python_parser::ParseErrorType::OtherError(s), + raw_location, .. }) => { if s.starts_with("Expected an indented block after") { if allow_incomplete { - self.ctx.exceptions.incomplete_input_error + // Check that all chars in the error are whitespace, if so, the source is + // incomplete. Otherwise, we've found code that might violates + // indentation rules. + let mut is_incomplete = true; + if let Some(source) = source { + let start = raw_location.start().to_usize(); + let end = raw_location.end().to_usize(); + let mut iter = source.chars(); + iter.nth(start); + for _ in start..end { + if let Some(c) = iter.next() { + if !c.is_ascii_whitespace() { + is_incomplete = false; + } + } else { + break; + } + } + } + + if is_incomplete { + self.ctx.exceptions.incomplete_input_error + } else { + self.ctx.exceptions.indentation_error + } } else { self.ctx.exceptions.indentation_error } From f2b222566fcfe62d2611fbebb6a27d99f41a1a12 Mon Sep 17 00:00:00 2001 From: Aneesh Durg Date: Fri, 23 May 2025 17:11:05 -0500 Subject: [PATCH 10/14] fix spelling --- src/shell.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shell.rs b/src/shell.rs index 1930dc997f..801a989cf2 100644 --- a/src/shell.rs +++ b/src/shell.rs @@ -175,7 +175,7 @@ pub fn run_shell(vm: &VirtualMachine, scope: Scope) -> PyResult<()> { ShellExecResult::Ok => { if continuing_block { if empty_line_given { - // We should exit continue mode since the block succesfully executed + // We should exit continue mode since the block successfully executed continuing_block = false; full_input.clear(); } From ac524e08e6495bb91d13a08beb20370963b335b8 Mon Sep 17 00:00:00 2001 From: Aneesh Durg Date: Sat, 24 May 2025 12:12:58 -0500 Subject: [PATCH 11/14] fix exception name --- vm/src/exceptions.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/vm/src/exceptions.rs b/vm/src/exceptions.rs index 11d941a09e..60b6d86704 100644 --- a/vm/src/exceptions.rs +++ b/vm/src/exceptions.rs @@ -1627,7 +1627,11 @@ pub(super) mod types { } } - #[pyexception(name, base = "PySyntaxError", ctx = "incomplete_input_error")] + #[pyexception( + name = "_IncompleteInputError", + base = "PySyntaxError", + ctx = "incomplete_input_error" + )] #[derive(Debug)] pub struct PyIncompleteInputError {} From 6a188efa9364ca7e969555cef50f9b9412f2cac8 Mon Sep 17 00:00:00 2001 From: Aneesh Durg Date: Sun, 25 May 2025 16:30:55 -0500 Subject: [PATCH 12/14] Skip pickle test of _IncompleteInputError --- Lib/test/test_pickle.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Lib/test/test_pickle.py b/Lib/test/test_pickle.py index d61570ce10..e01ddcf0a8 100644 --- a/Lib/test/test_pickle.py +++ b/Lib/test/test_pickle.py @@ -664,6 +664,9 @@ def test_exceptions(self): BaseExceptionGroup, ExceptionGroup): continue + # TODO: RUSTPYTHON: fix name mapping for _IncompleteInputError + if exc is _IncompleteInputError: + continue if exc is not OSError and issubclass(exc, OSError): self.assertEqual(reverse_mapping('builtins', name), ('exceptions', 'OSError')) From 3e612dae45e606754b6643693b171952c8015c12 Mon Sep 17 00:00:00 2001 From: Aneesh Durg Date: Tue, 27 May 2025 10:33:20 -0500 Subject: [PATCH 13/14] Use py3.15's codeop implementation --- Lib/codeop.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/Lib/codeop.py b/Lib/codeop.py index 75127a1825..eea6cbc701 100644 --- a/Lib/codeop.py +++ b/Lib/codeop.py @@ -65,12 +65,10 @@ def _maybe_compile(compiler, source, filename, symbol): try: compiler(source + "\n", filename, symbol) return None + except _IncompleteInputError as e: + return None except SyntaxError as e: - # XXX: RustPython; support multiline definitions in REPL - # See also: https://github.com/RustPython/RustPython/pull/5743 - strerr = str(e) - if isinstance(e, _IncompleteInputError): - return None + pass # fallthrough return compiler(source, filename, symbol, incomplete_input=False) From 3fc91d2470b96d93b2724afa5439375719414533 Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" <69878+youknowone@users.noreply.github.com> Date: Thu, 5 Jun 2025 14:41:45 +0900 Subject: [PATCH 14/14] Update Lib/test/test_baseexception.py --- Lib/test/test_baseexception.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_baseexception.py b/Lib/test/test_baseexception.py index cdeae3655b..09db151ad2 100644 --- a/Lib/test/test_baseexception.py +++ b/Lib/test/test_baseexception.py @@ -83,7 +83,7 @@ def test_inheritance(self): exc_set = set(e for e in exc_set if not e.startswith('_')) # RUSTPYTHON specific exc_set.discard("JitError") - # RUSTPYTHON specific + # TODO: RUSTPYTHON; this will be officially introduced in Python 3.15 exc_set.discard("IncompleteInputError") self.assertEqual(len(exc_set), 0, "%s not accounted for" % exc_set)