host_env: share thread, locale, loader and BSTR by youknowone · Pull Request #8702 · RustPython/RustPython · GitHub
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
d805684
host_env: share thread, locale, loader and BSTR
youknowone Sep 13, 2026
03f9538
host_env: expose rustls-free ssl core on wasm
youknowone Sep 13, 2026
c835fa3
stdlib: add wasm _ssl on rustls-free host_env
youknowone Sep 14, 2026
e970948
host_env: address #8702 review comments
youknowone Sep 14, 2026
49e2a90
host_env: gate the whole ssl module on feature ssl
youknowone Sep 14, 2026
3780f85
host_env: fix Windows types and thread names
youknowone Sep 14, 2026
b4a47e8
host_env: compile rustls engine on WASI
youknowone Sep 14, 2026
ba51979
compiler: pick the earliest CPython parse override
youknowone Sep 14, 2026
f6e95f5
host_env: address remaining #8702 review comments
youknowone Sep 14, 2026
86d44d9
stdlib: add wasm _socket and read-all MemoryBIO
youknowone Sep 14, 2026
d59ae00
host_env: fix clippy, WASI ssl cfg, and wasm rustls
youknowone Sep 14, 2026
7cefd9a
host_env: fix Windows clippy and wasm UnixTime
youknowone Sep 15, 2026
6ea05fd
host_env: lock rustls-pki-types web for wasm
youknowone Sep 15, 2026
5fb9cc8
host_env: silence Windows unused thread id and shear
youknowone Sep 15, 2026
b23874d
stdlib: drop Windows locale clippy needless wraps
youknowone Sep 15, 2026
0dc5a92
compiler: keep lexer errors over print hints
youknowone Sep 15, 2026
03ae1f9
compiler: keep decode errors over unclosed openers
youknowone Sep 15, 2026
ce9f084
wasm: drop duplicate _socket; split locale cfgs
youknowone Sep 15, 2026
0224def
compiler: keep decode blockers over unclosed openers
youknowone Sep 15, 2026
70bdcf6
compiler: keep earlier f-string over later lexer errors
youknowone Sep 15, 2026
9572676
compiler: rank decode vs lexer vs print by class
youknowone Sep 16, 2026
b9c82b0
compiler: keep unclosed paren over line-cont at EOF
youknowone Sep 16, 2026
a69bab9
test_threading: xfail daemon join finalization off Linux
youknowone Sep 16, 2026
a385329
vm: hang daemon threads at interpreter finalize
youknowone Sep 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
1 change: 0 additions & 1 deletion Lib/test/test_threading.py
Original file line number Diff line number Diff line change
Expand Up @@ -1183,7 +1183,6 @@ def __del__(self):
self.assertEqual(out.strip(), b"OK")
self.assertIn(b"can't create new thread at interpreter shutdown", err)

@unittest.expectedFailure # TODO: RUSTPYTHON
def test_join_daemon_thread_in_finalization(self):
# gh-123940: Py_Finalize() prevents other threads from running Python
# code, so join() can not succeed unless the thread is already done.
Expand Down
220 changes: 207 additions & 13 deletions crates/compiler/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve tokenizer precedence over earlier grammar hints

For input print x; 0x, this offset comparison selects the legacy-print diagnostic at byte 0, so RustPython reports “Missing parentheses”; checked against CPython 3.14.4, which reports invalid hexadecimal literal because 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 👍 / 👎.

Copy link
Copy Markdown
Member Author

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

{
*best = Some(RankedOverride {
diagnostic,
unclosed_bracket,
class,
});
}
}

fn cpython_parse_diagnostic_override(
error: &parser::ParseError,
source_file: &SourceFile,
Expand All @@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Limit opener precedence to terminal backslashes

Restore the line-continuation distinction here for nonterminal backslashes. For example, compiling '(\\)' in eval mode produces a LineContinuationError, while the bracket scanner skips both the backslash and ) and leaves ( marked unclosed; this branch then replaces the lexical error with "'(' was never closed". CPython 3.14.4 instead reports unexpected character after line continuation character, so the opener should take precedence only when the backslash actually terminates the input.

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),
);
}

Expand All @@ -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,
));
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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",
Expand All @@ -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
Expand Down
20 changes: 16 additions & 4 deletions crates/host_env/Cargo.toml
Loading
Loading