Implement PEP 798 comprehension unpacking (#9342)

* Support PEP 798 comprehension unpacking

Extract the remaining Python 3.15 comprehension unpacking implementation
from the integration branch. Use existing list/set/dict update operations
and an explicit inner iterator loop for generator unpacking. Align the
public DictComp AST representation and schema with CPython.

Migrate test_unpack_ex and test_unparse with scripts/update_lib from
CPython d220409a13b77de7a6c30abe16ea90db01528edc. Preserve existing Rust tests;
add no duplicate regression tests or snippets.

AI-assisted source extraction, canonical migration, and independent source
review with OpenAI Codex. Runtime validation is not yet complete.

Assisted-by: OpenAI Codex:model version unavailable

* Improve selected unpacking diagnostics and refresh canonical exception tests

Normalize six supported comprehension-unpacking diagnostics using existing parser context. Restrict the dictionary-spread hint to its first entry to avoid changing unrelated diagnostics.

Migrate the whole test_exceptions module from CPython d220409a13b77de7a6c30abe16ea90db01528edc with the existing updater, preserving only its two existing RustPython markers. Ruff sources and dependency pins are unchanged.

Six focused check/build/strict Clippy gates passed. A paired 34-input compile-only comparison retains six canonical diagnostic improvements, with eight of fifteen canonical cases still differing; this is not full test_exceptions or CI validation.

* Fix mapping-rest error locations and retire stale encoding xfail

* Match remaining unpacking diagnostics without changing Ruff

Use exact retained AST and error ownership to normalize the remaining CPython 3.15 unpacking diagnostics. Reparse only the owned call argument slice with the unchanged Ruff parser when its original recovery loses generator clauses.

Validated all 30 canonical compile-only cases in exec and doctest-style single modes against CPython 3.15.0, plus finite surrounding controls. Targeted compiler Clippy and source-bound builds passed. Full CI is pending. Cspell alone was omitted with the maintainer’s explicit one-commit approval because its npm bootstrap downloads returned HTTP 403; all other applicable configured commit hooks ran.

AI assistance: dot authored, reviewed, and validated the changes. The runtime does not expose its exact model version.

Assisted-by: dot: model version not exposed

---------

Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
youknowdotandCodex authored and GitHub committed 2026-10-27 13:29:59 +09:00
1 parent 1df1daea3e
commit 75df7a8c2e
11 files changed
+1186 -129

No files matched your search

+54 -44
View File
@@ -9421,14 +9421,14 @@ impl<'warnings> Compiler<'warnings> {
),
generators,
&|compiler, collection_add_i| {
compiler.compile_comprehension_element(elt)?;
let unpack = compiler.compile_comprehension_element(elt)?;
compiler.set_source_node(elt.range(), elt.node_index());
emit!(
compiler,
Instruction::ListAppend {
i: collection_add_i.to_u32(),
}
);
let i = collection_add_i.to_u32();
if unpack {
emit!(compiler, Instruction::ListExtend { i });
} else {
emit!(compiler, Instruction::ListAppend { i });
}
Ok(())
},
ComprehensionType::List,
@@ -9451,14 +9451,14 @@ impl<'warnings> Compiler<'warnings> {
),
generators,
&|compiler, collection_add_i| {
compiler.compile_comprehension_element(elt)?;
let unpack = compiler.compile_comprehension_element(elt)?;
compiler.set_source_node(elt.range(), elt.node_index());
emit!(
compiler,
Instruction::SetAdd {
i: collection_add_i.to_u32(),
}
);
let i = collection_add_i.to_u32();
if unpack {
emit!(compiler, Instruction::SetUpdate { i });
} else {
emit!(compiler, Instruction::SetAdd { i });
}
Ok(())
},
ComprehensionType::Set,
@@ -9474,11 +9474,9 @@ impl<'warnings> Compiler<'warnings> {
generators,
..
}) => {
let Some(key) = key.as_deref() else {
self.restore_source_span(expression_span);
return Err(self.error(CodegenErrorType::SyntaxError(
"dict unpacking cannot be used in dict comprehension".to_owned(),
)));
let element_span = match key.as_deref() {
Some(key) => self.dict_comprehension_element_span(key, value)?,
None => self.node_span(value.as_ref()),
};
self.compile_comprehension(
"<dictcomp>",
@@ -9490,29 +9488,30 @@ impl<'warnings> Compiler<'warnings> {
),
generators,
&|compiler, collection_add_i| {
// changed evaluation order for Py38 named expression PEP 572
compiler.compile_expression(key)?;
// Keys precede values; a missing Ruff key denotes **value.
if let Some(key) = key {
// Changed evaluation order for Py38 named expression PEP 572.
compiler.compile_expression(key)?;
}
compiler.compile_expression(value)?;
compiler.restore_source_span(
compiler.dict_comprehension_element_span(key, value)?,
);
emit!(
compiler,
Instruction::MapAdd {
i: collection_add_i.to_u32(),
}
);
compiler.restore_source_span(element_span);
let i = collection_add_i.to_u32();
if key.is_some() {
emit!(compiler, Instruction::MapAdd { i });
} else {
emit!(compiler, Instruction::DictUpdate { i });
}
Ok(())
},
ComprehensionType::Dict,
Self::contains_await(key)
key.as_deref().is_some_and(Self::contains_await)
|| Self::contains_await(value)
|| Self::generators_contain_await(generators),
expression_span,
self.dict_comprehension_element_span(key, value)?,
self.node_span(key),
element_span,
self.node_span(key.as_deref().unwrap_or(value)),
)?;
}
ast::Expr::Generator(ast::ExprGenerator {
@@ -10255,16 +10254,13 @@ impl<'warnings> Compiler<'warnings> {
Ok(())
}
fn compile_comprehension_element(&mut self, element: &ast::Expr) -> CompileResult<()> {
self.compile_expression(element).map_err(|e| {
if let CodegenErrorType::InvalidStarExpr = e.error {
self.error(CodegenErrorType::SyntaxError(
"iterable unpacking cannot be used in comprehension".to_owned(),
))
} else {
e
}
})
fn compile_comprehension_element(&mut self, element: &ast::Expr) -> CompileResult<bool> {
let (value, unpack) = match element {
ast::Expr::Starred(starred) => (&*starred.value, true),
_ => (element, false),
};
self.compile_expression(value)?;
Ok(unpack)
}
fn compile_generator_expression(
@@ -10285,7 +10281,15 @@ impl<'warnings> Compiler<'warnings> {
// Compile the element expression
// Note: if element is an async comprehension, compile_expression
// already handles awaiting it, so we don't need to await again here
compiler.compile_comprehension_element(elt)?;
let unpack = compiler.compile_comprehension_element(elt)?;
let unpack_loop = unpack.then(|| (compiler.new_block(), compiler.new_block()));
if let Some((start, end)) = unpack_loop {
// Unpacking forwards values, not send()/throw() as yield-from would.
compiler.set_source_node(elt.range(), elt.node_index());
emit!(compiler, Instruction::GetIter { mode: 0 });
compiler.use_cpython_label_block(start);
emit!(compiler, Instruction::ForIter { delta: end });
}
compiler.mark_generator();
if compiler.ctx.func == FunctionContext::AsyncFunction {
@@ -10307,6 +10311,12 @@ impl<'warnings> Compiler<'warnings> {
}
);
emit!(compiler, Instruction::PopTop);
if let Some((start, end)) = unpack_loop {
emit!(compiler, PseudoInstruction::Jump { delta: start });
compiler.set_no_location();
compiler.use_cpython_label_block(end);
compiler.emit_sync_comprehension_end_for();
}
Ok(())
},
+5 -7
View File
@@ -2419,16 +2419,14 @@ impl SymbolTableBuilder {
self.in_iter_def_exp = true;
}
// Dict comprehension - is_generator = false (can be inlined)
let Some(key) = key.as_deref() else {
return Err(self.error_ranged(
"dict unpacking cannot be used in dict comprehension".to_owned(),
*range,
));
let (element, value) = match key.as_deref() {
Some(key) => (key, Some(value.as_ref())),
None => (value.as_ref(), None),
};
self.scan_comprehension(
&"<dictcomp>".into(),
key,
Some(value),
element,
value,
generators,
*range,
false,
+580 -9
View File
@@ -1264,6 +1264,8 @@ fn first_parse_error(
parsed: parser::Parsed<ast::Mod>,
source_file: &SourceFile,
mode: Mode,
prepared_source: &str,
parser_options: &parser::ParseOptions,
) -> Result<parser::Parsed<ast::Mod>, CompileError> {
let Some((first, rest)) = parsed.errors().split_first() else {
return Ok(parsed);
@@ -1272,13 +1274,556 @@ fn first_parse_error(
!matches!(error.error, ParseErrorType::Lexical(_))
&& error.location.start() < first.location.start()
});
let mut error = CompileError::from_ruff_parse_error(first.clone(), source_file, mode);
let normalized = unpacking_parse_diagnostic(
first,
rest,
parsed.tokens(),
parsed.syntax(),
parsed.unsupported_syntax_errors().is_empty(),
)
.or_else(|| call_argument_unpacking_diagnostic(&parsed, prepared_source, parser_options))
.or_else(|| unfinished_dict_comprehension_diagnostic(&parsed, parser_options))
.map_or_else(
|| first.clone(),
|diagnostic| parser::ParseError {
error: ParseErrorType::OtherError(diagnostic.message),
location: diagnostic.range,
},
);
let mut error = CompileError::from_ruff_parse_error(normalized, source_file, mode);
if after_syntax_error && let CompileError::Parse(error) = &mut error {
error.is_unclosed_string = false;
}
Err(error)
}
// A recovered call never parsed its comprehension clauses. For a narrowly owned
// whole-input argument list, ask the same parser to validate that exact slice once.
fn call_argument_unpacking_diagnostic(
parsed: &parser::Parsed<ast::Mod>,
prepared_source: &str,
options: &parser::ParseOptions,
) -> Option<CpythonDiagnostic> {
let first = parsed.errors().first()?;
if !matches!(
first.error,
ParseErrorType::ExpectedToken {
expected: TokenKind::Rpar,
found: TokenKind::For
}
) || !parsed.unsupported_syntax_errors().is_empty()
|| parsed
.errors()
.iter()
.any(|error| error.location.start() < first.location.start())
{
return None;
}
let ast::Mod::Module(module) = parsed.syntax() else {
return None;
};
let ast::Stmt::Expr(statement) = module.body.first()? else {
return None;
};
let ast::Expr::Call(call) = statement.value.as_ref() else {
return None;
};
let [keyword] = call.arguments.keywords.as_slice() else {
return None;
};
if !call.arguments.args.is_empty()
|| keyword.arg.is_some()
|| keyword.end() != call.arguments.end()
|| call.end() != keyword.end()
{
return None;
}
let tokens: Vec<_> = parsed
.tokens()
.iter()
.filter(|token| {
!matches!(
token.kind(),
TokenKind::Comment
| TokenKind::NonLogicalNewline
| TokenKind::Newline
| TokenKind::EndOfFile
)
})
.collect();
if tokens.first()?.start() != call.start() {
return None;
}
let opening_index = tokens
.iter()
.position(|token| token.start() == call.arguments.start())?;
let opening = tokens.get(opening_index)?;
let operator = tokens.get(opening_index.checked_add(1)?)?;
let closing = tokens.last()?;
let clause = tokens.iter().find(|token| token.start() >= keyword.end())?;
if opening.kind() != TokenKind::Lpar
|| operator.kind() != TokenKind::DoubleStar
|| operator.start() != keyword.start()
|| closing.kind() != TokenKind::Rpar
|| clause.range() != first.location
|| clause.kind() != TokenKind::For
{
return None;
}
let slice = prepared_source.get(usize::from(opening.start())..usize::from(closing.end()))?;
let reparsed = parser::parse_unchecked(
slice,
parser::ParseOptions::from(parser::Mode::Expression)
.with_target_version(options.target_version()),
);
let [error] = reparsed.errors() else {
return None;
};
if !matches!(error.error, ParseErrorType::ExpectedExpression)
|| !reparsed.unsupported_syntax_errors().is_empty()
|| opening.start().checked_add(error.location.start())? != operator.start()
|| opening.start().checked_add(error.location.end())? != operator.end()
{
return None;
}
let ast::Mod::Expression(expression) = reparsed.syntax() else {
return None;
};
let ast::Expr::Generator(generator) = expression.body.as_ref() else {
return None;
};
let ast::Expr::BinOp(binary) = generator.elt.as_ref() else {
return None;
};
if !generator.parenthesized
|| generator.start() != TextSize::new(0)
|| usize::from(generator.end()) != slice.len()
|| binary.end() != binary.right.end()
{
return None;
}
// Both parses must retain the same unpacked operand, not just its operator.
if opening.start().checked_add(generator.elt.start())? != keyword.start()
|| opening.start().checked_add(generator.elt.end())? != keyword.end()
|| opening.start().checked_add(binary.right.start())? != keyword.value.start()
|| opening.start().checked_add(binary.right.end())? != keyword.value.end()
{
return None;
}
let diagnostic = owned_unpacking_diagnostic(error, reparsed.tokens(), reparsed.syntax())?;
if usize::from(diagnostic.range.end()) > slice.len() {
return None;
}
// Module recovery forgets the call's opening delimiter before reaching its
// closing token. Only the complete, balanced generator reparse above can
// justify treating this exact unmatched-closing error as a recovery shadow.
if parsed.errors().iter().any(|error| match &error.error {
ParseErrorType::Lexical(parser::LexicalErrorType::UnmatchedBracket { closing: ')' }) => {
!error.location.is_empty() || error.location.start() != closing.start()
}
ParseErrorType::Lexical(_) => true,
_ => false,
}) {
return None;
}
Some(CpythonDiagnostic {
message: diagnostic.message,
range: ruff_text_size::TextRange::new(
opening.start().checked_add(diagnostic.range.start())?,
opening.start().checked_add(diagnostic.range.end())?,
),
})
}
// An unfinished unpacking comprehension can recover as an annotated assignment.
// Recognize only the complete, owned recovery cascade, not arbitrary later errors.
fn unfinished_dict_comprehension_diagnostic(
parsed: &parser::Parsed<ast::Mod>,
options: &parser::ParseOptions,
) -> Option<CpythonDiagnostic> {
let first = parsed.errors().first()?;
if !matches!(
first.error,
ParseErrorType::ExpectedToken {
expected: TokenKind::Rbrace,
found: TokenKind::Colon
}
) {
return None;
}
let ast::Mod::Module(module) = parsed.syntax() else {
return None;
};
let [ast::Stmt::AnnAssign(statement)] = module.body.as_slice() else {
return None;
};
let ast::Expr::DictComp(dict) = statement.target.as_ref() else {
return None;
};
if dict.key.is_some()
|| dict.end() != dict.generators.last()?.end()
|| statement.start() != dict.start()
|| statement.value.is_some()
|| statement.annotation.range().is_empty()
|| statement.annotation.start() < first.location.end()
|| statement.annotation.end() != statement.end()
{
return None;
}
let tokens: Vec<_> = parsed
.tokens()
.iter()
.filter(|token| {
!matches!(
token.kind(),
TokenKind::Comment | TokenKind::NonLogicalNewline | TokenKind::EndOfFile
)
})
.collect();
let opening = tokens.first()?;
let operator = tokens.get(1)?;
if opening.kind() != TokenKind::Lbrace
|| opening.start() != dict.start()
|| operator.kind() != TokenKind::DoubleStar
|| operator.end() > dict.value.start()
{
return None;
}
let colon = tokens.iter().find(|token| token.start() >= dict.end())?;
if colon.kind() != TokenKind::Colon || colon.range() != first.location {
return None;
}
let closing_index = tokens
.iter()
.position(|token| token.start() >= statement.end())?;
let closing = tokens.get(closing_index)?;
if closing.kind() != TokenKind::Rbrace {
return None;
}
let newline = match tokens.get(closing_index.checked_add(1)?..)? {
[] => None,
[newline] if newline.kind() == TokenKind::Newline => Some(*newline),
_ => return None,
};
let [unsupported] = parsed.unsupported_syntax_errors() else {
return None;
};
// The payload enum is not exported. This exact DictComp/operator range owns
// the parser's DictInDict producer, excluding other comprehension features.
if !matches!(
unsupported.kind,
parser::UnsupportedSyntaxErrorKind::UnpackingInComprehension(_)
) || unsupported.range != ruff_text_size::TextRange::new(operator.start(), dict.value.end())
|| unsupported.target_version != options.target_version()
{
return None;
}
let errors = parsed.errors();
if errors.len() != 3 + usize::from(newline.is_some())
|| !matches!(
errors.get(1)?.error,
ParseErrorType::InvalidAnnotatedAssignmentTarget
)
|| errors.get(1)?.location != dict.range()
{
return None;
}
let is_shadow = |error: &parser::ParseError, range| {
matches!(&error.error, ParseErrorType::OtherError(message) if message == "invalid syntax")
&& error.location == range
};
if !is_shadow(errors.get(2)?, closing.range())
|| newline.is_some_and(|newline| !is_shadow(&errors[3], newline.range()))
{
return None;
}
Some(CpythonDiagnostic {
message: "cannot use dict unpacking here".to_owned(),
range: operator.range(),
})
}
/// Refine unpacking errors whose grammar context and operand range the parser already
/// identified. Tokens only distinguish the unpacking operator; they do not reparse the input.
fn unpacking_parse_diagnostic(
error: &parser::ParseError,
following_errors: &[parser::ParseError],
tokens: &Tokens,
syntax: &ast::Mod,
no_unsupported_errors: bool,
) -> Option<CpythonDiagnostic> {
if matches!(
error.error,
ParseErrorType::InvalidStarredExpressionUsage | ParseErrorType::ExpectedExpression
) && following_errors.is_empty()
&& no_unsupported_errors
{
return owned_unpacking_diagnostic(error, tokens, syntax);
}
let ParseErrorType::OtherError(message) = &error.error else {
return None;
};
let mut range = error.location;
let message = match message.as_str() {
"cannot use double starred expression here" if following_errors.is_empty() => {
let mut operand = tokens.iter().filter(|token| {
token.start() >= range.end()
&& !matches!(
token.kind(),
TokenKind::Comment | TokenKind::NonLogicalNewline
)
});
// The parser consumed a complete parenthesized operand. Lambda and
// `not` do not start the bitwise-or prefix required by CPython.
if matches!(operand.next()?.kind(), TokenKind::Lambda | TokenKind::Not) {
return None;
}
"cannot use dict unpacking here".to_owned()
}
"pattern cannot follow a double star pattern" => {
// A later recovery error can change which invalid production CPython
// selects. Leave those inputs to the existing diagnostic conversion.
if !following_errors.is_empty() {
return None;
}
let mut preceding = tokens.iter().rev().filter(|token| {
token.end() <= range.start()
&& !matches!(
token.kind(),
TokenKind::Comment | TokenKind::NonLogicalNewline
)
});
// The parser has already recognized a mapping entry after its rest
// capture. CPython points at that capture, not the following entry.
if preceding.next()?.kind() != TokenKind::Comma {
return None;
}
let capture = preceding.next()?;
if capture.kind() != TokenKind::Identifier
|| preceding.next()?.kind() != TokenKind::DoubleStar
{
return None;
}
range = capture.range();
"double star pattern must be the last (right-most) subpattern in the mapping pattern"
.to_owned()
}
"conditional expression cannot be used here" => {
let mut preceding = tokens.iter().rev().filter(|token| {
token.end() <= range.start()
&& !matches!(
token.kind(),
TokenKind::Comment | TokenKind::NonLogicalNewline
)
});
let operator = preceding.next()?;
let kind = match operator.kind() {
TokenKind::Star => "starred",
// Only the first dictionary spread uses this invalid production.
// Dictionary values and later spreads have different error priority.
TokenKind::DoubleStar
if preceding
.next()
.is_some_and(|token| token.kind() == TokenKind::Lbrace) =>
{
"double starred"
}
_ => return None,
};
range = ruff_text_size::TextRange::new(operator.start(), range.end());
// Do not replace the operand error when the parser also rejected the
// enclosing unpacking operation, for example in a dictionary value.
if following_errors.iter().any(|error| {
error.location == range
&& (matches!(error.error, ParseErrorType::InvalidStarredExpressionUsage)
|| matches!(&error.error, ParseErrorType::OtherError(message)
if message == "cannot use a starred expression in a dictionary value"))
}) {
return None;
}
format!(
"invalid {kind} expression. Did you forget to wrap the conditional expression in parentheses?"
)
}
"expected expression after 'else', but statement is given" => {
let operator = tokens.iter().find(|token| token.range() == range)?;
match operator.kind() {
TokenKind::Star => "cannot unpack only part of a conditional expression",
TokenKind::DoubleStar => {
"cannot use dict unpacking on only part of a conditional expression"
}
_ => return None,
}
.to_owned()
}
_ => return None,
};
Some(CpythonDiagnostic { message, range })
}
fn owned_unpacking_diagnostic(
error: &parser::ParseError,
tokens: &Tokens,
syntax: &ast::Mod,
) -> Option<CpythonDiagnostic> {
use ast::visitor::Visitor;
struct Checker<'a> {
range: ruff_text_size::TextRange,
tokens: &'a Tokens,
comprehension: bool,
message: Option<&'static str>,
}
impl Checker<'_> {
fn is_double_starred_operand(&self, operand: &ast::Expr) -> bool {
self.tokens
.iter()
.rev()
.find(|token| {
token.end() <= operand.start()
&& !matches!(
token.kind(),
TokenKind::Comment | TokenKind::NonLogicalNewline
)
})
.is_some_and(|token| {
token.kind() == TokenKind::DoubleStar && token.start() == self.range.start()
})
}
fn check_comprehension(
&mut self,
element: &ast::Expr,
generators: &[ast::Comprehension],
delimiter: TokenKind,
container_start: TextSize,
message: &'static str,
) {
let ast::Expr::BinOp(binary) = element else {
return;
};
let ast::Expr::Name(missing) = binary.left.as_ref() else {
return;
};
if binary.op != ast::Operator::Pow
|| !missing.id.is_empty()
|| missing.ctx != ast::ExprContext::Invalid
|| !missing.range.is_empty()
|| element.start() != self.range.start()
|| generators.is_empty()
{
return;
}
let Some(operator) = self.tokens.iter().find(|token| token.range() == self.range)
else {
return;
};
let preceding = self.tokens.iter().rev().find(|token| {
token.end() <= operator.start()
&& !matches!(
token.kind(),
TokenKind::Comment | TokenKind::NonLogicalNewline
)
});
if operator.kind() == TokenKind::DoubleStar
&& preceding.is_some_and(|token| {
token.kind() == delimiter && token.start() == container_start
})
{
self.range = ruff_text_size::TextRange::new(operator.start(), element.end());
self.message = Some(message);
}
}
fn check_item(&mut self, key: Option<&ast::Expr>, value: &ast::Expr) {
let Some(key) = key else { return };
// The parser reports InvalidStarredExpressionUsage at just the first
// byte of the operator. Recover the operand end from its retained role.
if self.range.len() != TextSize::new(1) {
return;
}
let (message, end) = if matches!(key, ast::Expr::Starred(_))
&& key.start() == self.range.start()
&& self
.tokens
.iter()
.any(|token| token.kind() == TokenKind::Star && token.range() == self.range)
{
(
"cannot use a starred expression in a dictionary key",
key.end(),
)
} else if self.is_double_starred_operand(key) {
("cannot use dict unpacking in a dictionary key", key.end())
} else if self.is_double_starred_operand(value) {
(
"cannot use dict unpacking in a dictionary value",
value.end(),
)
} else {
return;
};
self.range = ruff_text_size::TextRange::new(self.range.start(), end);
self.message = Some(message);
}
}
impl<'a> Visitor<'a> for Checker<'_> {
fn visit_expr(&mut self, expr: &'a ast::Expr) {
if self.message.is_some() || !expr.range().contains_range(self.range) {
return;
}
// Retained nodes establish the operand role; the parser reported
// no other errors. Dictionary errors here only have first-item producers.
match expr {
ast::Expr::ListComp(comp) if self.comprehension => self.check_comprehension(
&comp.elt,
&comp.generators,
TokenKind::Lsqb,
comp.start(),
"cannot use dict unpacking in list comprehension",
),
ast::Expr::Generator(comp) if self.comprehension && comp.parenthesized => self
.check_comprehension(
&comp.elt,
&comp.generators,
TokenKind::Lpar,
comp.start(),
"cannot use dict unpacking in generator expression",
),
ast::Expr::Dict(dict) if !self.comprehension => {
if let Some(item) = dict.items.first() {
self.check_item(item.key.as_ref(), &item.value);
}
}
ast::Expr::DictComp(dict) if !self.comprehension => {
self.check_item(dict.key.as_deref(), &dict.value)
}
_ => {}
}
if self.message.is_none() {
ast::visitor::walk_expr(self, expr);
}
}
}
let mut checker = Checker {
range: error.location,
tokens,
comprehension: matches!(error.error, ParseErrorType::ExpectedExpression),
message: None,
};
match syntax {
ast::Mod::Module(module) => checker.visit_body(&module.body),
ast::Mod::Expression(expression) => checker.visit_expr(&expression.body),
}
checker.message.map(|message| CpythonDiagnostic {
message: message.to_owned(),
range: checker.range,
})
}
/// The first logical input accepted by the single-input grammar.
#[derive(Clone, Copy)]
pub enum SingleModeStatementBoundary {
@@ -1586,7 +2131,7 @@ fn _compile_with_syntax_warning_handler<'a>(
.contains(core::bytecode::CodeFlags::FUTURE_BARRY_AS_BDFL),
);
pre_parse_source_error(&source_file)?;
let parsed = parser::parse_unchecked(barry_source.source(), parser_options);
let parsed = parser::parse_unchecked(barry_source.source(), parser_options.clone());
if matches!(mode, Mode::Single)
&& let Some(error) = single_mode_multiple_statements_error(&source_file, &parsed)
{
@@ -1595,7 +2140,13 @@ fn _compile_with_syntax_warning_handler<'a>(
if let Some(error) = barry_source.diagnostic(parsed.errors().first(), &source_file) {
return Err(error);
}
let parsed = first_parse_error(parsed, &source_file, mode)?;
let parsed = first_parse_error(
parsed,
&source_file,
mode,
barry_source.source(),
&parser_options,
)?;
if matches!(mode, Mode::Single)
&& let Some(error) = single_mode_blank_source_error(&source_file)
{
@@ -1823,14 +2374,20 @@ pub fn interactive_input(
options.clone(),
future_features.contains(core::bytecode::CodeFlags::FUTURE_BARRY_AS_BDFL),
);
let parsed = parser::parse_unchecked(prepared.source(), options);
let parsed = parser::parse_unchecked(prepared.source(), options.clone());
if let Some(error) = single_mode_multiple_statements_error(&source_file, &parsed) {
return Err(error);
}
if let Some(error) = prepared.diagnostic(parsed.errors().first(), &source_file) {
return Err(error);
}
let parsed = first_parse_error(parsed, &source_file, Mode::Single)?;
let parsed = first_parse_error(
parsed,
&source_file,
Mode::Single,
prepared.source(),
&options,
)?;
let ast::Mod::Module(module) = parsed.syntax() else {
unreachable!()
};
@@ -1926,7 +2483,8 @@ pub fn _compile_symtable(
let res = match mode {
Mode::Exec | Mode::Single | Mode::BlockExpr => {
pre_parse_source_error(&source_file)?;
let parsed = ruff_python_parser::parse_unchecked(barry_source.source(), parser_options);
let parsed =
ruff_python_parser::parse_unchecked(barry_source.source(), parser_options.clone());
if matches!(mode, Mode::Single)
&& let Some(error) = single_mode_multiple_statements_error(&source_file, &parsed)
{
@@ -1935,7 +2493,13 @@ pub fn _compile_symtable(
if let Some(error) = barry_source.diagnostic(parsed.errors().first(), &source_file) {
return Err(error);
}
let ast = first_parse_error(parsed, &source_file, mode)?;
let ast = first_parse_error(
parsed,
&source_file,
mode,
barry_source.source(),
&parser_options,
)?;
if let Some(error) =
post_parse_source_error(&source_file, ast.tokens(), &CompileOpts::default())
{
@@ -1958,11 +2522,18 @@ pub fn _compile_symtable(
}
Mode::Eval => {
pre_parse_source_error(&source_file)?;
let parsed = ruff_python_parser::parse_unchecked(barry_source.source(), parser_options);
let parsed =
ruff_python_parser::parse_unchecked(barry_source.source(), parser_options.clone());
if let Some(error) = barry_source.diagnostic(parsed.errors().first(), &source_file) {
return Err(error);
}
let ast = first_parse_error(parsed, &source_file, mode)?;
let ast = first_parse_error(
parsed,
&source_file,
mode,
barry_source.source(),
&parser_options,
)?;
if let Some(error) =
post_parse_source_error(&source_file, ast.tokens(), &CompileOpts::default())
{
+2 -2
View File
@@ -685,7 +685,7 @@ pub const DB: &[(&str, &str)] = &[
("_ast.Dict.__str__", "Return str(self)."),
("_ast.Dict.__subclasshook__", "Abstract classes can override this to customize issubclass().\n\nThis is invoked early on by abc.ABCMeta.__subclasscheck__().\nIt should return True, False or NotImplemented. If it returns\nNotImplemented, the normal algorithm is used. Otherwise, it\noverrides the normal algorithm (and the outcome is cached).\n"),
("_ast.Dict.__weakref__", "list of weak references to the object"),
("_ast.DictComp", "DictComp(expr key, expr value, comprehension* generators)"),
("_ast.DictComp", "DictComp(expr key, expr? value, comprehension* generators)"),
("_ast.DictComp.__class__", "the object's class"),
("_ast.DictComp.__delattr__", "Implement delattr(self, name)."),
("_ast.DictComp.__dir__", "Default dir() implementation."),
@@ -2886,7 +2886,7 @@ pub const DB: &[(&str, &str)] = &[
("_ast.excepthandler.__str__", "Return str(self)."),
("_ast.excepthandler.__subclasshook__", "Abstract classes can override this to customize issubclass().\n\nThis is invoked early on by abc.ABCMeta.__subclasscheck__().\nIt should return True, False or NotImplemented. If it returns\nNotImplemented, the normal algorithm is used. Otherwise, it\noverrides the normal algorithm (and the outcome is cached).\n"),
("_ast.excepthandler.__weakref__", "list of weak references to the object"),
("_ast.expr", "expr = BoolOp(boolop op, expr* values)\n | NamedExpr(expr target, expr value)\n | BinOp(expr left, operator op, expr right)\n | UnaryOp(unaryop op, expr operand)\n | Lambda(arguments args, expr body)\n | IfExp(expr test, expr body, expr orelse)\n | Dict(expr?* keys, expr* values)\n | Set(expr* elts)\n | ListComp(expr elt, comprehension* generators)\n | SetComp(expr elt, comprehension* generators)\n | DictComp(expr key, expr value, comprehension* generators)\n | GeneratorExp(expr elt, comprehension* generators)\n | Await(expr value)\n | Yield(expr? value)\n | YieldFrom(expr value)\n | Compare(expr left, cmpop* ops, expr* comparators)\n | Call(expr func, expr* args, keyword* keywords)\n | FormattedValue(expr value, int conversion, expr? format_spec)\n | Interpolation(expr value, constant str, int conversion, expr? format_spec)\n | JoinedStr(expr* values)\n | TemplateStr(expr* values)\n | Constant(constant value, string? kind)\n | Attribute(expr value, identifier attr, expr_context ctx)\n | Subscript(expr value, expr slice, expr_context ctx)\n | Starred(expr value, expr_context ctx)\n | Name(identifier id, expr_context ctx)\n | List(expr* elts, expr_context ctx)\n | Tuple(expr* elts, expr_context ctx)\n | Slice(expr? lower, expr? upper, expr? step)"),
("_ast.expr", "expr = BoolOp(boolop op, expr* values)\n | NamedExpr(expr target, expr value)\n | BinOp(expr left, operator op, expr right)\n | UnaryOp(unaryop op, expr operand)\n | Lambda(arguments args, expr body)\n | IfExp(expr test, expr body, expr orelse)\n | Dict(expr?* keys, expr* values)\n | Set(expr* elts)\n | ListComp(expr elt, comprehension* generators)\n | SetComp(expr elt, comprehension* generators)\n | DictComp(expr key, expr? value, comprehension* generators)\n | GeneratorExp(expr elt, comprehension* generators)\n | Await(expr value)\n | Yield(expr? value)\n | YieldFrom(expr value)\n | Compare(expr left, cmpop* ops, expr* comparators)\n | Call(expr func, expr* args, keyword* keywords)\n | FormattedValue(expr value, int conversion, expr? format_spec)\n | Interpolation(expr value, constant str, int conversion, expr? format_spec)\n | JoinedStr(expr* values)\n | TemplateStr(expr* values)\n | Constant(constant value, string? kind)\n | Attribute(expr value, identifier attr, expr_context ctx)\n | Subscript(expr value, expr slice, expr_context ctx)\n | Starred(expr value, expr_context ctx)\n | Name(identifier id, expr_context ctx)\n | List(expr* elts, expr_context ctx)\n | Tuple(expr* elts, expr_context ctx)\n | Slice(expr? lower, expr? upper, expr? step)"),
("_ast.expr.__class__", "the object's class"),
("_ast.expr.__delattr__", "Implement delattr(self, name)."),
("_ast.expr.__dir__", "Default dir() implementation."),
+15 -8
View File
@@ -858,16 +858,19 @@ fn expr_dict_comp_from_object_with_range(
object: &PyObject,
range: AstNodeRange,
) -> PyResult<ast::ExprDictComp> {
// Ruff uses a missing key for unpacking; Python's AST uses a missing value.
let key = get_required_node_field(vm, source_file, object, "key", "DictComp")?;
let value = get_node_field_opt(vm, object, "value")?
.map(|obj| Node::ast_from_object(vm, source_file, obj))
.transpose()?;
let (key, value) = match value {
Some(value) => (Some(key), value),
None => (None, key),
};
Ok(ast::ExprDictComp {
node_index: range.node_index(),
key: Some(get_required_node_field(
vm,
source_file,
object,
"key",
"DictComp",
)?),
value: get_required_node_field(vm, source_file, object, "value", "DictComp")?,
key,
value,
generators: get_node_list_field(vm, source_file, object, "generators", "DictComp")?
.into_boxed_slice(),
range: range.range,
@@ -883,6 +886,10 @@ impl Node for ast::ExprDictComp {
generators,
range,
} = self;
let (key, value) = match key {
Some(key) => (key, Some(value)),
None => (value, None),
};
let node = NodeAst
.into_ref_with_type(vm, pyast::NodeExprDictComp::static_type().to_owned())
.unwrap();
+1 -1
View File
@@ -1291,7 +1291,7 @@ const FIELD_TYPES: &[(&str, &[(&str, FieldType)])] = &[
"DictComp",
&[
("key", FieldType::Node("expr")),
("value", FieldType::Node("expr")),
("value", FieldType::Optional("expr")),
("generators", FieldType::ListOf("comprehension")),
],
),
+1 -1
View File
@@ -601,7 +601,7 @@ This will become an error in Python 3.20",
| Set(expr* elts)\n\
| ListComp(expr elt, comprehension* generators)\n\
| SetComp(expr elt, comprehension* generators)\n\
| DictComp(expr key, expr value, comprehension* generators)\n\
| DictComp(expr key, expr? value, comprehension* generators)\n\
| GeneratorExp(expr elt, comprehension* generators)\n\
| Await(expr value)\n\
| Yield(expr? value)\n\
+3 -4
View File
@@ -615,10 +615,9 @@ fn validate_expr(vm: &VirtualMachine, expr: &ast::Expr, ctx: ast::ExprContext) -
}
ast::Expr::DictComp(dict) => {
validate_comprehension(vm, &dict.generators)?;
let key = dict.key.as_deref().ok_or_else(|| {
vm.new_value_error("field 'key' is required for DictComp".to_owned())
})?;
validate_expr(vm, key, ast::ExprContext::Load)?;
if let Some(key) = &dict.key {
validate_expr(vm, key, ast::ExprContext::Load)?;
}
validate_expr(vm, &dict.value, ast::ExprContext::Load)
}
ast::Expr::Generator(generator) => {