Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions crates/codegen/src/compile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5083,6 +5083,14 @@ impl<'warnings> Compiler<'warnings> {
func_range: TextRange,
) -> CompileResult<bool> {
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);
Comment on lines 5085 to 5094

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.

}

Expand Down Expand Up @@ -31247,6 +31255,25 @@ def f(x: T): pass
);
}

#[test]
fn future_unannotated_function_does_not_hide_next_annotation_block() {
let code = compile_exec(
"\
from __future__ import annotations
def plain(x): pass
def annotated(x: int): pass
",
);
let annotate = find_direct_child_code(&code, "__annotate__")
.expect("second function must retain its annotation closure");
assert!(
annotate.constants.iter().any(
|constant| matches!(constant, ConstantData::Str { value } if value.as_str() == Ok("int"))
),
"annotation closure must belong to the annotated function"
);
}

#[test]
fn deferred_annotation_format_name_does_not_capture_helper_parameter() {
let code = compile_exec(
Expand Down
Loading