Skip to content

Fix future annotation block alignment - #8506

Merged
youknowone merged 1 commit into
RustPython:mainfrom
youknowone:agent/consume-unannotated-annotation-block
Aug 12, 2026
Merged

Fix future annotation block alignment#8506
youknowone merged 1 commit into
RustPython:mainfrom
youknowone:agent/consume-unannotated-annotation-block

Conversation

@youknowone

@youknowone youknowone commented Aug 12, 2026

Copy link
Copy Markdown
Member

Summary

  • consume unused hidden function-signature annotation blocks under from __future__ import annotations
  • keep the hidden annotation cursor aligned for the next function
  • add a regression test for an unannotated function followed by an annotated one

Root cause

The symbol table creates a hidden AnnotationBlock for 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.rs
  • cargo test -p rustpython-codegen (784 passed)
  • CPython/PyPy oracle: an unannotated function followed by def annotated(x: int) retains {'x': 'int'}

Summary by CodeRabbit

  • Bug Fixes
    • Fixed annotation handling when unannotated and annotated functions appear together.
    • Ensured later annotated functions retain their annotation metadata correctly.

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The compiler now consumes hidden annotation symbols for unannotated functions. A regression test verifies that a later annotated function keeps its __annotate__ closure and "int" annotation.

Changes

Annotation scope handling

Layer / File(s) Summary
Consume unannotated function annotation symbols
crates/codegen/src/compile.rs
Unannotated functions now push and pop their hidden annotation symbol table. A regression test verifies the generated closure for a following annotated function.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

  • RustPython/RustPython#8498: Both changes modify annotation-scope handling in crates/codegen/src/compile.rs, but address different issues.

Suggested labels: z-ca-2026

Suggested reviewers: shaharnaveh, kyokuping

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: correcting future annotation block alignment.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ 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

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

@youknowone
youknowone marked this pull request as ready for review August 12, 2026 21:16

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
crates/codegen/src/compile.rs (1)

5085-5094: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Optional: extract a shared helper for the repeated push-then-pop pattern.

compile_annotations_closure (Line 5091-5093) and consume_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 example fn 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

📥 Commits

Reviewing files that changed from the base of the PR and between 6701520 and 75cc025.

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

Comment on lines 5085 to 5094
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);

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.

🎯 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.rs

Repository: 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.rs

Repository: 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.rs

Repository: 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")
PY

Repository: 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")
PY

Repository: 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.

@youknowone
youknowone merged commit 212c0d0 into RustPython:main Aug 12, 2026
28 checks passed
@youknowone
youknowone deleted the agent/consume-unannotated-annotation-block branch August 12, 2026 22:24
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.

1 participant