Fix future annotation block alignment - #8506
Conversation
📝 WalkthroughWalkthroughThe compiler now consumes hidden annotation symbols for unannotated functions. A regression test verifies that a later annotated function keeps its ChangesAnnotation scope handling
Estimated code review effort: 2 (Simple) | ~10 minutes Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
crates/codegen/src/compile.rs (1)
5085-5094: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueOptional: extract a shared helper for the repeated push-then-pop pattern.
compile_annotations_closure(Line 5091-5093) andconsume_function_annotation_symbol_table_if_used(Line 10038-10045) both implement "push the next annotation symbol table if one exists, then pop it" with a different guard condition. Extracting a small private helper, for examplefn consume_next_annotation_symbol_table_if_present(&mut self) -> bool, and calling it from both sites would remove the duplicated push/pop pair while keeping each caller's guard condition distinct.♻️ Proposed helper extraction
+ /// Push the next annotation symbol table, if one is available, then pop + /// it immediately. Returns whether a table was consumed. + fn consume_next_annotation_symbol_table_if_present(&mut self) -> bool { + if self.push_annotation_symbol_table() { + self.pop_annotation_symbol_table(); + true + } else { + false + } + } + fn compile_annotations_closure( ... ) -> CompileResult<bool> { if !self.next_function_annotation_symbol_table_uses_annotations() { - if self.push_annotation_symbol_table() { - self.pop_annotation_symbol_table(); - } + self.consume_next_annotation_symbol_table_if_present(); return Ok(false); }Also applies to: 10034-10047
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/codegen/src/compile.rs` around lines 5085 - 5094, Extract the repeated push-then-pop logic from compile_annotations_closure and consume_function_annotation_symbol_table_if_used into a private helper such as consume_next_annotation_symbol_table_if_present, returning whether a symbol table was consumed. Replace both duplicated blocks with the helper while preserving each caller’s existing guard condition and behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/codegen/src/compile.rs`:
- Around line 5085-5094: Update consume_function_annotation_symbol_table_if_used
so it advances past the hidden annotation block even when
next_function_annotation_symbol_table_uses_annotations() is false, while
preserving the existing missing-table error behavior for used annotations.
Ensure skipped unannotated functions consume their block before returning, so
subsequent annotated functions receive the correct block.
---
Nitpick comments:
In `@crates/codegen/src/compile.rs`:
- Around line 5085-5094: Extract the repeated push-then-pop logic from
compile_annotations_closure and consume_function_annotation_symbol_table_if_used
into a private helper such as consume_next_annotation_symbol_table_if_present,
returning whether a symbol table was consumed. Replace both duplicated blocks
with the helper while preserving each caller’s existing guard condition and
behavior.
🪄 Autofix
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 Plus
Run ID: 93b30280-c6d9-49fe-b5b7-3e6f177a541a
📒 Files selected for processing (1)
crates/codegen/src/compile.rs
| if !self.next_function_annotation_symbol_table_uses_annotations() { | ||
| // CPython creates a hidden AnnotationBlock for every function | ||
| // signature under `from __future__ import annotations`, including | ||
| // an unannotated one. It still belongs to this function: consume | ||
| // it so the next function sees its own block rather than remaining | ||
| // pinned to this unused entry. | ||
| if self.push_annotation_symbol_table() { | ||
| self.pop_annotation_symbol_table(); | ||
| } | ||
| return Ok(false); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Trace callers of consume_function_annotation_symbol_table_if_used
# and check whether skipped FunctionDef nodes without annotations can reach it
# under future annotations.
set -euo pipefail
rg -n -C 5 'consume_function_annotation_symbol_table_if_used' crates/codegen/src/compile.rsRepository: RustPython/RustPython
Length of output: 1082
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '9960,10420p' crates/codegen/src/compile.rs
printf '\n--- annotation-table helpers ---\n'
sed -n '1560,1670p' crates/codegen/src/compile.rs
printf '\n--- relevant tests and visitor references ---\n'
rg -n -C 8 'SkippedStatementScopeVisitor|future_unannotated_function|except-handler|except handler|annotation_symbol_table|hidden_annotation_blocks' crates/codegen/src/compile.rsRepository: RustPython/RustPython
Length of output: 38783
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- skipped-scope call sites ---'
rg -n -C 12 'consume_skipped_nested_scopes_in_(statements|except_handlers|expr)' crates/codegen/src/compile.rs
printf '%s\n' '--- exception compilation paths ---'
rg -n -C 18 'consume_skipped_nested_scopes_in_except_handlers|SkippedHandlerScopeVisitor|compile.*except|ExceptHandler' crates/codegen/src/compile.rs
printf '%s\n' '--- symbol-table construction for hidden annotation blocks ---'
rg -n -C 12 'hidden_annotation_blocks|next_hidden_annotation_block|annotations_used.*Annotation|AnnotationBlock' crates/codegen/src crates/compiler crates -g '*.rs'Repository: RustPython/RustPython
Length of output: 50377
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- function-definition compilation ---'
rg -n -C 20 'Stmt::FunctionDef|compile_function|compile_annotations_closure' crates/codegen/src/compile.rs | head -n 260
printf '%s\n' '--- symbol-table cursor helpers ---'
rg -n -C 16 'fn push_symbol_table|fn pop_symbol_table|fn current_symbol_table_cursors|fn set_symbol_table_cursors|fn compile_statements' crates/codegen/src/compile.rs
printf '%s\n' '--- try/except cursor movement ---'
sed -n '4327,4425p' crates/codegen/src/compile.rsRepository: RustPython/RustPython
Length of output: 21710
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
source = Path("crates/codegen/src/compile.rs").read_text()
required = [
"self.consume_skipped_nested_scopes_in_except_handlers(handlers)?;",
"self.compile_statements(orelse)?;",
"self.set_symbol_table_cursors(handler_symbol_table_cursors);",
"self.compiler.consume_function_annotation_symbol_table_if_used()",
"self.visit_parameter_defaults(parameters);",
"self.consume_function_annotation_scope_if_used();",
"self.consume_scope();",
"if !self.next_function_annotation_symbol_table_uses_annotations()",
"return Ok(());",
]
for fragment in required:
assert fragment in source, f"missing source fragment: {fragment}"
# Model the relevant cursor operations for:
# handler: def plain(x): pass
# orelse: def annotated(x: int): pass
hidden_blocks = [("plain", False), ("annotated", True)]
hidden_cursor = 0
sub_cursor = 0
# SkippedStatementScopeVisitor visits the handler FunctionDef.
# The current implementation does not consume an unused annotation block.
uses_annotations = hidden_blocks[hidden_cursor][1]
if uses_annotations:
hidden_cursor += 1
sub_cursor += 1 # consume the FunctionBlock
assert hidden_cursor == 0
assert sub_cursor == 1
# Codegen then compiles the orelse. Its function definition consumes the
# still-current hidden block, so it sees the handler's unused block.
orelse_seen = hidden_blocks[hidden_cursor]
assert orelse_seen == ("plain", False), orelse_seen
assert orelse_seen[1] is False
print("reachable path: try/except skips handler scopes before compiling orelse")
print("current skipped consumer leaves the unannotated handler AnnotationBlock")
print("next orelse function therefore receives the handler block")
print("the sibling gap reproduces the same hidden-block cursor misalignment")
PYRepository: RustPython/RustPython
Length of output: 339
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
import re
source = Path("crates/codegen/src/compile.rs").read_text()
required_patterns = [
r"consume_skipped_nested_scopes_in_except_handlers\(handlers\)\?",
r"compile_statements\(orelse\)\?",
r"set_symbol_table_cursors\(handler_symbol_table_cursors\)",
r"compiler\s*\.\s*consume_function_annotation_symbol_table_if_used\(\)",
r"visit_parameter_defaults\(parameters\)",
r"consume_function_annotation_scope_if_used\(\)",
r"consume_scope\(\)",
r"if !self\.next_function_annotation_symbol_table_uses_annotations\(\)",
r"return Ok\(\(\)\);",
]
for pattern in required_patterns:
assert re.search(pattern, source), f"missing source pattern: {pattern}"
# Under future annotations, the parent scope stores one hidden AnnotationBlock
# per function in AST order, including unannotated functions.
hidden_blocks = [("handler_plain", False), ("orelse_annotated", True)]
hidden_cursor = 0
sub_cursor = 0
# SkippedStatementScopeVisitor visits the handler FunctionDef.
# The sibling consumer returns without advancing for an unused block.
uses_annotations = hidden_blocks[hidden_cursor][1]
if uses_annotations:
hidden_cursor += 1
sub_cursor += 1 # consume the FunctionBlock
assert hidden_cursor == 0
assert sub_cursor == 1
# Orelse is compiled before the real handler body. Its next function therefore
# observes the handler's unused AnnotationBlock.
orelse_observed = hidden_blocks[hidden_cursor]
assert orelse_observed == ("handler_plain", False), orelse_observed
print("reachable: try/except skips handler scopes before compiling orelse")
print("skipped unannotated FunctionDef leaves its hidden AnnotationBlock")
print("next orelse FunctionDef observes the stale hidden block")
print("sibling consumer has the same cursor-alignment gap")
PYRepository: RustPython/RustPython
Length of output: 399
Consume hidden annotation blocks for skipped unannotated functions.
consume_function_annotation_symbol_table_if_used returns without advancing when annotations_used is false. Under future annotations, every function still has a hidden AnnotationBlock. A skipped handler function can therefore leave its block current, causing a later annotated function in orelse to consume the wrong block. Advance the block for both used and unused annotations, while retaining the missing-table error for used blocks.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/codegen/src/compile.rs` around lines 5085 - 5094, Update
consume_function_annotation_symbol_table_if_used so it advances past the hidden
annotation block even when
next_function_annotation_symbol_table_uses_annotations() is false, while
preserving the existing missing-table error behavior for used annotations.
Ensure skipped unannotated functions consume their block before returning, so
subsequent annotated functions receive the correct block.
Summary
from __future__ import annotationsRoot cause
The symbol table creates a hidden
AnnotationBlockfor every function signature while future annotations are active, including signatures with no annotations. Code generation returned early for an unused block without advancing the cursor, so the following function inspected the previous block and could lose its__annotate__closure.This is observable through
functools.singledispatch: a later annotated registration function can have an empty__annotations__mapping.Validation
cargo fmt --check -- crates/codegen/src/compile.rscargo test -p rustpython-codegen(784 passed)def annotated(x: int)retains{'x': 'int'}Summary by CodeRabbit