Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Support incomplete parsing #5764

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

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
4 changes: 1 addition & 3 deletions Lib/codeop.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,7 @@ 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 source.endswith(":") and "expected an indented block" in strerr:
return None
elif "incomplete input" in str(e):
if isinstance(e, _IncompleteInputError):
return None
# fallthrough

Expand Down
2 changes: 2 additions & 0 deletions Lib/test/test_baseexception.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
2 changes: 2 additions & 0 deletions compiler/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
pub struct ParseError {
#[source]
pub error: parser::ParseErrorType,
pub raw_location: ruff_text_size::TextRange,
pub location: SourceLocation,
pub source_path: String,
}
Expand All @@ -48,6 +49,7 @@
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(),
})
Expand Down Expand Up @@ -91,7 +93,7 @@
source_path: &str,
opts: CompileOpts,
) -> Result<CodeObject, CompileError> {
// TODO: do this less hackily; ruff's parser should translate a CRLF line

Check warning on line 96 in compiler/src/lib.rs

View workflow job for this annotation

GitHub Actions / Check Rust code with rustfmt and clippy

Unknown word (hackily)
// break in a multiline string into just an LF in the parsed value
#[cfg(windows)]
let source = &source.replace("\r\n", "\n");
Expand Down
100 changes: 75 additions & 25 deletions src/shell.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -14,19 +15,26 @@
enum ShellExecResult {
Ok,
PyErr(PyBaseExceptionRef),
Continue,
ContinueBlock,
ContinueLine,
}

fn shell_exec(
vm: &VirtualMachine,
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
// 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, "<stdin>".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,
Expand All @@ -40,8 +48,32 @@
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::ContinueLine,
Err(err) => {
// Check if the error is from an unclosed triple quoted string (which should always
// continue)
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
// 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.
Expand All @@ -50,10 +82,12 @@
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
}
Expand All @@ -68,7 +102,7 @@
if empty_line_given || bad_error {
ShellExecResult::PyErr(vm.new_syntax_error(&err, Some(source)))
} else {
ShellExecResult::Continue
ShellExecResult::ContinueBlock
}
}
}
Expand All @@ -93,10 +127,19 @@
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)
Expand All @@ -105,6 +148,8 @@
Ok(ref s) => s.as_str(),
Err(_) => "",
};

continuing_line = false;
let result = match repl.readline(prompt) {
ReadlineResult::Line(line) => {
debug!("You entered {:?}", line);
Expand All @@ -120,39 +165,44 @@
}
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

Check warning on line 178 in src/shell.rs

View workflow job for this annotation

GitHub Actions / Check Rust code with rustfmt and clippy

Misspelled word (succesfully) Suggestions: (successfully*)
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());
Expand Down
7 changes: 7 additions & 0 deletions vm/src/compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
22 changes: 22 additions & 0 deletions vm/src/exceptions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,7 @@ pub struct ExceptionZoo {
pub not_implemented_error: &'static Py<PyType>,
pub recursion_error: &'static Py<PyType>,
pub syntax_error: &'static Py<PyType>,
pub incomplete_input_error: &'static Py<PyType>,
pub indentation_error: &'static Py<PyType>,
pub tab_error: &'static Py<PyType>,
pub system_error: &'static Py<PyType>,
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -817,6 +819,7 @@ impl ExceptionZoo {
not_implemented_error,
recursion_error,
syntax_error,
incomplete_input_error,
indentation_error,
tab_error,
system_error,
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -1623,6 +1627,24 @@ pub(super) mod types {
}
}

#[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 = "PySyntaxError", ctx = "indentation_error", impl)]
#[derive(Debug)]
pub struct PyIndentationError {}
Expand Down
5 changes: 3 additions & 2 deletions vm/src/stdlib/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -295,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
Expand Down
10 changes: 8 additions & 2 deletions vm/src/stdlib/builtins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,8 @@
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"))]
{
Expand All @@ -207,14 +209,17 @@
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::<parser::Mode>()
.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))
}
}
}
Expand Down Expand Up @@ -438,7 +443,7 @@
.is_ok_and(|fd| fd == expected)
};

// everything is normalish, we can just rely on rustyline to use stdin/stdout

Check warning on line 446 in vm/src/stdlib/builtins.rs

View workflow job for this annotation

GitHub Actions / Check Rust code with rustfmt and clippy

Unknown word (normalish)
if fd_matches(&stdin, 0) && fd_matches(&stdout, 1) && std::io::stdin().is_terminal() {
let prompt = prompt.as_ref().map_or("", |s| s.as_str());
let mut readline = Readline::new(());
Expand Down Expand Up @@ -1056,6 +1061,7 @@
"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(),
Expand Down
Loading
Loading