Skip to content

Bytecode parity CFG normalization and jump cleanup#7721

Merged
youknowone merged 7 commits into
RustPython:mainfrom
youknowone:bytecode-parity-cfg
Apr 30, 2026
Merged

Bytecode parity CFG normalization and jump cleanup#7721
youknowone merged 7 commits into
RustPython:mainfrom
youknowone:bytecode-parity-cfg

Conversation

@youknowone
Copy link
Copy Markdown
Member

@youknowone youknowone commented Apr 28, 2026

Summary by CodeRabbit

  • Bug Fixes

    • Fixed control flow handling in complex try/except/finally blocks with nested conditionals.
    • Corrected global variable resolution within class body scopes.
  • Refactor

    • Revised constant folding and expression optimization mechanisms.
    • Changed static attribute collection approach during code compilation.
  • Tests

    • Extended bytecode generation and control-flow behavior test coverage.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 28, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: b9d476d1-2d2b-4eb8-9544-28b3d406bdd9

📥 Commits

Reviewing files that changed from the base of the PR and between 238d1e7 and c267535.

📒 Files selected for processing (1)
  • crates/codegen/src/compile.rs

📝 Walkthrough

Walkthrough

This PR refactors bytecode code generation with improved constant handling, enhanced try/finally control-flow shaping through NOP/jump management, better global name resolution in class scopes, incremental static attribute collection, and extensive structural test updates.

Changes

Cohort / File(s) Summary
Constant & Name Resolution
crates/codegen/src/compile.rs
Refactors slice constant folding to return Option<ConstantData> without emitting directly; stops treating tuple literals as constants; adds __debug__ constant folding for Name loads; enhances compile_name to detect class-body globals and adjust NameOp selection accordingly.
Control-Flow & NOP/Jump Management
crates/codegen/src/compile.rs, crates/codegen/src/ir.rs
Introduces predicates for try/finally entry NOP preservation when try body ends with return/raise; adds per-instruction metadata flags (remove_no_location_nop, lineno_override); converts zero-width jumps to NOPs in late linearization; refactors remove_nops() to preserve/drop NOPs using flags and CFG context; replaces many Jump emissions with JumpNoInterrupt.
CFG Reordering & Deoptimization
crates/codegen/src/ir.rs
Extends CFG reorder heuristics with suspension-point, stop-iteration, and WITH/suppress-exit constraints; adds borrow deoptimization passes (deoptimize_borrow_after_protected_store_tail, deoptimize_borrow_in_protected_attr_chain_tail); restructures block classification and gating in multi-handler resume logic.
Static Attribute Collection
crates/codegen/src/compile.rs
Removes whole-AST scanning for __static_attributes__ and instead incrementally collects self.<attr> = ... assignments during compile_store via maybe_add_static_attribute_to_class.
Tests & Minor Fixes
crates/codegen/src/compile.rs, crates/codegen/src/symboltable.rs
Expands bytecode/shaping test suite with structural assertions for CFG/jump/no-location behavior, protected-borrow deopt tails, slice/slice-bound folding, static attributes, and __debug__ constant folding; corrects error message from "type alias expect name" to "type alias expects name".

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Poem

🐰 Through jumps and constants we now leap,
No NOPs left forgotten or deep,
Class globals dance in perfect place,
While CFG threads pace to pace,
Static attributes trim and fold—
Bytecode stories freshly told!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 52.53% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Bytecode parity CFG normalization and jump cleanup' directly reflects the main changes: CFG/jump behavior improvements, no-location NOP handling, and bytecode shaping updates across the codebase.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 6/8 reviews remaining, refill in 13 minutes and 18 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

@youknowone youknowone force-pushed the bytecode-parity-cfg branch from c9dec96 to ded263c Compare April 29, 2026 19:49
Reorder try/except/else/finally to emit else+finally before
except handlers matching CPython layout. Add set_no_location
for cleanup blocks. Extend CFG reorder pass to handle true-path
jump-back for generators, break/continue, and assert in loops.
Add stop-iteration error handler awareness to block protection.
remove_nops and remove_redundant_nops_in_blocks repeated
has_jump_predecessor / has_plain_jump_predecessor / target lookups
per block, scanning all blocks each time. With ~200,000 if blocks
this became O(B^2 * I) and timed out test_compile.test_stack_overflow.
Fold the three flags into one O(B * I) pass via
compute_target_predecessor_flags.
@youknowone youknowone force-pushed the bytecode-parity-cfg branch from ded263c to 23c689d Compare April 29, 2026 23:57
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 29, 2026

📦 Library Dependencies

The following Lib/ modules were modified. Here are their dependencies:

(module 'class test_dis' not found)

Legend:

  • [+] path exists in CPython
  • [x] up-to-date, [ ] outdated

@youknowone youknowone marked this pull request as ready for review April 30, 2026 02:31
Comment thread .cspell.json Outdated
"jitted",
"jitting",
"kwonly",
"lolcatz",
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.

adding vocabulary only for string in test data is bad idea

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Maybe leave it as a single ignore statement in one file?

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.

The test was duplication of test_dis.py. removed the test

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/codegen/src/compile.rs`:
- Around line 10175-10239: try_fold_slice_constant_part is too narrow (it only
matches literal AST variants) so slice bounds like unary-folded values and
special names (e.g. __debug__) aren't recognized; update
try_fold_slice_constant_part to first call the existing try_fold_constant_expr
(or delegate to the same folding logic) for the given expr and, if that returns
Some(ConstantData), return it; keep the existing literal matching as a fallback
only if try_fold_constant_expr isn't available/applicable, and preserve error
mapping (map_err(|e| self.error(e))?) and the CompileResult<Option<...>>
contract so try_fold_constant_slice can treat slice bounds the same as other
constant folding paths.
- Around line 574-607: statement_ends_with_scope_exit currently omits
ast::Stmt::Try and so misses nested-try cases; update
statement_ends_with_scope_exit to handle ast::Stmt::Try (match the Try arm) and
return true when the try has a non-empty finalbody or when it has handlers and
its try body ends with a scope-exit (mirror the logic used in
preserves_finally_entry_nop: check finalbody.is_empty() and/or
(!handlers.is_empty() && Self::statements_end_with_scope_exit(body)) so nested
try inside if/else is treated as ending with a scope-exit).

In `@crates/codegen/src/ir.rs`:
- Around line 4085-4088: The code currently bails out when
first_unprotected_suffix(block) returns Some(_) but it should treat that
position as the start of the deopt segment instead of continuing; change the
branch that checks same_block_tail_start so that when Some(start_idx) is
returned you set the segment start to that index (i.e., begin splitting/handling
the deopt segment at start_idx) and proceed with the existing deopt/splitting
logic, rather than continuing; ensure this preserves behavior elsewhere (blocks
still split only at jumps) and covers the same-block protected→unprotected tail
case (refer to first_unprotected_suffix, same_block_tail_start, and the
surrounding block-splitting/deopt logic).
- Around line 4410-4417: The ReturnIter matcher currently only checks for
Instruction::Call after the current cursor, so keyword-call variants are
ignored; update the branch that inspects real_instrs.get(cursor + 1) to also
accept Instruction::CallKw (in addition to Instruction::Call) when deciding to
produce DeoptKind::ReturnIter (with tail_start_idx: cursor + 2), i.e. match both
Call and CallKw against the real instruction (same logic that uses
Instruction::GetIter afterwards) so CallKw triggers the same deopt path;
reference symbols: Instruction::Call, Instruction::CallKw,
DeoptKind::ReturnIter, Instruction::GetIter, real_instrs, cursor.
- Around line 2563-2572: starts_after_for_cleanup currently only checks
immediate fallthroughs via block.next and misses cases where an END_FOR/POP_ITER
cleanup falls through through one or more empty blocks; update the computation
of fallthrough_predecessors (and thus starts_after_for_cleanup) to walk the
chain of next pointers from each block (using BlockIdx and block.next) until you
find the first non-empty successor (or NULL) and record that predecessor index,
then compute starts_after_for_cleanup by calling ends_with_for_cleanup on that
resolved successor index instead of the immediate block.next so cleanup
fallthroughs through empty blocks are correctly detected.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 368e1a8e-a411-4fc1-979f-9354540ded8e

📥 Commits

Reviewing files that changed from the base of the PR and between 6e895fb and 23c689d.

⛔ Files ignored due to path filters (1)
  • Lib/test/test_dis.py is excluded by !Lib/**
📒 Files selected for processing (4)
  • .cspell.json
  • crates/codegen/src/compile.rs
  • crates/codegen/src/ir.rs
  • crates/codegen/src/symboltable.rs

Comment on lines +574 to +607
fn statement_ends_with_scope_exit(stmt: &ast::Stmt) -> bool {
match stmt {
ast::Stmt::Return(_) | ast::Stmt::Raise(_) => true,
ast::Stmt::If(ast::StmtIf {
body,
elif_else_clauses,
..
}) => {
let has_else = elif_else_clauses
.last()
.is_some_and(|clause| clause.test.is_none());
has_else
&& Self::statements_end_with_scope_exit(body)
&& elif_else_clauses
.iter()
.all(|clause| Self::statements_end_with_scope_exit(&clause.body))
}
_ => false,
}
}

fn preserves_finally_entry_nop(body: &[ast::Stmt]) -> bool {
body.last().is_some_and(|stmt| match stmt {
ast::Stmt::Try(ast::StmtTry {
body,
handlers,
finalbody,
..
}) => {
!finalbody.is_empty()
|| (!handlers.is_empty() && Self::statements_end_with_scope_exit(body))
}
_ => false,
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle nested try statements in the scope-exit heuristic.

statement_ends_with_scope_exit() never returns true for ast::Stmt::Try, so preserves_finally_entry_nop() still drops the outer finally-entry anchor for shapes like if cond: try: return ... finally: ... else: try: return ... finally: .... That is the same nested-finally case this helper is meant to preserve.

Possible fix
     fn statement_ends_with_scope_exit(stmt: &ast::Stmt) -> bool {
         match stmt {
             ast::Stmt::Return(_) | ast::Stmt::Raise(_) => true,
+            ast::Stmt::Try(ast::StmtTry {
+                body,
+                handlers,
+                orelse,
+                finalbody,
+                ..
+            }) => {
+                let handlers_exit = !handlers.is_empty()
+                    && handlers.iter().all(|handler| {
+                        let ast::ExceptHandler::ExceptHandler(
+                            ast::ExceptHandlerExceptHandler { body, .. },
+                        ) = handler;
+                        Self::statements_end_with_scope_exit(body)
+                    });
+                Self::statements_end_with_scope_exit(body)
+                    && (orelse.is_empty() || Self::statements_end_with_scope_exit(orelse))
+                    && (!finalbody.is_empty() || handlers_exit)
+            }
             ast::Stmt::If(ast::StmtIf {
                 body,
                 elif_else_clauses,
                 ..
             }) => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/codegen/src/compile.rs` around lines 574 - 607,
statement_ends_with_scope_exit currently omits ast::Stmt::Try and so misses
nested-try cases; update statement_ends_with_scope_exit to handle ast::Stmt::Try
(match the Try arm) and return true when the try has a non-empty finalbody or
when it has handlers and its try body ends with a scope-exit (mirror the logic
used in preserves_finally_entry_nop: check finalbody.is_empty() and/or
(!handlers.is_empty() && Self::statements_end_with_scope_exit(body)) so nested
try inside if/else is treated as ending with a scope-exit).

Comment thread crates/codegen/src/compile.rs Outdated
Comment thread crates/codegen/src/ir.rs
Comment thread crates/codegen/src/ir.rs
Comment on lines +4085 to +4088
let same_block_tail_start = first_unprotected_suffix(block);
if same_block_tail_start.is_some() {
continue;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don’t skip the same-block protected tail case.

first_unprotected_suffix() returning Some(_) is the exact signal that this block already contains the protected→unprotected transition you want to deopt, but the current code bails out instead of starting the segment there. Because blocks are only split at jumps, not at POP_BLOCK, this leaves reachable same-block store-tail cases untreated.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/codegen/src/ir.rs` around lines 4085 - 4088, The code currently bails
out when first_unprotected_suffix(block) returns Some(_) but it should treat
that position as the start of the deopt segment instead of continuing; change
the branch that checks same_block_tail_start so that when Some(start_idx) is
returned you set the segment start to that index (i.e., begin splitting/handling
the deopt segment at start_idx) and proceed with the existing deopt/splitting
logic, rather than continuing; ensure this preserves behavior elsewhere (blocks
still split only at jumps) and covers the same-block protected→unprotected tail
case (refer to first_unprotected_suffix, same_block_tail_start, and the
surrounding block-splitting/deopt logic).

Comment thread crates/codegen/src/ir.rs Outdated
- try_fold_constant_slice now delegates to try_fold_constant_expr so
  slice bounds accept the same constants other folding paths do
  (unary-folded values, __debug__, etc.).
- remove_nops resolves fallthrough predecessors through empty blocks
  via next_nonempty_block before checking ends_with_for_cleanup.
- should_deopt_borrowed_attr_chain ReturnIter matcher now accepts
  Instruction::CallKw alongside Instruction::Call, matching the
  Call/CallKw treatment in the surrounding deopt trigger check.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (3)
crates/codegen/src/ir.rs (2)

2563-2568: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Don’t let empty blocks overwrite the cleanup predecessor.

This still breaks on A -> empty -> B: Line 2567 gets overwritten by the empty block, so starts_after_for_cleanup[B] becomes false and the block-start no-location NOP can survive incorrectly. Record only the last non-empty fallthrough predecessor here.

Suggested fix
 let mut fallthrough_predecessors = vec![None; self.blocks.len()];
 for (pred_idx, block) in self.blocks.iter().enumerate() {
+    if block.instructions.is_empty() {
+        continue;
+    }
     let next = next_nonempty_block(&self.blocks, block.next);
     if next != BlockIdx::NULL {
         fallthrough_predecessors[next.idx()] = Some(pred_idx);
     }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/codegen/src/ir.rs` around lines 2563 - 2568, The
fallthrough_predecessors assignment currently allows empty blocks to overwrite a
non-empty predecessor (breaking cases like A -> empty -> B); in the loop over
self.blocks (using fallthrough_predecessors, next_nonempty_block, and pred_idx)
only set fallthrough_predecessors[next.idx()] = Some(pred_idx) when the current
block is non-empty (i.e., skip assigning for empty blocks) so that the last
non-empty fallthrough predecessor is recorded and doesn't get clobbered.

4086-4088: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use the same-block unprotected suffix instead of skipping it.

first_unprotected_suffix() returning Some(start) is the exact case this pass needs to deopt, but Line 4087 bails out. That still leaves reachable same-block protected→unprotected store tails untreated.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/codegen/src/ir.rs` around lines 4086 - 4088, The code currently skips
blocks when first_unprotected_suffix(block) returns Some(start), but this is the
exact case that should trigger deoptimization; instead of continuing, use the
returned start index to handle the same-block protected→unprotected store tail
(i.e., call the deopt/emission logic with same_block_tail_start or the start
value from first_unprotected_suffix). Update the branch that checks
same_block_tail_start to invoke the same deopt path used for cross-block tails
(reusing the deopt emission code path or helper function) so same-block
unprotected suffixes are processed rather than ignored.
crates/codegen/src/compile.rs (1)

574-607: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Broaden the finally-entry preservation heuristic.

Line 595 still only preserves the anchor when the trailing statement is a literal Stmt::Try, and Line 574 still treats nested Stmt::Try as non-exiting. That means an outer try/finally whose body ends in an if/else whose branches each end with try ... finally: return/raise will still drop the finally-entry NOP and diverge from CPython’s shape.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/codegen/src/compile.rs` around lines 574 - 607, The heuristic needs
broadening: update statement_ends_with_scope_exit and
preserves_finally_entry_nop so nested Try nodes are treated as scope-exiting
when appropriate and the trailing anchor-preservation checks look through If/Try
recursively. Specifically, enhance statement_ends_with_scope_exit (currently
matching Return/Raise/If) to also recognize ast::Stmt::Try as exiting when its
finalbody is non-empty or when it has handlers and its try body itself ends with
a scope exit (reusing Self::statements_end_with_scope_exit). Then change
preserves_finally_entry_nop to not only match a literal trailing Stmt::Try but
to call statement_ends_with_scope_exit on the last statement (or otherwise
inspect the last stmt via the same recursive logic) so an outer try/finally
whose final visible control flow is from nested try/finally-return/raise inside
an if/else keeps the finally-entry NOP; use the existing helpers
(statement_ends_with_scope_exit, statements_end_with_scope_exit) and symbols
(preserves_finally_entry_nop, statement_ends_with_scope_exit) to implement this.
🧹 Nitpick comments (1)
crates/codegen/src/compile.rs (1)

10181-10207: ⚡ Quick win

Deduplicate the slice-bound folding logic.

These three match blocks only differ by the input expression. Pulling the fold-or-None logic into one local helper will keep this path from drifting again.

♻️ Suggested refactor
     fn try_fold_constant_slice(
         &mut self,
         lower: Option<&ast::Expr>,
         upper: Option<&ast::Expr>,
         step: Option<&ast::Expr>,
     ) -> CompileResult<Option<ConstantData>> {
-        let start = match lower {
-            Some(expr) => {
-                let Some(constant) = self.try_fold_constant_expr(expr)? else {
-                    return Ok(None);
-                };
-                constant
-            }
-            None => ConstantData::None,
-        };
-        let stop = match upper {
-            Some(expr) => {
-                let Some(constant) = self.try_fold_constant_expr(expr)? else {
-                    return Ok(None);
-                };
-                constant
-            }
-            None => ConstantData::None,
-        };
-        let step = match step {
-            Some(expr) => {
-                let Some(constant) = self.try_fold_constant_expr(expr)? else {
-                    return Ok(None);
-                };
-                constant
-            }
-            None => ConstantData::None,
-        };
+        let mut fold_part = |expr: Option<&ast::Expr>| -> CompileResult<Option<ConstantData>> {
+            match expr {
+                Some(expr) => self.try_fold_constant_expr(expr),
+                None => Ok(Some(ConstantData::None)),
+            }
+        };
+
+        let Some(start) = fold_part(lower)? else {
+            return Ok(None);
+        };
+        let Some(stop) = fold_part(upper)? else {
+            return Ok(None);
+        };
+        let Some(step) = fold_part(step)? else {
+            return Ok(None);
+        };
 
         Ok(Some(ConstantData::Slice {
             elements: Box::new([start, stop, step]),
         }))
     }

As per coding guidelines, "When branches differ only in a value but share common logic, extract the differing value first, then call the common logic once to avoid duplicate code".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/codegen/src/compile.rs` around lines 10181 - 10207, The three
identical match blocks for start/stop/step duplicate the fold-or-None logic;
extract a small local helper (e.g., a closure fold_bound = |opt_expr| ->
Result<ConstantData, _> or similar) that takes an Option<Expr>, calls
self.try_fold_constant_expr(opt_expr) when Some, returns the folded ConstantData
or propagates the outer Ok(None) semantics when try_fold_constant_expr yields
None, and returns ConstantData::None when input is None; then call this helper
to set start, stop and step instead of repeating the match. Ensure the helper
uses the same error propagation (the ? on try_fold_constant_expr) and preserves
the current behavior for lower/upper/step and the created start/stop/step
bindings.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@crates/codegen/src/compile.rs`:
- Around line 574-607: The heuristic needs broadening: update
statement_ends_with_scope_exit and preserves_finally_entry_nop so nested Try
nodes are treated as scope-exiting when appropriate and the trailing
anchor-preservation checks look through If/Try recursively. Specifically,
enhance statement_ends_with_scope_exit (currently matching Return/Raise/If) to
also recognize ast::Stmt::Try as exiting when its finalbody is non-empty or when
it has handlers and its try body itself ends with a scope exit (reusing
Self::statements_end_with_scope_exit). Then change preserves_finally_entry_nop
to not only match a literal trailing Stmt::Try but to call
statement_ends_with_scope_exit on the last statement (or otherwise inspect the
last stmt via the same recursive logic) so an outer try/finally whose final
visible control flow is from nested try/finally-return/raise inside an if/else
keeps the finally-entry NOP; use the existing helpers
(statement_ends_with_scope_exit, statements_end_with_scope_exit) and symbols
(preserves_finally_entry_nop, statement_ends_with_scope_exit) to implement this.

In `@crates/codegen/src/ir.rs`:
- Around line 2563-2568: The fallthrough_predecessors assignment currently
allows empty blocks to overwrite a non-empty predecessor (breaking cases like A
-> empty -> B); in the loop over self.blocks (using fallthrough_predecessors,
next_nonempty_block, and pred_idx) only set fallthrough_predecessors[next.idx()]
= Some(pred_idx) when the current block is non-empty (i.e., skip assigning for
empty blocks) so that the last non-empty fallthrough predecessor is recorded and
doesn't get clobbered.
- Around line 4086-4088: The code currently skips blocks when
first_unprotected_suffix(block) returns Some(start), but this is the exact case
that should trigger deoptimization; instead of continuing, use the returned
start index to handle the same-block protected→unprotected store tail (i.e.,
call the deopt/emission logic with same_block_tail_start or the start value from
first_unprotected_suffix). Update the branch that checks same_block_tail_start
to invoke the same deopt path used for cross-block tails (reusing the deopt
emission code path or helper function) so same-block unprotected suffixes are
processed rather than ignored.

---

Nitpick comments:
In `@crates/codegen/src/compile.rs`:
- Around line 10181-10207: The three identical match blocks for start/stop/step
duplicate the fold-or-None logic; extract a small local helper (e.g., a closure
fold_bound = |opt_expr| -> Result<ConstantData, _> or similar) that takes an
Option<Expr>, calls self.try_fold_constant_expr(opt_expr) when Some, returns the
folded ConstantData or propagates the outer Ok(None) semantics when
try_fold_constant_expr yields None, and returns ConstantData::None when input is
None; then call this helper to set start, stop and step instead of repeating the
match. Ensure the helper uses the same error propagation (the ? on
try_fold_constant_expr) and preserves the current behavior for lower/upper/step
and the created start/stop/step bindings.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 3a985768-6733-40e0-83a9-169f9b5a540b

📥 Commits

Reviewing files that changed from the base of the PR and between 23c689d and 238d1e7.

📒 Files selected for processing (2)
  • crates/codegen/src/compile.rs
  • crates/codegen/src/ir.rs

@youknowone youknowone changed the title Bytecode parity cfg Bytecode parity CFG normalization and jump cleanup Apr 30, 2026
@youknowone youknowone force-pushed the bytecode-parity-cfg branch from 2c31fa9 to c267535 Compare April 30, 2026 13:47
@youknowone youknowone merged commit e79df4a into RustPython:main Apr 30, 2026
21 checks passed
@youknowone youknowone deleted the bytecode-parity-cfg branch April 30, 2026 14:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants