Skip to content

Conversation

@youknowone
Copy link
Member

@youknowone youknowone commented Jan 17, 2026

Apply reviews from #6718

Summary by CodeRabbit

  • New Features

    • Improved PEP 649 conditional-annotation handling with per-scope tracking across control-flow to preserve annotation ordering.
  • Bug Fixes

    • Enforced forbidding of the identifier debug in annotations, imports, assignments and comprehension targets to raise syntax errors earlier.
  • Tests

    • Added parsing tests covering forbidden-name uses and annotation forms.
  • Chores

    • Bumped on-disk marshaling format version.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 17, 2026

📝 Walkthrough

Walkthrough

Adds PEP 649 conditional-annotation tracking (per-code-unit fields and enter/leave hooks), moves debug forbidden-name enforcement into the symbol-table with a varnames stack, bumps marshal format version 4→5 and adds slice type sentinel, and updates JIT tests/annotation extraction to use Wtf8 keys and forward-ref strings.

Changes

Cohort / File(s) Summary
PEP 649 Conditional Annotation Tracking
crates/codegen/src/compile.rs, crates/codegen/src/ir.rs
Added in_conditional_block and next_conditional_annotation_index to CodeInfo; added enter_conditional_block() / leave_conditional_block() on Compiler; wrapped control-flow constructs to manage conditional annotation context and per-code indexing.
Forbidden-Name Handling → Symbol Table
crates/codegen/src/compile.rs, crates/codegen/src/symboltable.rs, extra_tests/snippets/syntax_forbidden_name.py
Removed is_forbidden_name / check_forbidden_name from codegen; introduced varnames_stack and check_name in SymbolTableBuilder to enforce __debug__ restrictions across nested/annotation scopes; added parser tests for __debug__ misuse.
IR / CodeInfo Struct Update
crates/codegen/src/ir.rs
Exposed two new public fields on CodeInfo and updated code finalization to account for them.
Marshal Format & Slice Type
crates/compiler-core/src/marshal.rs
Bumped FORMAT_VERSION 4→5 and added Type::Slice (b':') handling in deserialization, returning BadType for unsupported slice constants.
JIT Tests & Annotation Extraction (Wtf8 keys)
crates/jit/tests/common.rs, crates/jit/Cargo.toml
Switched annotation map and StackValue::Map keys to Wtf8Buf; updated extraction to accept name refs or forward-ref string constants; added rustpython-wtf8 dev-dependency.

Sequence Diagram(s)

sequenceDiagram
  rect rgba(200,230,255,0.5)
    participant Parser
    participant Compiler
    participant CodeInfo
    participant SymbolTable
  end

  Parser->>Compiler: emit AST node (If/For/With/Try/Match)
  Compiler->>SymbolTable: enter_scope / enter_annotation_scope
  SymbolTable->>SymbolTable: push varnames_stack, seed annotation varnames
  Compiler->>CodeInfo: enter_conditional_block()
  CodeInfo-->>Compiler: in_conditional_block++
  Compiler->>Compiler: compile body (emit conditional annotations)
  Compiler->>CodeInfo: allocate next_conditional_annotation_index when needed
  Compiler->>CodeInfo: leave_conditional_block()
  CodeInfo-->>Compiler: in_conditional_block--
  Compiler->>SymbolTable: leave_annotation_scope / leave_scope
  SymbolTable->>SymbolTable: pop varnames_stack, restore parent varnames
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • ShaharNaveh

Poem

🐰
I hop through blocks where annotations hide,
I mark each conditional with a careful stride.
I guard the debug name from accidental bind,
I stitch small strings to types the parser finds,
Five’s the version now — onward we glide!

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 29.55% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title "Py 3.14 changes fix" is vague and generic, using non-descriptive language that doesn't convey specific information about the changeset's main purpose. Replace with a more specific title that describes the core change, such as: "Refactor forbidden-name checks and add conditional-annotation tracking for PEP 649" or "Move debug validation to symbol table phase."
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing touches
  • 📝 Generate docstrings

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 and usage tips.

@youknowone youknowone changed the title Py 3.14 fix Py 3.14 changes fix Jan 17, 2026
@youknowone youknowone marked this pull request as ready for review January 17, 2026 12:58
Copy link
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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/codegen/src/compile.rs (1)

5936-5955: Fix conditional annotation indexing for mixed class/module annotations.

next_conditional_annotation_index only increments when is_conditional is true. In class scopes that contain both unconditional and conditional annotations, this misaligns indices with compile_module_annotate’s enumeration order, which can cause unconditional annotations to be skipped or the wrong entries to be included.

Allocate an index for every simple annotation when has_conditional_annotations is true, and only gate insertion into __conditional_annotations__ by the conditionality check. Also treat class scope like module scope for tracking purposes.

🔧 Proposed fix
-                    let is_module = self.current_symbol_table().typ == CompilerScope::Module;
-                    let in_conditional_block = self.current_code_info().in_conditional_block > 0;
-                    let is_conditional = is_module || in_conditional_block;
-
-                    if is_conditional {
-                        // Get the current annotation index and increment
-                        let code_info = self.current_code_info();
-                        let annotation_index = code_info.next_conditional_annotation_index;
-                        code_info.next_conditional_annotation_index += 1;
+                    let scope_type = self.current_symbol_table().typ;
+                    let in_conditional_block = self.current_code_info().in_conditional_block > 0;
+                    let is_conditional = matches!(scope_type, CompilerScope::Module | CompilerScope::Class)
+                        || in_conditional_block;
+
+                    // Allocate a stable index for every annotation in this scope.
+                    let code_info = self.current_code_info();
+                    let annotation_index = code_info.next_conditional_annotation_index;
+                    code_info.next_conditional_annotation_index += 1;
+
+                    if is_conditional {
                         // Add index to __conditional_annotations__ set
                         let cond_annotations_name = self.name("__conditional_annotations__");
                         emit!(self, Instruction::LoadName(cond_annotations_name));
                         self.emit_load_const(ConstantData::Integer {
                             value: annotation_index.into(),
                         });
                         emit!(self, Instruction::SetAdd { i: 0_u32 });
                         emit!(self, Instruction::PopTop);
                     }
🤖 Fix all issues with AI agents
In `@crates/codegen/src/symboltable.rs`:
- Around line 991-995: The __debug__ binding check must be centralized so all
binding paths are validated: add a guard in register_name that rejects the
identifier "__debug__" (returning the same error check_name would) before any
registration occurs, and remove or avoid duplicate checks elsewhere; update
calls that currently call register_ident or register_name bypassing validation
(e.g., import alias handling and MatchAs capture registration) to call
register_name so imports, pattern-match captures, parameters, and comprehension
targets (including ExpressionContext::Iter) are validated; alternatively, if you
prefer not to centralize, treat ExpressionContext::Iter as
ExpressionContext::Store in the comprehension-target scanner and ensure
register_ident callers for imports and MatchAs invoke check_name first.

In `@crates/jit/tests/common.rs`:
- Around line 101-113: The current logic silently skips malformed annotation
values (when val_is_const is true but code.constants.get(val_idx) is not
ConstantData::Str, or when val_is_const is false but code.names.get(val_idx) is
None), causing later generic panics; update the branch around val_is_const /
val_idx resolution in crates/jit/tests/common.rs (the block that constructs
type_name and then calls annotations.insert(param_name,
StackValue::String(type_name))) to fail fast: when a const entry exists but is
not ConstantData::Str, or when a name index is missing, return/raise a clear
error or panic that includes param_name and val_idx (and ideally the offending
constant's debug info), rather than treating it as None, so malformed annotate
bytecode reports an explicit diagnostic.

Comment on lines 101 to 113
// Value can be a name (type ref) or a const string (forward ref)
let type_name = if val_is_const {
match code.constants.get(val_idx) {
Some(ConstantData::Str { value }) => {
Some(value.to_string_lossy().into_owned())
}
_ => None,
}
} else {
code.names.get(val_idx).cloned()
};
if let Some(type_name) = type_name {
annotations.insert(param_name, StackValue::String(type_name));
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Don’t silently drop unsupported annotation values.
If a const isn’t a string (or the name index is missing), the annotation is skipped and later fails with a generic “Argument have annotation” panic. Prefer failing fast with a clear message so malformed annotate bytecode is obvious.

🔧 Proposed fix (fail fast with explicit diagnostics)
-                            let type_name = if val_is_const {
-                                match code.constants.get(val_idx) {
-                                    Some(ConstantData::Str { value }) => {
-                                        Some(value.to_string_lossy().into_owned())
-                                    }
-                                    _ => None,
-                                }
-                            } else {
-                                code.names.get(val_idx).cloned()
-                            };
-                            if let Some(type_name) = type_name {
-                                annotations.insert(param_name, StackValue::String(type_name));
-                            }
+                            let type_name = if val_is_const {
+                                match code.constants.get(val_idx) {
+                                    Some(ConstantData::Str { value }) => {
+                                        value.to_string_lossy().into_owned()
+                                    }
+                                    Some(other) => {
+                                        panic!("Unsupported annotation const: {:?}", other)
+                                    }
+                                    None => panic!("Annotation const idx out of range: {val_idx}"),
+                                }
+                            } else {
+                                code.names
+                                    .get(val_idx)
+                                    .unwrap_or_else(|| {
+                                        panic!("Annotation name idx out of range: {val_idx}")
+                                    })
+                                    .clone()
+                            };
+                            annotations.insert(param_name, StackValue::String(type_name));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Value can be a name (type ref) or a const string (forward ref)
let type_name = if val_is_const {
match code.constants.get(val_idx) {
Some(ConstantData::Str { value }) => {
Some(value.to_string_lossy().into_owned())
}
_ => None,
}
} else {
code.names.get(val_idx).cloned()
};
if let Some(type_name) = type_name {
annotations.insert(param_name, StackValue::String(type_name));
// Value can be a name (type ref) or a const string (forward ref)
let type_name = if val_is_const {
match code.constants.get(val_idx) {
Some(ConstantData::Str { value }) => {
value.to_string_lossy().into_owned()
}
Some(other) => {
panic!("Unsupported annotation const: {:?}", other)
}
None => panic!("Annotation const idx out of range: {val_idx}"),
}
} else {
code.names
.get(val_idx)
.unwrap_or_else(|| {
panic!("Annotation name idx out of range: {val_idx}")
})
.clone()
};
annotations.insert(param_name, StackValue::String(type_name));
🤖 Prompt for AI Agents
In `@crates/jit/tests/common.rs` around lines 101 - 113, The current logic
silently skips malformed annotation values (when val_is_const is true but
code.constants.get(val_idx) is not ConstantData::Str, or when val_is_const is
false but code.names.get(val_idx) is None), causing later generic panics; update
the branch around val_is_const / val_idx resolution in
crates/jit/tests/common.rs (the block that constructs type_name and then calls
annotations.insert(param_name, StackValue::String(type_name))) to fail fast:
when a const entry exists but is not ConstantData::Str, or when a name index is
missing, return/raise a clear error or panic that includes param_name and
val_idx (and ideally the offending constant's debug info), rather than treating
it as None, so malformed annotate bytecode reports an explicit diagnostic.

Copy link
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: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/codegen/src/compile.rs (1)

5934-5956: Class-scope unconditional annotations are omitted from __annotate__ when any conditional annotation exists in the scope.

Because is_conditional only returns true for module-level annotations or when inside a conditional block, top-level class annotations are never added to __conditional_annotations__. However, compile_module_annotate uses set membership to determine which annotations to include, so an empty set results in all annotations being skipped.

The suggested fix is correct: include CompilerScope::Class in the is_conditional check to ensure class-scope annotations are properly tracked.

🛠️ Suggested fix for class-scope inclusion
- let is_module = self.current_symbol_table().typ == CompilerScope::Module;
- let in_conditional_block = self.current_code_info().in_conditional_block > 0;
- let is_conditional = is_module || in_conditional_block;
+ let scope_type = self.current_symbol_table().typ;
+ let in_conditional_block = self.current_code_info().in_conditional_block > 0;
+ let is_conditional =
+     matches!(scope_type, CompilerScope::Module | CompilerScope::Class) || in_conditional_block;

A test case with a class containing both conditional and unconditional annotations would catch this regression.

♻️ Duplicate comments (2)
crates/jit/tests/common.rs (1)

94-113: Fail fast on unsupported annotation values (still silently dropped).

This still skips invalid annotation values by returning None, which leads to a later generic panic. Please fail fast with a targeted error to surface malformed annotate bytecode immediately.

crates/codegen/src/symboltable.rs (1)

2181-2208: Treat ExpressionContext::Iter as a binding for __debug__.
Comprehension/loop targets use Iter; those should be rejected just like Store.

🛠️ Proposed fix
-            match context {
-                ExpressionContext::Store => {
+            match context {
+                ExpressionContext::Store | ExpressionContext::Iter => {
                     return Err(SymbolTableError {
                         error: "cannot assign to __debug__".to_owned(),
                         location,
                     });
                 }
Is "for __debug__ in iterable:" a SyntaxError in Python (i.e., is __debug__ forbidden as an iteration target)?

Copy link
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: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/codegen/src/compile.rs (1)

5936-5959: Conditional-annotation indices can desync from __annotate__ enumeration.
next_conditional_annotation_index is incremented for every simple annotated assignment compiled (including those nested in if/for/while/try/match), but compile_module_annotate enumerates only top-level annotations via collect_simple_annotations. This can shift indices and cause executed annotations to be omitted from __annotate__ (e.g., a conditional annotation before a top-level one).

Consider aligning enumeration with compilation order by recursively collecting simple annotations from nested bodies (or, alternatively, only increment the index for annotations that compile_module_annotate will enumerate).

🔧 Possible fix: recursively collect annotations in AST order
-    fn collect_simple_annotations(body: &[Stmt]) -> Vec<(&str, &Expr)> {
-        let mut annotations = Vec::new();
-        for stmt in body {
-            if let Stmt::AnnAssign(StmtAnnAssign {
-                target,
-                annotation,
-                simple,
-                ..
-            }) = stmt
-                && *simple
-                && let Expr::Name(ExprName { id, .. }) = target.as_ref()
-            {
-                annotations.push((id.as_str(), annotation.as_ref()));
-            }
-        }
-        annotations
-    }
+    fn collect_simple_annotations(body: &[Stmt]) -> Vec<(&str, &Expr)> {
+        fn walk<'a>(stmts: &'a [Stmt], out: &mut Vec<(&'a str, &'a Expr)>) {
+            for stmt in stmts {
+                match stmt {
+                    Stmt::AnnAssign(StmtAnnAssign { target, annotation, simple, .. })
+                        if *simple && matches!(target.as_ref(), Expr::Name(_)) =>
+                    {
+                        if let Expr::Name(ExprName { id, .. }) = target.as_ref() {
+                            out.push((id.as_str(), annotation.as_ref()));
+                        }
+                    }
+                    Stmt::If(StmtIf { body, elif_else_clauses, .. }) => {
+                        walk(body, out);
+                        for clause in elif_else_clauses {
+                            walk(&clause.body, out);
+                        }
+                    }
+                    Stmt::For(StmtFor { body, orelse, .. })
+                    | Stmt::While(StmtWhile { body, orelse, .. }) => {
+                        walk(body, out);
+                        walk(orelse, out);
+                    }
+                    Stmt::With(StmtWith { body, .. }) => walk(body, out),
+                    Stmt::Try(StmtTry { body, handlers, orelse, finalbody, .. }) => {
+                        walk(body, out);
+                        for handler in handlers {
+                            if let ExceptHandler::ExceptHandler(ExceptHandlerExceptHandler { body, .. }) = handler {
+                                walk(body, out);
+                            }
+                        }
+                        walk(orelse, out);
+                        walk(finalbody, out);
+                    }
+                    Stmt::Match(StmtMatch { cases, .. }) => {
+                        for case in cases {
+                            walk(&case.body, out);
+                        }
+                    }
+                    _ => {}
+                }
+            }
+        }
+        let mut annotations = Vec::new();
+        walk(body, &mut annotations);
+        annotations
+    }
♻️ Duplicate comments (2)
crates/codegen/src/symboltable.rs (1)

2181-2225: debug can still slip through pattern-capture bindings.

Now that register_name no longer enforces the forbidden-name rule, pattern captures (MatchAs, MatchStar, mapping rest) still call register_ident without check_name. That allows __debug__ to be bound via pattern matching, which should be rejected.

Please add check_name(..., ExpressionContext::Store, ...) before those register_ident calls.

🔧 Proposed fix
 fn scan_pattern(&mut self, pattern: &Pattern) -> SymbolTableResult {
     use Pattern::*;
     match pattern {
         MatchMapping(PatternMatchMapping { keys, patterns, rest, .. }) => {
             self.scan_expressions(keys, ExpressionContext::Load)?;
             self.scan_patterns(patterns)?;
             if let Some(rest) = rest {
+                self.check_name(rest.as_str(), ExpressionContext::Store, rest.range)?;
                 self.register_ident(rest, SymbolUsage::Assigned)?;
             }
         }
         MatchStar(PatternMatchStar { name, .. }) => {
             if let Some(name) = name {
+                self.check_name(name.as_str(), ExpressionContext::Store, name.range)?;
                 self.register_ident(name, SymbolUsage::Assigned)?;
             }
         }
         MatchAs(PatternMatchAs { pattern, name, .. }) => {
             if let Some(pattern) = pattern {
                 self.scan_pattern(pattern)?;
             }
             if let Some(name) = name {
+                self.check_name(name.as_str(), ExpressionContext::Store, name.range)?;
                 self.register_ident(name, SymbolUsage::Assigned)?;
             }
         }
         MatchOr(PatternMatchOr { patterns, .. }) => self.scan_patterns(patterns)?,
     }
     Ok(())
 }
crates/jit/tests/common.rs (1)

95-144: Warn-and-skip still hides malformed annotations.

The new warnings still drop malformed entries, which later causes a generic “Argument have annotation” panic. Prefer failing fast here with an explicit panic/error that includes the offending index/type.

🔧 Proposed fix
-                                match code.constants.get(val_idx) {
-                                    Some(ConstantData::Str { value }) => {
-                                        Some(value.as_str().map(|s| s.to_owned()).unwrap_or_else(
-                                            |_| value.to_string_lossy().into_owned(),
-                                        ))
-                                    }
-                                    Some(other) => {
-                                        eprintln!(
-                                            "Warning: Malformed annotation for '{:?}': expected string constant at index {}, got {:?}",
-                                            param_name, val_idx, other
-                                        );
-                                        None
-                                    }
-                                    None => {
-                                        eprintln!(
-                                            "Warning: Malformed annotation for '{:?}': constant index {} out of bounds (len={})",
-                                            param_name,
-                                            val_idx,
-                                            code.constants.len()
-                                        );
-                                        None
-                                    }
-                                }
+                                match code.constants.get(val_idx) {
+                                    Some(ConstantData::Str { value }) => Some(
+                                        value
+                                            .as_str()
+                                            .map(|s| s.to_owned())
+                                            .unwrap_or_else(|_| value.to_string_lossy().into_owned()),
+                                    ),
+                                    Some(other) => panic!(
+                                        "Unsupported annotation const for '{:?}' at idx {}: {:?}",
+                                        param_name, val_idx, other
+                                    ),
+                                    None => panic!(
+                                        "Annotation const idx out of bounds for '{:?}': {} (len={})",
+                                        param_name, val_idx, code.constants.len()
+                                    ),
+                                }
                             } else {
                                 match code.names.get(val_idx) {
-                                    Some(name) => Some(name.clone()),
-                                    None => {
-                                        eprintln!(
-                                            "Warning: Malformed annotation for '{}': name index {} out of bounds (len={})",
-                                            param_name,
-                                            val_idx,
-                                            code.names.len()
-                                        );
-                                        None
-                                    }
+                                    Some(name) => Some(name.clone()),
+                                    None => panic!(
+                                        "Annotation name idx out of bounds for '{:?}': {} (len={})",
+                                        param_name, val_idx, code.names.len()
+                                    ),
                                 }
                             };
🧹 Nitpick comments (1)
crates/compiler-core/src/marshal.rs (1)

471-476: Consider a distinct error for unsupported Slice constants.

Returning BadType hides the difference between “unknown marker” vs “known but unsupported.” A dedicated error improves diagnostics and feature‑gating.

💡 Possible refactor
 pub enum MarshalError {
     /// Unexpected End Of File
     Eof,
     /// Invalid Bytecode
     InvalidBytecode,
     /// Invalid utf8 in string
     InvalidUtf8,
     /// Invalid source location
     InvalidLocation,
     /// Bad type marker
     BadType,
+    /// Known type marker that RustPython does not yet support
+    UnsupportedType(u8),
 }
 
 impl core::fmt::Display for MarshalError {
     fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
         match self {
             Self::Eof => f.write_str("unexpected end of data"),
             Self::InvalidBytecode => f.write_str("invalid bytecode"),
             Self::InvalidUtf8 => f.write_str("invalid utf8"),
             Self::InvalidLocation => f.write_str("invalid source location"),
             Self::BadType => f.write_str("bad type marker"),
+            Self::UnsupportedType(marker) => write!(f, "unsupported type marker: {marker:`#x`}"),
         }
     }
 }
 
         Type::Slice => {
             // Slice constants are not yet supported in RustPython
             // This would require adding a Slice variant to ConstantData enum
             // For now, return an error if we encounter a slice in marshal data
-            return Err(MarshalError::BadType);
+            return Err(MarshalError::UnsupportedType(Type::Slice as u8));
         }

@youknowone youknowone merged commit 6df3753 into RustPython:main Jan 17, 2026
13 checks passed
@youknowone youknowone deleted the py-3.14-fix branch January 17, 2026 16:02
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