-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Enable PEP 709 inlined comprehensions #7412
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 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -459,34 +459,62 @@ impl SymbolTableAnalyzer { | |
|
|
||
| symbol_table.symbols = info.0; | ||
|
|
||
| // PEP 709: Merge symbols from inlined comprehensions into parent scope | ||
| // Only merge symbols that are actually bound in the comprehension, | ||
| // not references to outer scope variables (Free symbols). | ||
| // PEP 709: Merge symbols from inlined comprehensions into parent scope. | ||
| // If a comprehension-bound name conflicts with an existing parent symbol, | ||
| // disable inlining for that comprehension (the save/restore mechanism | ||
| // via LOAD_FAST_AND_CLEAR / STORE_FAST can't handle scope mismatches). | ||
| const BOUND_FLAGS: SymbolFlags = SymbolFlags::ASSIGNED | ||
| .union(SymbolFlags::PARAMETER) | ||
| .union(SymbolFlags::ITER) | ||
| .union(SymbolFlags::ASSIGNED_IN_COMPREHENSION); | ||
|
|
||
| for sub_table in sub_tables.iter() { | ||
| if sub_table.comp_inlined { | ||
| for (name, sub_symbol) in &sub_table.symbols { | ||
| // Skip the .0 parameter - it's internal to the comprehension | ||
| if name == ".0" { | ||
| continue; | ||
| } | ||
| // Only merge symbols that are bound in the comprehension | ||
| // Skip Free references to outer scope variables | ||
| if !sub_symbol.flags.intersects(BOUND_FLAGS) { | ||
| continue; | ||
| } | ||
| // If the symbol doesn't exist in parent, add it | ||
| if !symbol_table.symbols.contains_key(name) { | ||
| let mut symbol = sub_symbol.clone(); | ||
| // Mark as local in parent scope | ||
| symbol.scope = SymbolScope::Local; | ||
| symbol_table.symbols.insert(name.clone(), symbol); | ||
| } | ||
| // Track symbols added by inlined comprehensions, so later comps | ||
| // can detect when a reference would resolve to a comp-local. | ||
| let mut comp_added_symbols: IndexSet<String> = IndexSet::default(); | ||
|
|
||
| for sub_table in sub_tables.iter_mut() { | ||
| if !sub_table.comp_inlined { | ||
| continue; | ||
| } | ||
| // Don't inline if the comprehension contains nested scopes | ||
| // (lambdas, inner comprehensions, nested functions) — these need | ||
| // Cell/Free variable handling that inlining doesn't support yet. | ||
| if !sub_table.sub_tables.is_empty() { | ||
| sub_table.comp_inlined = false; | ||
| continue; | ||
| } | ||
| // Don't inline if a bound comprehension name conflicts with parent | ||
| let has_bound_conflict = sub_table.symbols.iter().any(|(name, sym)| { | ||
| name != ".0" | ||
| && sym.flags.intersects(BOUND_FLAGS) | ||
| && symbol_table.symbols.contains_key(name) | ||
| }); | ||
| // Don't inline if a non-bound reference would resolve to a | ||
| // symbol added by a previous inlined comprehension | ||
| let has_ref_conflict = sub_table.symbols.iter().any(|(name, sym)| { | ||
| name != ".0" | ||
| && !sym.flags.intersects(BOUND_FLAGS) | ||
| && comp_added_symbols.contains(name) | ||
| }); | ||
| if has_bound_conflict || has_ref_conflict { | ||
| sub_table.comp_inlined = false; | ||
| continue; | ||
| } | ||
| for (name, sub_symbol) in &sub_table.symbols { | ||
| if name == ".0" { | ||
| continue; | ||
| } | ||
| if symbol_table.symbols.contains_key(name) { | ||
| continue; | ||
| } | ||
| let mut symbol = sub_symbol.clone(); | ||
| if sub_symbol.flags.intersects(BOUND_FLAGS) { | ||
| symbol.scope = SymbolScope::Local; | ||
| comp_added_symbols.insert(name.clone()); | ||
| } | ||
| // Non-bound symbols keep their analyzed scope from the | ||
| // comprehension sub_table (e.g., GlobalImplicit, Free). | ||
| symbol_table.symbols.insert(name.clone(), symbol); | ||
| } | ||
|
Comment on lines
+475
to
518
Contributor
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. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "1) Merge path and parent analysis:"
sed -n '460,535p' crates/codegen/src/symboltable.rs
echo
echo "2) Inner-scope promotion logic:"
sed -n '718,740p' crates/codegen/src/symboltable.rs
echo
echo "Expected: the second snippet has no guard skipping st.comp_inlined before returning SymbolScope::Cell."Repository: RustPython/RustPython Length of output: 4707 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "=== All calls to found_in_inner_scope() ==="
rg 'found_in_inner_scope' crates/codegen/src/symboltable.rs -B 2 -A 2
echo
echo "=== Lines around parent analysis (line 523) ==="
sed -n '520,530p' crates/codegen/src/symboltable.rs
echo
echo "=== Check if comp_inlined is used elsewhere in symbol analysis ==="
rg 'comp_inlined' crates/codegen/src/symboltable.rs | head -20Repository: RustPython/RustPython Length of output: 1439 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "=== Confirm analyze_symbol definition and all params passed ==="
sed -n '635,680p' crates/codegen/src/symboltable.rs
echo
echo "=== Check comment context for comp_inlined behavior ==="
sed -n '462,475p' crates/codegen/src/symboltable.rs
echo
echo "=== Verify the field definition and its purpose ==="
rg 'comp_inlined:' crates/codegen/src/symboltable.rs -B 5 -A 1 | head -30Repository: RustPython/RustPython Length of output: 3373 Skip inlined comprehensions during closure promotion. After merging inlined comprehension symbols into the parent scope (lines 475–518), the parent analysis at line 523 still consults the full Modify fn found_in_inner_scope(
&self,
sub_tables: &[SymbolTable],
name: &str,
st_typ: CompilerScope,
) -> Option<SymbolScope> {
sub_tables.iter().find_map(|st| {
+ if st.comp_inlined {
+ return None;
+ }
let sym = st.symbols.get(name)?;
if sym.scope == SymbolScope::Free || sym.flags.contains(SymbolFlags::FREE_CLASS) {
if st_typ == CompilerScope::Class && name != "__class__" {
None
} else {🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
|
|
@@ -2037,13 +2065,31 @@ impl SymbolTableBuilder { | |
| self.line_index_start(range), | ||
| ); | ||
|
|
||
| // PEP 709: inlined comprehensions are not yet implemented in the | ||
| // compiler (is_inlined_comprehension_context always returns false), | ||
| // so do NOT mark comp_inlined here. Setting it would cause the | ||
| // symbol-table analyzer to merge comprehension-local symbols into | ||
| // the parent scope, while the compiler still emits a separate code | ||
| // object — leading to the merged symbols being missing from the | ||
| // comprehension's own symbol table lookup. | ||
| // PEP 709: Mark non-generator comprehensions for inlining, | ||
| // but only inside function-like scopes (fastlocals). | ||
| // Module/class scope uses STORE_NAME which is incompatible | ||
| // with LOAD_FAST_AND_CLEAR / STORE_FAST save/restore. | ||
| // Note: tables.last() is the comprehension scope we just pushed, | ||
| // so we check the second-to-last for the parent scope. | ||
| if !is_generator { | ||
| let parent_is_func = self | ||
| .tables | ||
| .iter() | ||
| .rev() | ||
| .nth(1) | ||
| .is_some_and(|t| { | ||
| matches!( | ||
| t.typ, | ||
| CompilerScope::Function | ||
| | CompilerScope::AsyncFunction | ||
| | CompilerScope::Lambda | ||
| | CompilerScope::Comprehension | ||
| ) | ||
| }); | ||
| if parent_is_func { | ||
| self.tables.last_mut().unwrap().comp_inlined = true; | ||
| } | ||
| } | ||
|
|
||
| // Register the passed argument to the generator function as the name ".0" | ||
| self.register_name(".0", SymbolUsage::Parameter, range)?; | ||
|
|
||
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.
Don't route async comprehensions through the inline path yet.
This branch now makes async list/set/dict comprehensions hit
compile_inlined_comprehension(), but the inline async loop still lacks theSetupFinally { delta: after_block }/AsyncComprehensionGeneratorscaffolding that the non-inlined path uses at Lines 7733-7745. In the inline version,after_blockis never targeted for the async case, soEND_ASYNC_FORat Line 7989 will not run when the iterator terminates.Possible short-term fix
🤖 Prompt for AI Agents