-
Notifications
You must be signed in to change notification settings - Fork 1.5k
host_env: share thread, locale, loader and BSTR #8702
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
d805684
03f9538
c835fa3
e970948
49e2a90
3780f85
b4a47e8
ba51979
f6e95f5
86d44d9
d59ae00
7cefd9a
6ea05fd
5fb9cc8
b23874d
0dc5a92
03ae1f9
ce9f084
0224def
70bdcf6
9572676
b9c82b0
a69bab9
a385329
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -208,6 +208,7 @@ impl NormalizedParseDiagnostic { | |
| /// column. These are reconstructed by re-scanning after ruff's parse has already failed, so they | ||
| /// carry CPython's wording rather than a translation of ruff's own error, and they never reach | ||
| /// ruff — `NormalizedParseDiagnostic` and `CompileError` are the only things that consume one. | ||
| #[derive(Clone)] | ||
| struct CpythonDiagnostic { | ||
| message: String, | ||
| range: ruff_text_size::TextRange, | ||
|
|
@@ -243,6 +244,52 @@ impl CpythonDiagnostic { | |
| } | ||
| } | ||
|
|
||
| /// Lexer-class failures outrank print hints. Decode and f-string diagnostics | ||
| /// compete with lexer failures by offset, and an unterminated quote is only | ||
| /// a fallback when no decode/f-string diagnostic exists. Print is considered | ||
| /// against the final winner: a later 0x still beats print, but an f-string | ||
| /// that replaced that 0x must not hide an earlier print. A decode diagnostic | ||
| /// also suppresses an EOF unclosed opener. | ||
| #[derive(Clone, Copy, PartialEq, Eq)] | ||
| enum OverrideClass { | ||
| Lexer, | ||
| Decode, | ||
| Print, | ||
| } | ||
|
|
||
| struct RankedOverride { | ||
| diagnostic: CpythonDiagnostic, | ||
| unclosed_bracket: bool, | ||
| class: OverrideClass, | ||
| } | ||
|
|
||
| fn consider_override( | ||
| best: &mut Option<RankedOverride>, | ||
| diagnostic: CpythonDiagnostic, | ||
| class: OverrideClass, | ||
| ) { | ||
| let unclosed_bracket = diagnostic.is_unclosed_bracket; | ||
| consider_ranked(best, diagnostic, unclosed_bracket, class); | ||
| } | ||
|
|
||
| fn consider_ranked( | ||
| best: &mut Option<RankedOverride>, | ||
| diagnostic: CpythonDiagnostic, | ||
| unclosed_bracket: bool, | ||
| class: OverrideClass, | ||
| ) { | ||
| if best | ||
| .as_ref() | ||
| .is_none_or(|current| diagnostic.range.start() < current.diagnostic.range.start()) | ||
| { | ||
| *best = Some(RankedOverride { | ||
| diagnostic, | ||
| unclosed_bracket, | ||
| class, | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| fn cpython_parse_diagnostic_override( | ||
| error: &parser::ParseError, | ||
| source_file: &SourceFile, | ||
|
|
@@ -258,18 +305,80 @@ fn cpython_parse_diagnostic_override( | |
| }; | ||
| } | ||
|
|
||
| source_error!(invalid_number_literal_error(source_text)); | ||
| source_error!(invalid_legacy_statement_error(source_text)); | ||
| source_error!(incompatible_string_prefix_error(source_text)); | ||
| source_error!(malformed_unicode_n_escape_error(source_text)); | ||
| source_error!(non_printable_character_error(source_text)); | ||
| source_error!(invalid_interpolated_string_error(source_text)); | ||
| source_error!(mixed_tstring_literal_error(error, source_text)); | ||
|
|
||
| if let Some(bracket) = bracket_syntax_error(source_text) { | ||
| let mut earliest: Option<RankedOverride> = None; | ||
| if let Some(diagnostic) = invalid_number_literal_error(source_text) { | ||
| consider_override(&mut earliest, diagnostic, OverrideClass::Lexer); | ||
| } | ||
| if let Some(diagnostic) = incompatible_string_prefix_error(source_text) { | ||
| consider_override(&mut earliest, diagnostic, OverrideClass::Lexer); | ||
| } | ||
| if let Some(diagnostic) = non_printable_character_error(source_text) { | ||
| consider_override(&mut earliest, diagnostic, OverrideClass::Lexer); | ||
| } | ||
| let bracket = bracket_syntax_error(source_text); | ||
| if let Some(bracket) = bracket.as_ref() { | ||
| // Unclosed openers are reported at the opener and only become errors | ||
| // at EOF. A later token-time diagnostic (invalid number, prefix, …) | ||
| // must keep winning. Mismatched closers stay in the lexer-class | ||
| // positional ranking. | ||
| if !bracket.unclosed { | ||
| consider_ranked( | ||
| &mut earliest, | ||
| bracket.diagnostic.clone(), | ||
| false, | ||
| OverrideClass::Lexer, | ||
| ); | ||
| } | ||
| } | ||
| let mut saw_decode = false; | ||
| if let Some(diagnostic) = malformed_unicode_n_escape_error(source_text) { | ||
| saw_decode = true; | ||
| consider_override(&mut earliest, diagnostic, OverrideClass::Decode); | ||
| } | ||
| if let Some(diagnostic) = invalid_interpolated_string_error(source_text) { | ||
| saw_decode = true; | ||
| consider_override(&mut earliest, diagnostic, OverrideClass::Decode); | ||
| } | ||
| if let Some(diagnostic) = mixed_tstring_literal_error(error, source_text) { | ||
| saw_decode = true; | ||
| consider_override(&mut earliest, diagnostic, OverrideClass::Decode); | ||
| } | ||
| // A later ordinary unterminated quote is only a fallback. Format-spec | ||
| // newlines and empty fields are decode diagnostics and must keep winning. | ||
| let line_continuation = matches!( | ||
| &error.error, | ||
| parser::ParseErrorType::Lexical(parser::LexicalErrorType::LineContinuationError) | ||
| ); | ||
| if !saw_decode | ||
| && !line_continuation | ||
| && let Some(diagnostic) = unterminated_string_error(source_text, mode) | ||
| { | ||
| consider_override(&mut earliest, diagnostic, OverrideClass::Lexer); | ||
| } | ||
| if earliest | ||
| .as_ref() | ||
| .is_none_or(|current| current.class != OverrideClass::Lexer) | ||
| && let Some(diagnostic) = invalid_legacy_statement_error(source_text) | ||
| { | ||
| consider_override(&mut earliest, diagnostic, OverrideClass::Print); | ||
| } | ||
| if !saw_decode | ||
| && earliest | ||
|
Comment on lines
+365
to
+366
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.
Restore the line-continuation distinction here for nonterminal backslashes. For example, compiling AGENTS.md reference: AGENTS.md:L7-L11 Useful? React with 👍 / 👎. |
||
| .as_ref() | ||
| .is_none_or(|current| current.class == OverrideClass::Print) | ||
| && let Some(bracket) = bracket.filter(|bracket| bracket.unclosed) | ||
| { | ||
| consider_ranked( | ||
| &mut earliest, | ||
| bracket.diagnostic, | ||
| true, | ||
| OverrideClass::Lexer, | ||
| ); | ||
| } | ||
| if let Some(override_diag) = earliest { | ||
| return Some( | ||
| NormalizedParseDiagnostic::other(source_file, bracket.diagnostic) | ||
| .with_unclosed_bracket(bracket.unclosed), | ||
| NormalizedParseDiagnostic::other(source_file, override_diag.diagnostic) | ||
| .with_unclosed_bracket(override_diag.unclosed_bracket), | ||
| ); | ||
| } | ||
|
|
||
|
|
@@ -293,7 +402,9 @@ fn cpython_parse_diagnostic_override( | |
| } | ||
| let loc = source_location(source_file, error.location.start() + TextSize::from(1)); | ||
| return Some(NormalizedParseDiagnostic::new( | ||
| error.error.clone(), | ||
| parser::ParseErrorType::OtherError( | ||
| "unexpected character after line continuation character".to_owned(), | ||
| ), | ||
| loc, | ||
| loc, | ||
| )); | ||
|
|
@@ -591,7 +702,11 @@ fn invalid_radix_literal_error( | |
| let mut has_digit = false; | ||
| loop { | ||
| let Some(&byte) = bytes.get(index) else { | ||
| return Some((format!("invalid {kind} literal"), start + 1)); | ||
| return if has_digit { | ||
| None | ||
| } else { | ||
| Some((format!("invalid {kind} literal"), start + 1)) | ||
| }; | ||
| }; | ||
| if byte == b'_' { | ||
| let Some(&next) = bytes.get(index + 1) else { | ||
|
|
@@ -5876,6 +5991,7 @@ fn expected_opening_bracket(closing: char) -> char { | |
|
|
||
| /// A bracket diagnostic, and whether it is an opener that was never closed. The caller needs | ||
| /// that apart from the message because ruff reports the unclosed case as an EOF error. | ||
| #[derive(Clone)] | ||
| struct BracketError { | ||
| diagnostic: CpythonDiagnostic, | ||
| unclosed: bool, | ||
|
|
@@ -5932,6 +6048,24 @@ fn bracket_syntax_error(source: &str) -> Option<BracketError> { | |
| continue; | ||
| } | ||
|
|
||
| if ch == '\\' { | ||
| match chars.get(index + 1).map(|(_, next)| *next) { | ||
| Some('\n' | '\r') => { | ||
| escape_next = true; | ||
| index += 1; | ||
| continue; | ||
| } | ||
| Some(_) => { | ||
| index += 2; | ||
| continue; | ||
| } | ||
| None => { | ||
| index += 1; | ||
| continue; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if ch == '\'' || ch == '"' { | ||
| is_raw_string = false; | ||
| for look_back in 1..=2.min(index) { | ||
|
|
@@ -7496,6 +7630,49 @@ mod tests { | |
| ("fu''", "'u' and 'f' prefixes are incompatible"), | ||
| ("fb''", "'b' and 'f' prefixes are incompatible"), | ||
| ("ufr''", "'u' and 'r' prefixes are incompatible"), | ||
| ( | ||
| "(]\nbu'x'", | ||
| "closing parenthesis ']' does not match opening parenthesis '('", | ||
| ), | ||
| ("0x\nbu'x'", "invalid hexadecimal literal"), | ||
| ("(0x", "invalid hexadecimal literal"), | ||
| ("print x; 0x", "invalid hexadecimal literal"), | ||
| ("exec x; 0x", "invalid hexadecimal literal"), | ||
| ( | ||
| "print x; 0x1", | ||
| "Missing parentheses in call to 'print'. Did you mean print(...)?", | ||
| ), | ||
| ( | ||
| "print x; (", | ||
| "Missing parentheses in call to 'print'. Did you mean print(...)?", | ||
| ), | ||
| ("print x; )", "unmatched ')'"), | ||
| ( | ||
| "( '\\N'", | ||
| "(unicode error) 'unicodeescape' codec can't decode bytes in position 0-1: malformed \\N character escape", | ||
| ), | ||
| ("(print x", "'(' was never closed"), | ||
| ( | ||
| "(print x; '\\N'", | ||
| "Missing parentheses in call to 'print'. Did you mean print(...)?", | ||
| ), | ||
| ("f'{x'; '", "f-string: expecting '}'"), | ||
| ("f'{x'; 0x", "f-string: expecting '}'"), | ||
| ( | ||
| "print x; f'{x'; 0x", | ||
| "Missing parentheses in call to 'print'. Did you mean print(...)?", | ||
| ), | ||
| ( | ||
| concat!("f'{1:", "d\n}'"), | ||
| "f-string: newlines are not allowed in format specifiers", | ||
| ), | ||
| ("f'{\n}'", "f-string: valid expression required before '}'"), | ||
| ( | ||
| "f'''\n{\n# only a comment\n}'''", | ||
| "f-string: valid expression required before '}'", | ||
| ), | ||
| ("{\\'a\\'}", "unexpected character after line continuation"), | ||
| ("\"\\\n\"(1 for c in I,\\\n\\", "'(' was never closed"), | ||
| ( | ||
| r"'\N'", | ||
| "(unicode error) 'unicodeescape' codec can't decode bytes in position 0-1: malformed \\N character escape", | ||
|
|
@@ -7514,6 +7691,23 @@ mod tests { | |
| } | ||
| } | ||
|
|
||
| #[test] | ||
| fn unclosed_fstring_field_keeps_the_unclosed_bracket_flag() { | ||
| let err = compile("f'{", Mode::Eval, "<interp>", CompileOpts::default()) | ||
| .expect_err("should not compile"); | ||
| let crate::CompileError::Parse(parse) = err else { | ||
| panic!("expected a parse error, got {err}"); | ||
| }; | ||
| assert!( | ||
| parse.is_unclosed_bracket, | ||
| "unclosed f-string field must stay incomplete, got {parse}" | ||
| ); | ||
| assert!( | ||
| parse.to_string().contains("'{' was never closed"), | ||
| "got {parse}" | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn interpolated_literals_do_not_take_the_escaped_quote_hint() { | ||
| // Parser/lexer/lexer.c offers "perhaps you escaped the end quote?" from its plain-string | ||
|
|
||

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.
For input
print x; 0x, this offset comparison selects the legacy-printdiagnostic at byte 0, so RustPython reports “Missing parentheses”; checked against CPython 3.14.4, which reportsinvalid hexadecimal literalbecause the later tokenizer failure takes precedence over parser-level hints. The previous ordered checks produced that result, so ranking diagnostics solely by source position regresses syntax-error compatibility whenever an early grammar hint precedes a later lexical error.AGENTS.md reference: AGENTS.md:L7-L11
Useful? React with 👍 / 👎.
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.
Addressed in 374014d: lexer-class diagnostics now outrank parser-level hints, so print x; 0x reports invalid hexadecimal literal. A finished radix literal such as 0x1 is no longer treated as invalid, so print x; 0x1 still gets the print hint.
— commented by Claude